Merge pull request #18488 from MetaMask/Version-v10.29.0
Version v10.29.0 RC
@ -701,7 +701,7 @@ jobs:
|
||||
|
||||
test-e2e-firefox-snaps:
|
||||
executor: node-browsers
|
||||
parallelism: 2
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -738,7 +738,7 @@ jobs:
|
||||
|
||||
test-e2e-chrome-snaps:
|
||||
executor: node-browsers
|
||||
parallelism: 2
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
|
@ -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',
|
||||
|
@ -3,10 +3,9 @@ PASSWORD=METAMASK PASSWORD
|
||||
INFURA_PROJECT_ID=00000000000
|
||||
SEGMENT_WRITE_KEY=
|
||||
SWAPS_USE_DEV_APIS=
|
||||
PUBNUB_PUB_KEY=
|
||||
PUBNUB_SUB_KEY=
|
||||
PORTFOLIO_URL=
|
||||
TRANSACTION_SECURITY_PROVIDER=
|
||||
MULTICHAIN=
|
||||
|
||||
; Set this to test changes to the phishing warning page.
|
||||
PHISHING_WARNING_PAGE_URL=
|
||||
|
@ -38,6 +38,9 @@ addParameters({
|
||||
],
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
expanded: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const globalTypes = {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { draftTransactionInitialState } from '../ui/ducks/send';
|
||||
import { HardwareKeyringTypes } from '../shared/constants/hardware-wallets';
|
||||
import { KeyringType } from '../shared/constants/keyring';
|
||||
|
||||
const state = {
|
||||
invalidCustomNetwork: {
|
||||
@ -1168,20 +1168,23 @@ const state = {
|
||||
unapprovedTypedMessages: {},
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
keyringTypes: [
|
||||
HardwareKeyringTypes.imported,
|
||||
HardwareKeyringTypes.hdKeyTree,
|
||||
HardwareKeyringTypes.trezor,
|
||||
HardwareKeyringTypes.ledger,
|
||||
KeyringType.imported,
|
||||
KeyringType.hdKeyTree,
|
||||
KeyringType.trezor,
|
||||
KeyringType.ledger,
|
||||
],
|
||||
keyrings: [
|
||||
{
|
||||
type: HardwareKeyringTypes.hdKeyTree,
|
||||
type: KeyringType.hdKeyTree,
|
||||
accounts: [
|
||||
'0x64a845a5b02460acf8a3d84503b0d68d028b4bb4',
|
||||
'0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e',
|
||||
'0x9d0ba4ddac06032527b140912ec808ab9451b788',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: KeyringType.ledger,
|
||||
accounts: ['0x9d0ba4ddac06032527b140912ec808ab9451b788'],
|
||||
},
|
||||
],
|
||||
networkConfigurations: {
|
||||
'test-networkConfigurationId-1': {
|
||||
|
38
CHANGELOG.md
@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [10.29.0]
|
||||
### Added
|
||||
- [FLASK] Redesign snaps permission screens ([#18372](https://github.com/MetaMask/metamask-extension/pull/18372))
|
||||
- [FLASK] Add tooltips to show info about a permission ([#17685](https://github.com/MetaMask/metamask-extension/pull/17685))
|
||||
|
||||
### Changed
|
||||
- Add Ledger instructions to the Sign In With Ethereum page ([#18589](https://github.com/MetaMask/metamask-extension/pull/18589))
|
||||
- Removed advanced gas toggle from the settings ([#18138](https://github.com/MetaMask/metamask-extension/pull/18138))
|
||||
- Improve security provider warning messaging, to give users more info about transactions security providers flag as potentially suspicious ([#18545](https://github.com/MetaMask/metamask-extension/pull/18545))
|
||||
- Update wording on token allowance screen: replace "contract" with "third party" ([#18101](https://github.com/MetaMask/metamask-extension/pull/18101))
|
||||
- Update wording on token allowance screen: change the review spending cap header text ([#18214](https://github.com/MetaMask/metamask-extension/pull/18214))
|
||||
- Added fallback copy for when we're not able to retrieve a erc721 or erc1155 name in the setApprovalForAll screen ([#17992](https://github.com/MetaMask/metamask-extension/pull/17992))
|
||||
- Bump contract-metadata version, so that tokens added ([v2.3.0](https://github.com/MetaMask/contract-metadata/pull/1169)) and ([v2.3.1](https://github.com/MetaMask/contract-metadata/pull/1173)) are included in the default MetaMask token lists ([#18589](https://github.com/MetaMask/metamask-extension/pull/18589))
|
||||
- [FLASK] Redesign snap content delineator ([#18385](https://github.com/MetaMask/metamask-extension/pull/18385))
|
||||
- [FLASK] Redesign key management modal ([#18263](https://github.com/MetaMask/metamask-extension/pull/18263))
|
||||
- [FLASK] Redesign snap authorship component ([#18262](https://github.com/MetaMask/metamask-extension/pull/18262))
|
||||
- [FLASK] Improve design of snaps settings page when no snaps are installed ([#18172](https://github.com/MetaMask/metamask-extension/pull/18172))
|
||||
- [FLASK] Remove permission footer in snap install/update flow ([#18240](https://github.com/MetaMask/metamask-extension/pull/18240))
|
||||
- [FLASK] **BREAKING:** Snaps are now required to request permission for at least one handler permission (e.g. `onRpcRequest`) ([#18371](https://github.com/MetaMask/metamask-extension/pull/18371))
|
||||
- [FLASK] Fix issues with using `atob` and `btoa` in snaps ([#18371](https://github.com/MetaMask/metamask-extension/pull/18371))
|
||||
- [FLASK] Combine the snap installation popups into a single popup ([#18142](https://github.com/MetaMask/metamask-extension/pull/18142))
|
||||
- [FLASK] **BREAKING:** Disallow snaps requesting `eth_requestAccounts` and `wallet_requestSnaps` RPC methods ([#18142](https://github.com/MetaMask/metamask-extension/pull/18142))
|
||||
|
||||
### Fixed
|
||||
- Add a title to the security provider "What's New" notification ([#18526](https://github.com/MetaMask/metamask-extension/pull/18526))
|
||||
- Fix cursor styling on Sign Typed Data screen to use the 'pointer' cursor ([#18046](https://github.com/MetaMask/metamask-extension/pull/18046))
|
||||
- Fix layout/styling of the "Hold to reveal" button in the SRP reveal flow([#18496](https://github.com/MetaMask/metamask-extension/pull/18496))
|
||||
- Fixed hardware wallet info popup on token allowance screen ([#17881](https://github.com/MetaMask/metamask-extension/pull/17881))
|
||||
- Fix send flow on Optimism Goerli network ([#18478](https://github.com/MetaMask/metamask-extension/pull/18478))
|
||||
- Disabled button for Import Tokens Modal when no token is selected ([#18396](https://github.com/MetaMask/metamask-extension/pull/18396))
|
||||
- [FLASK] Fix crash when requesting unknown snap permission ([#18447](https://github.com/MetaMask/metamask-extension/pull/18447))
|
||||
- [FLASK] Fix overflow issues with text coming from snap UI ([#18169](https://github.com/MetaMask/metamask-extension/pull/18169))
|
||||
- [FLASK] Snaps e2e test stability improvements ([#18090](https://github.com/MetaMask/metamask-extension/pull/18090))
|
||||
|
||||
## [10.28.3]
|
||||
### Fixed
|
||||
- Fix network switching prompted by dapps for users that added the network prior to v10.28.0. ([#18513](https://github.com/MetaMask/metamask-extension/pull/18513))
|
||||
@ -15,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix network switching prompted by dapps by fixing the `wallet_switchEthereumChain` handler. ([#18483](https://github.com/MetaMask/metamask-extension/pull/18483))
|
||||
- Fix to ensure all users see the NFT and transaction security notifications ([#18460](https://github.com/MetaMask/metamask-extension/pull/18460))
|
||||
- Fix issue blocking Hindi, Japanese and Turkish language users from installing from the Chrome store ([#18487](https://github.com/MetaMask/metamask-extension/pull/18487))
|
||||
- [FLASK] Fix window overflow issues with snap UI text ([#18169](https://github.com/MetaMask/metamask-extension/pull/18169))
|
||||
|
||||
## [10.28.1]
|
||||
### Changed
|
||||
@ -3653,7 +3688,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Uncategorized
|
||||
- Added the ability to restore accounts from seed words.
|
||||
|
||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.28.3...HEAD
|
||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.29.0...HEAD
|
||||
[10.29.0]: https://github.com/MetaMask/metamask-extension/compare/v10.28.3...v10.29.0
|
||||
[10.28.3]: https://github.com/MetaMask/metamask-extension/compare/v10.28.2...v10.28.3
|
||||
[10.28.2]: https://github.com/MetaMask/metamask-extension/compare/v10.28.1...v10.28.2
|
||||
[10.28.1]: https://github.com/MetaMask/metamask-extension/compare/v10.28.0...v10.28.1
|
||||
|
@ -142,6 +142,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
|
||||
- [How to use the TREZOR emulator](./docs/trezor-emulator.md)
|
||||
- [Developing on MetaMask](./development/README.md)
|
||||
- [How to generate a visualization of this repository's development](./development/gource-viz.sh)
|
||||
- [How to add new confirmations](./docs/confirmations.md)
|
||||
|
||||
## Dapp Developer Resources
|
||||
|
||||
|
27
app/_locales/am/messages.json
generated
@ -663,12 +663,6 @@
|
||||
"settings": {
|
||||
"message": "ቅንብሮች"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "የላቁ የነዳጅ ቁጥጥሮች"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "በላክ እና አረጋግጥ ማያዎች ላይ የነዳጅ ዋጋን ለማሳየትና ቁጥጥሮችን ለመገደብ ይህን ይምረጡ።"
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "ልወጣን በ Testnets ላይ አሳይ"
|
||||
},
|
||||
@ -732,27 +726,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "ምልክቱ 11 ቁምፊዎች ወይም ከዚያ ያነሰ መሆን አለበት።"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "ከሞባይል ጋር አሳምር"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "ይህን ኮድ ስካን ሲያደርጉ ሌላ ሰው የእርስዎን ማያ እየተመለከተ አለመሆኑን ያረጋግጡ"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "ውሂብዎ በሚገባ ተሳምሯል። በ MetaMask የሞባይል መተግበሪያ ይደሰቱ!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "መለያዎችዎንና መረጃዎችዎን ከሞባይል መሳሪያዎ ጋር ያሳምሩ። የ MetaMask የሞባይል መተግበሪያን ይክፈቱ፣ ወደ \"ቅንብሮች\" ይሂዱና \"ከማሰሺያ ቅጥያ አሳምር\" የሚለውን ይንኩ"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "የ MetaMask የሞባይል መተግበሪያን ለመጀመሪያ ጊዜ ገና እየከፈቱ ከሆነ፣ በስልክዎ ላይ ያሉትን ቅደም ተከተሎች ብቻ ይከተሉ።"
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "ይህን ኮድ በ MetaMask የሞባይል መተግበሪያዎ ስካን ያድርጉ"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "ከሞባይል ጋር አሳምር"
|
||||
},
|
||||
"terms": {
|
||||
"message": "የአጠቃቀም ደንቦች"
|
||||
},
|
||||
|
27
app/_locales/ar/messages.json
generated
@ -675,12 +675,6 @@
|
||||
"settings": {
|
||||
"message": "الإعدادات"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "أدوات التحكم المتقدمة للغاز"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "حدد هذا لإظهار سعر عملة جاس والحد من الضوابط مباشرة على شاشات الإرسال والتأكيد."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "عرض التحويل على Testnets"
|
||||
},
|
||||
@ -744,27 +738,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "يجب أن يكون الرمز 11 حرفًا أو أقل."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "مزامنة مع الهاتف المحمول"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "تأكد من عدم قيام أي شخص آخر بالنظر إلى شاشتك عند مسح هذا الرمز"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "تمت مزامنة بياناتك بنجاح. استمتع بتطبيق MetaMask للهاتف المحمول!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "يمكنك مزامنة حساباتك ومعلوماتك مع جهازك المحمول. افتح تطبيق MetaMask على الجهاز المحمول وانتقل إلى \"الإعدادات\" ثم انقر على \"المزامنة من إضافة المتصفح\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "إذا قمت للتوّ بفتح تطبيق MetaMask للهواتف المحمولة لأول مرة، فاتبع الخطوات الموجودة في هاتفك."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "امسح هذا الرمز ضوئياً باستخدام تطبيق MetaMask للهواتف المحمولة"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "المزامنة مع الجهاز المحمول"
|
||||
},
|
||||
"terms": {
|
||||
"message": "شروط الاستخدام"
|
||||
},
|
||||
|
27
app/_locales/bg/messages.json
generated
@ -674,12 +674,6 @@
|
||||
"settings": {
|
||||
"message": "Настройки"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Разширено управление на газа"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Изберете това, за да покажете цените на газа и ограничите контрола директно на екраните за изпращане и потвърждение."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Показване на преобразуването на Testnets"
|
||||
},
|
||||
@ -743,27 +737,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Символът трябва да е 11 символа или по-малко."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Синхронизиране с мобилни устройства"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Уверете се, че никой друг не гледа екрана ви, когато сканирате този код"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Вашите данни са синхронизирани успешно. Насладете се на мобилното приложение MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Можете да синхронизирате вашите акаунти и информация с мобилното си устройство. Отворете мобилното приложение MetaMask, отидете на „Настройки“ и докоснете „Синхронизиране от разширението на браузъра“"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Ако отворите приложението MetaMask Mobile за първи път, просто следвайте стъпките в телефона си."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Сканирайте този код с вашето мобилно приложение MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Синхронизиране с мобилни устройства"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Условия за ползване"
|
||||
},
|
||||
|
27
app/_locales/bn/messages.json
generated
@ -672,12 +672,6 @@
|
||||
"settings": {
|
||||
"message": "সেটিংস"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "উন্নত গ্যাস নিয়ন্ত্রণসমূহ"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "গ্যাসের মূল্য দেখাতে এটি নির্বাচন করুন এবং পাঠানোর এবং নিশ্চিতকরণের স্ক্রিনগুলিতে নিয়ন্ত্রণগুলি সরাসরি সীমিত করুন।"
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Testnets এ রূপান্তর দেখান"
|
||||
},
|
||||
@ -741,27 +735,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "প্রতীকটি 11 টি অক্ষর বা তার চেয়ে কম হতে হবে।"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "মোবাইল দিয়ে সিঙ্ক করুন"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "এই কোডটি স্ক্যান করার সময় কেউ আপনার স্ক্রিনটি দেখছে না তা নিশ্চিত করুন"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "আপনার ডেটা সফলভাবে সিঙ্ক করা হয়েছে। MetaMask মোবাইল অ্যাপ উপভোগ করুন!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "আপনি আপনার অ্যাকাউন্ট এবং তথ্য আপনার মোবাইল ডিভাইস দিয়ে সিঙ্ক করতে পারেন। MetaMask মোবাইল অ্যাপটি খুলুন, \"সেটিংস\" এ যান এবং \"ব্রাউজার এক্সটেনশন থেকে সিঙ্ক করুন\" এ ট্যাপ করুন "
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "আপনি প্রথমবারের জন্য MetaMask খোলার পরে, শুধু আপনার ফোনে পদক্ষেপগুলি অনুসরণ করুন। "
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "আপনার MetaMask মোবাইল অ্যাপ দিয়ে এই কোডটি স্ক্যান করুন"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "মোবাইল দিয়ে সিঙ্ক করুন"
|
||||
},
|
||||
"terms": {
|
||||
"message": "ব্যবহারের শর্তাবলী"
|
||||
},
|
||||
|
27
app/_locales/ca/messages.json
generated
@ -656,12 +656,6 @@
|
||||
"settings": {
|
||||
"message": "Configuració"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controls de gas avançats"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Selecciona això per a mostrar el preu del gas i els controls de límit directament a les pantalles d'enviament i confirmació."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Mostra Conversió a Testnets"
|
||||
},
|
||||
@ -725,27 +719,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "El símbol ha de tenir com a mínim 11 caràcters."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronitza amb el mòbil"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Assegura't que no hi ha ningú mirant la teva pantalla quan escanegis aquest codi"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Les teves dades s'han sincronitzat amb èxit. Disfruta de l'app mòbil de MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Pots sincronitzar els teus comptes i la teva informació amb el teu dispositiu mòbil. Obre l'app mòbil de MetaTask, ves a \"Configuració\" i \"Sincronitzar desde l"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Si acabes d'obrir l'app mòbil MetaMask per primer cop, tan sols has de seguir els passos del teu telèfon."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Escaneja aquest codi amb la teva aplicació mòbil de MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronitzar amb mòbil"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Condicions d'ús"
|
||||
},
|
||||
|
27
app/_locales/da/messages.json
generated
@ -656,12 +656,6 @@
|
||||
"settings": {
|
||||
"message": "Indstillinger "
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Avanceret Gas-styring"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Vælg dette for at vise brændstofprisen og begræns styringen direkte på afsendelses- og bekræftelseskærmene."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Vis konvertering på testnet"
|
||||
},
|
||||
@ -722,27 +716,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbolet skal være mindst 11 tegn."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synkronisér med mobil"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Sørg for at der ikke er nogen der kigger på din skærm, når du scanner denne kode"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Dine data er blevet synkroniseret korrekt. Nyd MetaMask-mobilappen!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Du kan synkronisere dine konti og informationer med din mobilenhed. Åbn MetaMask-mobilappen, gå til \"Indstillinger\" og tryk på \"Synkroniser fra browserudvidelse\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Hvis du netop har åbnet MetaMask-mobilappen for første gang, skal du blot følge trinnene på din telefon."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Scan denne kode med din MetaMask-mobilapp"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synkroniser med mobil"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Brugsbetingelser"
|
||||
},
|
||||
|
59
app/_locales/de/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Auf Etherscan anzeigen"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Erweitern Sie Ihre Web3-Erfahrung"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Ansicht erweitern"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Fehlerhafte Daten"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Verwalten Sie Ihre installierten Snaps"
|
||||
},
|
||||
"max": {
|
||||
"message": "Max."
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Sie sehen Ihren Token nicht?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "Die Funktion \"Mit Erweiterung synchronisieren\" ist vorübergehend deaktiviert. Wenn Sie Ihre Erweiterungs-Wallet auf MetaMask mobile verwenden möchten, dann gehen Sie in Ihrer mobilen App zurück zu den Einrichtungsoptionen für die Wallet und wählen Sie die Option \"Import mit Geheime Wiederherstellungsphrase\". Verwenden Sie die Geheime Wiederherstellungsphrase Ihrer Erweiterungs-Wallet, um Ihre Wallet in Mobile zu importieren."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Mehr in Kürze ..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Nonce ist höher als vorgeschlagen nonce von $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "NFT kann nicht hinzugefügt werden, da die Eigentumsangaben nicht übereinstimmen. Stellen Sie sicher, dass Sie die richtigen Informationen eingegeben haben."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Zeigen"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Erweiterte Gaskontrollen"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Wählen Sie dies aus, um den Gaspreis und die Limitkontrollen direkt auf den Senden- und Bestätigen-Bildschirmen anzuzeigen."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Umwandlung auf Testnets anzeigen"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Sie gewähren dem Snap „$1“ wichtige $2-Zugriffsrechte. Dies kann nicht rückgängig gemacht werden und gibt „$1“ Kontrolle über Ihre $2-Konten und Vermögenswerte. Stellen Sie sicher, dass Sie „$1“ vertrauen, bevor Sie fortfahren.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Für diesen Snap werden die folgenden Berechtigungen beantragt:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Snap aktualisieren"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 benötigt eine neuere Version Ihres Snaps.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snaps"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Ein Fehler ist mit $1: $2 aufgetreten",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Transaktions-Einsicht wird geladen ..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Ein Snap wird nur ausgeführt, wenn er aktiviert ist"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Die vom Snap spezifizierte UI ist ungültig."
|
||||
"message": "Die vom Snap spezifizierte UI ist ungültig.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Einige Netzwerke können Sicherheits- und/oder Datenschutzrisiken bergen. Informieren Sie sich über die Risiken, bevor Sie ein Netzwerk hinzufügen und nutzen."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Das Symbol darf maximal 11 Zeichen lang sein."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Sync fehlgeschlagen"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Synchronisation läuft"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Mit Mobilgerät synchronisieren"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Stellen Sie sicher, dass niemand sonst auf Ihren Bildschirm blickt, wenn Sie diesen Code scannen"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Ihre Daten wurden erfolgreich synchronisiert. Viel Spaß mit der MetaMask-Handy-App!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Sie können Ihre Konten und Informationen mit Ihrem Mobilgerät synchronisieren. Öffnen Sie die MetaMask-Mobilapp, gehen Sie zu \"Einstellungen\" und tippen Sie auf \"Von Browsererweiterung aus synchronisieren\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Wenn Sie die MetaMask-Mobilapp gerade zum ersten Mal öffnen, folgen Sie einfach den Schritten auf Ihrem Telefon."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Scannen Sie diesen Code mit Ihrer MetaMask-Mobilapp"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Mit Mobilgerät synchronisieren"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% Erhöhung"
|
||||
},
|
||||
|
59
app/_locales/el/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Προβολή στην Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Επεκτείνετε την εμπειρία σας στο web3 με τα Snap του MetaMask"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Ανάπτυξη Προβολής"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Παραμορφωμένα δεδομένα"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Διαχειριστείτε τα εγκατεστημένα Snap σας"
|
||||
},
|
||||
"max": {
|
||||
"message": "Μέγ."
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Δεν βλέπετε το token σας;"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "Η λειτουργία 'Συγχρονισμός με επέκταση' είναι προσωρινά απενεργοποιημένη. Αν θέλετε να χρησιμοποιήσετε το πορτοφόλι της επέκτασής σας στο MetaMask mobile, τότε στην εφαρμογή για το κινητό σας: επιστρέψτε στις επιλογές εγκατάστασης του πορτοφολιού και επιλέξτε την επιλογή 'Εισαγωγή με Μυστική Φράση Ανάκτησης'. Χρησιμοποιήστε τη μυστική φράση του πορτοφολιού της επέκτασής σας για να εισαγάγετε το πορτοφόλι σας στο κινητό."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Περισσότερα έρχονται σύντομα..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Το Nonce είναι υψηλότερο από το προτεινόμενο nonce του $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "Τα NFT δεν μπορούν να προστεθούν, διότι τα στοιχεία της κυριότητας δεν ταυτίζονται. Σιγουρευτείτε ότι έχετε εισαγάγει τα σωστά στοιχεία."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Εμφάνιση"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Προωθημένος έλεγχος gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Επιλέξτε αυτό για να εμφανίσετε τις τιμές αερίου και να περιορίσετε τα στοιχεία ελέγχου απευθείας στις οθόνες αποστολής και επιβεβαίωσης."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Εμφάνιση Μετατροπής σε Δοκιμαστικά Δίκτυα"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Εκχωρείτε στο $2 βασική πρόσβαση στο snap \"$1\". Αυτό είναι αμετάκλητο και παρέχει στο \"$1\" τον έλεγχο των λογαριασμών και των περιουσιακών σας στοιχείων $2. Βεβαιωθείτε ότι εμπιστεύεστε το \"$1\" προτού συνεχίσετε.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Αυτό το snap αιτείται τις παρακάτω άδειες:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Ενημέρωση Snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "Το $1 χρειάζεται μια νεότερη έκδοση του snap σας.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snaps"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Παρουσιάστηκε ένα σφάλμα με $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Φόρτωση πληροφοριών συναλλαγών..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Ένα snap θα εκτελεστεί μόνο εάν είναι ενεργοποιημένο"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Η Διεπαφή Χρήστη (UI) που καθορίζεται από το στιγμιότυπο δεν είναι έγκυρη."
|
||||
"message": "Η Διεπαφή Χρήστη (UI) που καθορίζεται από το στιγμιότυπο δεν είναι έγκυρη.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Ορισμένα δίκτυα ενδέχεται να ενέχουν κινδύνους για την ασφάλεια ή/και το απόρρητο. Ενημερωθείτε για τους κινδύνους πριν προσθέσετε και χρησιμοποιήσετε ένα δίκτυο."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Το σύμβολο πρέπει να είναι τουλάχιστον 11 χαρακτήρες."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Ο συγχρονισμός απέτυχε"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Συγχρονισμός σε εξέλιξη"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Συγχρονισμός με κινητό"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Σιγουρευτείτε ότι κανένας δεν κοιτάζει στην οθόνη σας όταν κάνετε σάρωση αυτού του κωδικού"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Τα δεδομένα σας έχουν συγχρονιστεί με επιτυχία. Απολαύστε την εφαρμογή MetaMask για κινητά!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Μπορείτε να συγχρονίσετε τους λογαριασμούς και τις πληροφορίες σας με την κινητή συσκευή σας. Ανοίξτε την εφαρμογή MetaMask για κινητά, μεταβείτε στην ενότητα \"Ρυθμίσεις\" και πατήστε \"Συγχρονισμός από Επέκταση Προγράμματος Περιήγησης\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Εάν απλά ανοίξετε την εφαρμογή MetaMask Mobile για πρώτη φορά, απλώς ακολουθήστε τα βήματα στο τηλέφωνό σας."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Σαρώστε αυτόν τον κώδικα με την εφαρμογή MetaMask για κινητά"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Συγχρονισμός με κινητό"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% αύξηση"
|
||||
},
|
||||
|
377
app/_locales/en/messages.json
generated
@ -103,6 +103,9 @@
|
||||
"SIWEWarningTitle": {
|
||||
"message": "Are you sure?"
|
||||
},
|
||||
"ShowMore": {
|
||||
"message": "Show more"
|
||||
},
|
||||
"about": {
|
||||
"message": "About"
|
||||
},
|
||||
@ -150,6 +153,9 @@
|
||||
"accountSelectionRequired": {
|
||||
"message": "You need to select an account!"
|
||||
},
|
||||
"activated": {
|
||||
"message": "Active"
|
||||
},
|
||||
"active": {
|
||||
"message": "Active"
|
||||
},
|
||||
@ -171,6 +177,9 @@
|
||||
"addANickname": {
|
||||
"message": "Add a nickname"
|
||||
},
|
||||
"addAccount": {
|
||||
"message": "Add account"
|
||||
},
|
||||
"addAcquiredTokens": {
|
||||
"message": "Add the tokens you've acquired using MetaMask"
|
||||
},
|
||||
@ -257,6 +266,9 @@
|
||||
"message": "This network connection relies on third parties. This connection may be less reliable or enable third-parties to track activity. $1",
|
||||
"description": "$1 is Learn more link"
|
||||
},
|
||||
"addNewToken": {
|
||||
"message": "Add new token"
|
||||
},
|
||||
"addSuggestedTokens": {
|
||||
"message": "Add suggested tokens"
|
||||
},
|
||||
@ -318,6 +330,10 @@
|
||||
"message": "All of your $1",
|
||||
"description": "$1 is the symbol or name of the token that the user is approving spending"
|
||||
},
|
||||
"allYourNFTsOf": {
|
||||
"message": "All of your NFTs from $1",
|
||||
"description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name"
|
||||
},
|
||||
"allowExternalExtensionTo": {
|
||||
"message": "Allow this external extension to:"
|
||||
},
|
||||
@ -335,6 +351,9 @@
|
||||
"amount": {
|
||||
"message": "Amount"
|
||||
},
|
||||
"apiUrl": {
|
||||
"message": "API URL"
|
||||
},
|
||||
"appDescription": {
|
||||
"message": "An Ethereum Wallet in your Browser",
|
||||
"description": "The description of the application"
|
||||
@ -362,6 +381,10 @@
|
||||
"message": "Allow access to and transfer of all your $1?",
|
||||
"description": "$1 is the symbol of the token for which the user is granting approval"
|
||||
},
|
||||
"approveAllTokensTitleWithoutSymbol": {
|
||||
"message": "Allow access to and transfer all of your NFTs from $1?",
|
||||
"description": "$1 a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name"
|
||||
},
|
||||
"approveAndInstall": {
|
||||
"message": "Approve & install"
|
||||
},
|
||||
@ -378,6 +401,10 @@
|
||||
"approveTokenDescription": {
|
||||
"message": "This allows a third party to access and transfer the following NFTs without further notice until you revoke its access."
|
||||
},
|
||||
"approveTokenDescriptionWithoutSymbol": {
|
||||
"message": "This allows a third party to access and transfer all of your NFTs from $1 without further notice until you revoke its access.",
|
||||
"description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name"
|
||||
},
|
||||
"approveTokenTitle": {
|
||||
"message": "Allow access to and transfer of your $1?",
|
||||
"description": "$1 is the symbol of the token for which the user is granting approval"
|
||||
@ -621,9 +648,45 @@
|
||||
"close": {
|
||||
"message": "Close"
|
||||
},
|
||||
"codefiCompliance": {
|
||||
"message": "Codefi Compliance"
|
||||
},
|
||||
"coingecko": {
|
||||
"message": "CoinGecko"
|
||||
},
|
||||
"complianceActivatedDesc": {
|
||||
"message": "You can now use compliance in MetaMask Institutional. Receiving AML/CFT analysis within the confirmation screen on all the addresses you interact with."
|
||||
},
|
||||
"complianceActivatedTitle": {
|
||||
"message": "Your compliance feature is activated"
|
||||
},
|
||||
"complianceBlurb0": {
|
||||
"message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties."
|
||||
},
|
||||
"complianceBlurb1": {
|
||||
"message": "Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting."
|
||||
},
|
||||
"complianceBlurbStep1": {
|
||||
"message": "Sign up to Codefi Compliance below"
|
||||
},
|
||||
"complianceBlurbStep2": {
|
||||
"message": "Create an organisation"
|
||||
},
|
||||
"complianceBlurbStep3": {
|
||||
"message": "Create a project"
|
||||
},
|
||||
"complianceBlurbStep4": {
|
||||
"message": "Set your compliance settings"
|
||||
},
|
||||
"complianceBlurbStep5": {
|
||||
"message": "Click the \"Enable Compliance in MMI\" button"
|
||||
},
|
||||
"complianceBlurpStep0": {
|
||||
"message": "Steps to enable AML/CFT Compliance:"
|
||||
},
|
||||
"complianceSettingsExplanation": {
|
||||
"message": "Change your settings or view reports by opening up Codefi Compliance or disconnect below."
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
},
|
||||
@ -723,6 +786,9 @@
|
||||
"connectingToSepolia": {
|
||||
"message": "Connecting to Sepolia test network"
|
||||
},
|
||||
"connectionError": {
|
||||
"message": "Connection error"
|
||||
},
|
||||
"contactUs": {
|
||||
"message": "Contact us"
|
||||
},
|
||||
@ -749,7 +815,7 @@
|
||||
"message": "Contract deployment"
|
||||
},
|
||||
"contractDescription": {
|
||||
"message": "To protect yourself against scammers, take a moment to verify contract details."
|
||||
"message": "To protect yourself against scammers, take a moment to verify third-party details."
|
||||
},
|
||||
"contractInteraction": {
|
||||
"message": "Contract interaction"
|
||||
@ -764,10 +830,10 @@
|
||||
"message": "Contract requesting signature"
|
||||
},
|
||||
"contractRequestingSpendingCap": {
|
||||
"message": "Contract requesting spending cap"
|
||||
"message": "Third party requesting spending cap"
|
||||
},
|
||||
"contractTitle": {
|
||||
"message": "Contract details"
|
||||
"message": "Third-party details"
|
||||
},
|
||||
"contractToken": {
|
||||
"message": "Token contract"
|
||||
@ -857,6 +923,12 @@
|
||||
"curveMediumGasEstimate": {
|
||||
"message": "Market gas estimate graph"
|
||||
},
|
||||
"custodian": {
|
||||
"message": "Custodian"
|
||||
},
|
||||
"custodianAccount": {
|
||||
"message": "Custodian account"
|
||||
},
|
||||
"custom": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
@ -1311,9 +1383,17 @@
|
||||
"errorWhileConnectingToRPC": {
|
||||
"message": "Error while connecting to the custom network."
|
||||
},
|
||||
"errorWithSnap": {
|
||||
"message": "Error with $1",
|
||||
"description": "$1 represents the name of the snap"
|
||||
},
|
||||
"ethGasPriceFetchWarning": {
|
||||
"message": "Backup gas price is provided as the main gas estimation service is unavailable right now."
|
||||
},
|
||||
"ethereumProviderAccess": {
|
||||
"message": "Grant Ethereum provider access to $1",
|
||||
"description": "The parameter is the name of the requesting origin"
|
||||
},
|
||||
"ethereumPublicAddress": {
|
||||
"message": "Ethereum public address"
|
||||
},
|
||||
@ -1326,18 +1406,21 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "View on Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Expand your web3 experience with MetaMask Snaps"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Expand view"
|
||||
},
|
||||
"experimental": {
|
||||
"message": "Experimental"
|
||||
},
|
||||
"exploreMetaMaskSnaps": {
|
||||
"message": "Explore MetaMask Snaps"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Export private key"
|
||||
},
|
||||
"extendWalletWithSnaps": {
|
||||
"message": "Extend the wallet experience."
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "External extension"
|
||||
},
|
||||
@ -1367,6 +1450,9 @@
|
||||
"message": "File import not working? Click here!",
|
||||
"description": "Helps user import their account from a JSON file"
|
||||
},
|
||||
"fileTooBig": {
|
||||
"message": "The dropped file is too big."
|
||||
},
|
||||
"flaskSnapSettingsCardButtonCta": {
|
||||
"message": "See details",
|
||||
"description": "Call to action a user can take to see more information about the snap that is installed"
|
||||
@ -1543,6 +1629,9 @@
|
||||
"hardware": {
|
||||
"message": "Hardware"
|
||||
},
|
||||
"hardwareWallet": {
|
||||
"message": "Hardware wallet"
|
||||
},
|
||||
"hardwareWalletConnected": {
|
||||
"message": "Hardware wallet connected"
|
||||
},
|
||||
@ -1627,6 +1716,9 @@
|
||||
"holdToRevealTitle": {
|
||||
"message": "Keep your SRP safe"
|
||||
},
|
||||
"id": {
|
||||
"message": "Id"
|
||||
},
|
||||
"ignoreAll": {
|
||||
"message": "Ignore all"
|
||||
},
|
||||
@ -1716,18 +1808,25 @@
|
||||
"message": "Your initial transaction was confirmed by the network. Click OK to go back."
|
||||
},
|
||||
"inputLogicEmptyState": {
|
||||
"message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later."
|
||||
"message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later."
|
||||
},
|
||||
"inputLogicEqualOrSmallerNumber": {
|
||||
"message": "This allows the contract to spend $1 from your current balance.",
|
||||
"message": "This allows the third party to spend $1 from your current balance.",
|
||||
"description": "$1 is the current token balance in the account and the name of the current token"
|
||||
},
|
||||
"inputLogicHigherNumber": {
|
||||
"message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
|
||||
"message": "This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
|
||||
},
|
||||
"insightsFromSnap": {
|
||||
"message": "Insights from $1",
|
||||
"description": "$1 represents the name of the snap"
|
||||
},
|
||||
"install": {
|
||||
"message": "Install"
|
||||
},
|
||||
"institutionalFeatures": {
|
||||
"message": "Institutional Features"
|
||||
},
|
||||
"insufficientBalance": {
|
||||
"message": "Insufficient balance."
|
||||
},
|
||||
@ -1940,6 +2039,9 @@
|
||||
"lock": {
|
||||
"message": "Lock"
|
||||
},
|
||||
"lockMetaMask": {
|
||||
"message": "Lock MetaMask"
|
||||
},
|
||||
"lockTimeTooGreat": {
|
||||
"message": "Lock time is too great"
|
||||
},
|
||||
@ -1976,9 +2078,6 @@
|
||||
"malformedData": {
|
||||
"message": "Malformed data"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Manage your installed snaps"
|
||||
},
|
||||
"max": {
|
||||
"message": "Max"
|
||||
},
|
||||
@ -2054,8 +2153,11 @@
|
||||
"missingToken": {
|
||||
"message": "Don't see your token?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile."
|
||||
"mmiAddToken": {
|
||||
"message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional"
|
||||
},
|
||||
"mmiAuthenticate": {
|
||||
"message": "The page at $1 would like to authorise the following project’s compliance settings in MetaMask Institutional"
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "More coming soon..."
|
||||
@ -2105,6 +2207,9 @@
|
||||
"networkIsBusy": {
|
||||
"message": "Network is busy. Gas prices are high and estimates are less accurate."
|
||||
},
|
||||
"networkMenuHeading": {
|
||||
"message": "Select a network"
|
||||
},
|
||||
"networkName": {
|
||||
"message": "Network name"
|
||||
},
|
||||
@ -2215,9 +2320,6 @@
|
||||
"message": "Nonce is higher than suggested nonce of $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "NFT can’t be added as the ownership details do not match. Make sure you have entered correct information."
|
||||
},
|
||||
@ -2270,7 +2372,7 @@
|
||||
"message": "No NFTs yet"
|
||||
},
|
||||
"noSnaps": {
|
||||
"message": "No Snaps installed"
|
||||
"message": "You don't have any snaps installed."
|
||||
},
|
||||
"noThanksVariant2": {
|
||||
"message": "No, thanks."
|
||||
@ -2381,6 +2483,9 @@
|
||||
"message": "OpenSea is the first provider for this feature. More providers coming soon!",
|
||||
"description": "Description of a notification in the 'See What's New' popup. Describes Opensea Security Provider feature."
|
||||
},
|
||||
"notifications18Title": {
|
||||
"message": "Stay safe with security alerts"
|
||||
},
|
||||
"notifications19ActionText": {
|
||||
"message": "Enable NFT autodetection"
|
||||
},
|
||||
@ -2407,6 +2512,18 @@
|
||||
"message": "Swapping on mobile is here!",
|
||||
"description": "Title for a notification in the 'See What's New' popup. Tells users that they can now use MetaMask Swaps on Mobile."
|
||||
},
|
||||
"notifications20ActionText": {
|
||||
"message": "Learn more",
|
||||
"description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a ledger page to resolve the U2F connection issue."
|
||||
},
|
||||
"notifications20Description": {
|
||||
"message": "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support.",
|
||||
"description": "Description of a notification in the 'See What's New' popup. Describes the U2F support being dropped by firefox and that it affects ledger users."
|
||||
},
|
||||
"notifications20Title": {
|
||||
"message": "Ledger and Firefox Users Experiencing Connection Issues",
|
||||
"description": "Title for a notification in the 'See What's New' popup. Tells users that latest firefox users using U2F may experience connection issues."
|
||||
},
|
||||
"notifications3ActionText": {
|
||||
"message": "Read more",
|
||||
"description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website."
|
||||
@ -2652,6 +2769,9 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Only connect with sites you trust."
|
||||
},
|
||||
"openCodefiCompliance": {
|
||||
"message": "Open Codefi Compliance"
|
||||
},
|
||||
"openFullScreenForLedgerWebHid": {
|
||||
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
|
||||
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."
|
||||
@ -2761,22 +2881,42 @@
|
||||
"message": "Access the internet.",
|
||||
"description": "The description of the `endowment:network-access` permission."
|
||||
},
|
||||
"permission_accessNetworkDescription": {
|
||||
"message": "Allow the snap to access the internet. This can be used to both send and receive data with third-party servers.",
|
||||
"description": "An extended description of the `endowment:network-access` permission."
|
||||
},
|
||||
"permission_accessSnap": {
|
||||
"message": "Connect to the $1 snap.",
|
||||
"description": "The description for the `wallet_snap` permission. $1 is the name of the snap."
|
||||
},
|
||||
"permission_accessSnapDescription": {
|
||||
"message": "Allow the website or snap to interact with $1.",
|
||||
"description": "The description for the `wallet_snap_*` permission. $1 is the name of the Snap."
|
||||
},
|
||||
"permission_cronjob": {
|
||||
"message": "Schedule and execute periodic actions.",
|
||||
"description": "The description for the `snap_cronjob` permission"
|
||||
},
|
||||
"permission_cronjobDescription": {
|
||||
"message": "Allow the snap to perform actions that run periodically at fixed times, dates, or intervals. This can be used to trigger time-sensitive interactions or notifications.",
|
||||
"description": "An extended description for the `snap_cronjob` permission"
|
||||
},
|
||||
"permission_customConfirmation": {
|
||||
"message": "Display a confirmation in MetaMask.",
|
||||
"description": "The description for the `snap_confirm` permission"
|
||||
},
|
||||
"permission_customConfirmationDescription": {
|
||||
"message": "Allow the snap to display MetaMask popups with custom text, and buttons to approve or reject an action.",
|
||||
"description": "An extended description for the `snap_confirm` permission"
|
||||
},
|
||||
"permission_dialog": {
|
||||
"message": "Display dialog windows in MetaMask.",
|
||||
"description": "The description for the `snap_dialog` permission"
|
||||
},
|
||||
"permission_dialogDescription": {
|
||||
"message": "Allow the snap to display MetaMask popups with custom text, input field, and buttons to approve or reject an action.\nCan be used to create e.g. alerts, confirmations, and opt-in flows for a snap.",
|
||||
"description": "An extended description for the `snap_dialog` permission"
|
||||
},
|
||||
"permission_ethereumAccounts": {
|
||||
"message": "See address, account balance, activity and suggest transactions to approve",
|
||||
"description": "The description for the `eth_accounts` permission"
|
||||
@ -2785,22 +2925,42 @@
|
||||
"message": "Access the Ethereum provider.",
|
||||
"description": "The description for the `endowment:ethereum-provider` permission"
|
||||
},
|
||||
"permission_ethereumProviderDescription": {
|
||||
"message": "Allow the snap to communicate with MetaMask directly, in order for it to read data from the blockchain and suggest messages and transactions.",
|
||||
"description": "An extended description for the `endowment:ethereum-provider` permission"
|
||||
},
|
||||
"permission_getEntropy": {
|
||||
"message": "Derive arbitrary keys unique to this snap.",
|
||||
"description": "The description for the `snap_getEntropy` permission"
|
||||
},
|
||||
"permission_getEntropyDescription": {
|
||||
"message": "Allow the snap to derive arbitrary keys unique to this snap, without exposing them. These keys are separate from your MetaMask account(s) and not related to your private keys or Secret Recovery Phrase. Other snaps cannot access this information.",
|
||||
"description": "An extended description for the `snap_getEntropy` permission"
|
||||
},
|
||||
"permission_longRunning": {
|
||||
"message": "Run indefinitely.",
|
||||
"description": "The description for the `endowment:long-running` permission"
|
||||
},
|
||||
"permission_longRunningDescription": {
|
||||
"message": "Allow the snap to run indefinitely while, for example, processing large amounts of data.",
|
||||
"description": "An extended description for the `endowment:long-running` permission"
|
||||
},
|
||||
"permission_manageBip32Keys": {
|
||||
"message": "Control your accounts and assets under $1 ($2).",
|
||||
"description": "The description for the `snap_getBip32Entropy` permission. $1 is a derivation path, e.g. 'm/44'/0'/0''. $2 is the elliptic curve name, e.g. 'secp256k1'."
|
||||
},
|
||||
"permission_manageBip32KeysDescription": {
|
||||
"message": "Allow the snap to derive BIP-32 key pairs based on your Secret Recovery Phrase without exposing it. This grants full access to all accounts and assets on $1.\nWith the power to manage keys, the snap can support a variety of blockchain protocols beyond Ethereum (EVMs).",
|
||||
"description": "An extended description for the `snap_getBip32Entropy` permission. $1 is a derivation path (name)"
|
||||
},
|
||||
"permission_manageBip44Keys": {
|
||||
"message": "Control your \"$1\" accounts and assets.",
|
||||
"message": "Control your $1 accounts and assets.",
|
||||
"description": "The description for the `snap_getBip44Entropy` permission. $1 is the name of a protocol, e.g. 'Filecoin'."
|
||||
},
|
||||
"permission_manageBip44KeysDescription": {
|
||||
"message": "Allow the snap to derive BIP-44 key pairs based on your Secret Recovery Phrase without exposing it. This grants full access to all accounts and assets on $1.\nWith the power to manage keys, the snap can support a variety of blockchain protocols beyond Ethereum (EVMs).",
|
||||
"description": "An extended description for the `snap_getBip44Entropy` permission. $1 is the name of a protocol, e.g., 'Filecoin'."
|
||||
},
|
||||
"permission_manageNamedBip32Keys": {
|
||||
"message": "Control your $1 accounts and assets.",
|
||||
"description": "The description for the `snap_getBip32Entropy` permission. $1 is a name for the derivation path, e.g., 'Ethereum accounts'. $2 is the plain derivation path, e.g. 'm/44'/0'/0''."
|
||||
@ -2809,22 +2969,42 @@
|
||||
"message": "Store and manage its data on your device.",
|
||||
"description": "The description for the `snap_manageState` permission"
|
||||
},
|
||||
"permission_manageStateDescription": {
|
||||
"message": "Allow the snap to store, update, and retrieve data securely with encryption. Other snaps cannot access this information.",
|
||||
"description": "An extended description for the `snap_manageState` permission"
|
||||
},
|
||||
"permission_notifications": {
|
||||
"message": "Show notifications.",
|
||||
"description": "The description for the `snap_notify` permission"
|
||||
},
|
||||
"permission_notificationsDescription": {
|
||||
"message": "Allow the snap to display notifications within MetaMask. A short notification text can be triggered by a snap for actionable or time-sensitive information.",
|
||||
"description": "An extended description for the `snap_notify` permission"
|
||||
},
|
||||
"permission_rpc": {
|
||||
"message": "Allow $1 to communicate directly with this snap.",
|
||||
"description": "The description for the `endowment:rpc` permission. $1 is 'other snaps' or 'websites'."
|
||||
},
|
||||
"permission_rpcDescription": {
|
||||
"message": "Allow $1 to send messages to the snap and receive a response from the snap.",
|
||||
"description": "An extended description for the `endowment:rpc` permission. $1 is 'other snaps' or 'websites'."
|
||||
},
|
||||
"permission_transactionInsight": {
|
||||
"message": "Fetch and display transaction insights.",
|
||||
"description": "The description for the `endowment:transaction-insight` permission"
|
||||
},
|
||||
"permission_transactionInsightDescription": {
|
||||
"message": "Allow the snap to decode transactions and show insights within the MetaMask UI. This can be used for anti-phishing and security solutions.",
|
||||
"description": "An extended description for the `endowment:transaction-insight` permission"
|
||||
},
|
||||
"permission_transactionInsightOrigin": {
|
||||
"message": "See the origins of websites that suggest transactions",
|
||||
"description": "The description for the `transactionOrigin` caveat, to be used with the `endowment:transaction-insight` permission"
|
||||
},
|
||||
"permission_transactionInsightOriginDescription": {
|
||||
"message": "Allow the snap to see the origin (URI) of websites that suggest transactions. This can be used for anti-phishing and security solutions.",
|
||||
"description": "An extended description for the `transactionOrigin` caveat, to be used with the `endowment:transaction-insight` permission"
|
||||
},
|
||||
"permission_unknown": {
|
||||
"message": "Unknown permission: $1",
|
||||
"description": "$1 is the name of a requested permission that is not recognized."
|
||||
@ -2833,6 +3013,10 @@
|
||||
"message": "View your public key for $1 ($2).",
|
||||
"description": "The description for the `snap_getBip32PublicKey` permission. $1 is a derivation path, e.g. 'm/44'/0'/0''. $2 is the elliptic curve name, e.g. 'secp256k1'."
|
||||
},
|
||||
"permission_viewBip32PublicKeysDescription": {
|
||||
"message": "Allow the snap to view your public keys (and addresses) for $1. This does not grant any control of accounts or assets.",
|
||||
"description": "An extended description for the `snap_getBip32PublicKey` permission. $1 is a derivation path (name)"
|
||||
},
|
||||
"permission_viewNamedBip32PublicKeys": {
|
||||
"message": "View your public key for $1.",
|
||||
"description": "The description for the `snap_getBip32PublicKey` permission. $1 is a name for the derivation path, e.g., 'Ethereum accounts'."
|
||||
@ -2841,6 +3025,10 @@
|
||||
"message": "Support for WebAssembly.",
|
||||
"description": "The description of the `endowment:webassembly` permission."
|
||||
},
|
||||
"permission_webAssemblyDescription": {
|
||||
"message": "Allow the snap to access low-level execution environments via WebAssembly.",
|
||||
"description": "An extended description of the `endowment:webassembly` permission."
|
||||
},
|
||||
"permissions": {
|
||||
"message": "Permissions"
|
||||
},
|
||||
@ -2860,6 +3048,9 @@
|
||||
"portfolio": {
|
||||
"message": "Portfolio"
|
||||
},
|
||||
"portfolioView": {
|
||||
"message": "Portfolio view"
|
||||
},
|
||||
"preferredLedgerConnectionType": {
|
||||
"message": "Preferred Ledger connection type",
|
||||
"description": "A header for a dropdown in Settings > Advanced. Appears above the ledgerConnectionPreferenceDescription message"
|
||||
@ -2901,6 +3092,12 @@
|
||||
"proceedWithTransaction": {
|
||||
"message": "I want to proceed anyway"
|
||||
},
|
||||
"projectIdInvalid": {
|
||||
"message": "Provided Project ID is invalid"
|
||||
},
|
||||
"projectName": {
|
||||
"message": "Project Name"
|
||||
},
|
||||
"proposedApprovalLimit": {
|
||||
"message": "Proposed approval limit"
|
||||
},
|
||||
@ -3016,6 +3213,9 @@
|
||||
"replace": {
|
||||
"message": "replace"
|
||||
},
|
||||
"requestFailed": {
|
||||
"message": "Request failed"
|
||||
},
|
||||
"requestFlaggedAsMaliciousFallbackCopyReason": {
|
||||
"message": "The security provider has not shared additional details"
|
||||
},
|
||||
@ -3117,22 +3317,30 @@
|
||||
"message": "Reveal seed phrase"
|
||||
},
|
||||
"reviewSpendingCap": {
|
||||
"message": "Review your spending cap"
|
||||
"message": "Review the spending cap for your"
|
||||
},
|
||||
"revokeAllTokensTitle": {
|
||||
"message": "Revoke permission to access and transfer all of your $1?",
|
||||
"description": "$1 is the symbol of the token for which the user is revoking approval"
|
||||
},
|
||||
"revokeAllTokensTitleWithoutSymbol": {
|
||||
"message": "Revoke permission to access and transfer all of your NFTs from $1?",
|
||||
"description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name"
|
||||
},
|
||||
"revokeApproveForAllDescription": {
|
||||
"message": "This revokes the permission for a third party to access and transfer all of your $1 without further notice.",
|
||||
"description": "$1 is either a string or link of a given token symbol or name"
|
||||
},
|
||||
"revokeApproveForAllDescriptionWithoutSymbol": {
|
||||
"message": "This revokes the permission for a third party to access and transfer all of your NFTs from $1 without further notice.",
|
||||
"description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name"
|
||||
},
|
||||
"revokeSpendingCap": {
|
||||
"message": "Revoke spending cap for your $1",
|
||||
"description": "$1 is a token symbol"
|
||||
},
|
||||
"revokeSpendingCapTooltipText": {
|
||||
"message": "This contract will be unable to spend any more of your current or future tokens."
|
||||
"message": "This third party will be unable to spend any more of your current or future tokens."
|
||||
},
|
||||
"rpcUrl": {
|
||||
"message": "New RPC URL"
|
||||
@ -3248,6 +3456,9 @@
|
||||
"selectHdPath": {
|
||||
"message": "Select HD path"
|
||||
},
|
||||
"selectJWT": {
|
||||
"message": "Select token"
|
||||
},
|
||||
"selectNFTPrivacyPreference": {
|
||||
"message": "Turn on NFT detection in Settings"
|
||||
},
|
||||
@ -3320,12 +3531,6 @@
|
||||
"show": {
|
||||
"message": "Show"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Advanced gas controls"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Select this to show gas price and limit controls directly on the send and confirm screens."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Show conversion on test networks"
|
||||
},
|
||||
@ -3415,36 +3620,60 @@
|
||||
"snapInstall": {
|
||||
"message": "Install snap"
|
||||
},
|
||||
"snapInstallRequest": {
|
||||
"message": "$1 wants to install $2. Make sure you trust the authors before you proceed.",
|
||||
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
|
||||
},
|
||||
"snapInstallRequestsPermission": {
|
||||
"message": "$1 wants to install $2, which is requesting the following permissions. Make sure you trust the authors before you proceed.",
|
||||
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
|
||||
},
|
||||
"snapInstallWarningCheck": {
|
||||
"message": "To confirm that you understand, check the box."
|
||||
"message": "Ensure that the permission below align with your intended actions. Only proceed with authors you trust."
|
||||
},
|
||||
"snapInstallWarningCheckPlural": {
|
||||
"message": "To confirm that you understand, check all the boxes."
|
||||
"message": "Ensure that the permissions below align with your intended actions. Only proceed with authors you trust."
|
||||
},
|
||||
"snapInstallWarningHeading": {
|
||||
"message": "Proceed with caution"
|
||||
},
|
||||
"snapInstallWarningKeyAccess": {
|
||||
"message": "You are granting $2 key access to the snap \"$1\". This is irrevocable and grants \"$1\" control of your $2 accounts and assets. Make sure you trust \"$1\" before proceeding.",
|
||||
"message": "Grant $2 account control to $1",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "This snap is requesting the following permissions:"
|
||||
"snapInstallWarningPublicKeyAccess": {
|
||||
"message": "Grant $2 public key access to $1",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapResultError": {
|
||||
"message": "Error"
|
||||
},
|
||||
"snapResultSuccess": {
|
||||
"message": "Success"
|
||||
},
|
||||
"snapResultSuccessDescription": {
|
||||
"message": "$1 is now available to use."
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Update snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 needs a newer version of your snap.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
"snapUpdateRequest": {
|
||||
"message": "$1 wants to update $2. Make sure you trust the authors before you proceed.",
|
||||
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
|
||||
},
|
||||
"snapUpdateRequestsPermission": {
|
||||
"message": "$1 wants to update $2, which is requesting the following permissions. Make sure you trust the authors before you proceed.",
|
||||
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snaps"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "An error occured with $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Loading transaction insight..."
|
||||
},
|
||||
"snapsInvalidUIError": {
|
||||
"message": "The UI specified by the snap is invalid."
|
||||
},
|
||||
"snapsNoInsight": {
|
||||
"message": "The snap didn't return any insight"
|
||||
},
|
||||
@ -3458,7 +3687,8 @@
|
||||
"message": "A snap will only run if it is enabled"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "The UI specified by the snap is invalid."
|
||||
"message": "Contact the creators of $1 for further support.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network."
|
||||
@ -3567,6 +3797,9 @@
|
||||
"statusNotConnected": {
|
||||
"message": "Not connected"
|
||||
},
|
||||
"statusNotConnectedAccount": {
|
||||
"message": "No accounts connected"
|
||||
},
|
||||
"step1LatticeWallet": {
|
||||
"message": "Connect your Lattice1"
|
||||
},
|
||||
@ -3896,7 +4129,7 @@
|
||||
"message": "Select a quote"
|
||||
},
|
||||
"swapSelectAToken": {
|
||||
"message": "Select a token"
|
||||
"message": "Select token"
|
||||
},
|
||||
"swapSelectQuotePopoverDescription": {
|
||||
"message": "Below are all the quotes gathered from multiple liquidity sources."
|
||||
@ -4032,33 +4265,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbol must be 11 characters or fewer."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Sync failed"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Sync in progress"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sync with mobile"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Make sure nobody else is looking at your screen when you scan this code"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Your data has been synced successfully. Enjoy the MetaMask mobile app!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "You can sync your accounts and information with your mobile device. Open the MetaMask mobile app, go to \"Settings\" and tap on \"Sync from Browser Extension\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "If you just open the MetaMask Mobile app for the first time, just follow the steps in your phone."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Scan this code with your MetaMask mobile app"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sync with mobile"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% increase"
|
||||
},
|
||||
@ -4083,6 +4289,9 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Things to keep in mind:"
|
||||
},
|
||||
"thisCollection": {
|
||||
"message": "this collection"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "This is based on information from "
|
||||
},
|
||||
@ -4162,6 +4371,12 @@
|
||||
"tooltipApproveButton": {
|
||||
"message": "I understand"
|
||||
},
|
||||
"tooltipSatusConnected": {
|
||||
"message": "connected"
|
||||
},
|
||||
"tooltipSatusNotConnected": {
|
||||
"message": "not connected"
|
||||
},
|
||||
"total": {
|
||||
"message": "Total"
|
||||
},
|
||||
@ -4285,6 +4500,22 @@
|
||||
"transferFrom": {
|
||||
"message": "Transfer from"
|
||||
},
|
||||
"troubleConnectingToLedgerU2FOnFirefox": {
|
||||
"message": "We're having trouble connecting your Ledger. $1",
|
||||
"description": "$1 is a link to the wallet connection guide;"
|
||||
},
|
||||
"troubleConnectingToLedgerU2FOnFirefox2": {
|
||||
"message": "Review our hardware wallet connection guide and try again.",
|
||||
"description": "$1 of the ledger wallet connection guide"
|
||||
},
|
||||
"troubleConnectingToLedgerU2FOnFirefoxLedgerSolution": {
|
||||
"message": "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support. Learn how to fix this issue $1.",
|
||||
"description": "It is a link to the ledger website for the workaround."
|
||||
},
|
||||
"troubleConnectingToLedgerU2FOnFirefoxLedgerSolution2": {
|
||||
"message": "here",
|
||||
"description": "Second part of the error message; It is a link to the ledger website for the workaround."
|
||||
},
|
||||
"troubleConnectingToWallet": {
|
||||
"message": "We had trouble connecting to your $1, try reviewing $2 and try again.",
|
||||
"description": "$1 is the wallet device name; $2 is a link to wallet connection guide"
|
||||
@ -4375,6 +4606,9 @@
|
||||
"upArrow": {
|
||||
"message": "up arrow"
|
||||
},
|
||||
"update": {
|
||||
"message": "Update"
|
||||
},
|
||||
"updatedWithDate": {
|
||||
"message": "Updated $1"
|
||||
},
|
||||
@ -4427,7 +4661,7 @@
|
||||
"message": "Username"
|
||||
},
|
||||
"verifyContractDetails": {
|
||||
"message": "Verify contract details"
|
||||
"message": "Verify third-party details"
|
||||
},
|
||||
"verifyThisTokenDecimalOn": {
|
||||
"message": "Token decimal can be found on $1",
|
||||
@ -4470,6 +4704,9 @@
|
||||
"message": "View $1 on Etherscan",
|
||||
"description": "$1 is the action type. e.g (Account, Transaction, Swap)"
|
||||
},
|
||||
"viewOnExplorer": {
|
||||
"message": "View on explorer"
|
||||
},
|
||||
"viewOnOpensea": {
|
||||
"message": "View on Opensea"
|
||||
},
|
||||
@ -4510,7 +4747,7 @@
|
||||
"message": "Warning"
|
||||
},
|
||||
"warningTooltipText": {
|
||||
"message": "$1 The contract could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.",
|
||||
"message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.",
|
||||
"description": "$1 is a warning icon with text 'Be careful' in 'warning' colour"
|
||||
},
|
||||
"weak": {
|
||||
|
59
app/_locales/es/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Ver en Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Amplíe su experiencia web3 con complementos de MetaMask"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Expandir vista"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Datos con formato incorrecto"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Administre sus complementos instalados"
|
||||
},
|
||||
"max": {
|
||||
"message": "Máx."
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "¿No ve su token?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "La función 'Sincronizar con la extensión' está temporalmente desactivada. Si desea utilizar su cartera de extensión en MetaMask móvil, haga lo siguiente en la aplicación móvil: vuelva a las opciones de configuración de la cartera y seleccione la opción 'Importar con frase secreta de recuperación'. Use la frase secreta de su cartera de extensión para importar su cartera al móvil."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Más próximamente..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "El nonce es superior al nonce sugerido de $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "No se puede agregar el NFT porque los detalles de propiedad no coinciden. Asegúrese de haber ingresado la información correcta."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Mostrar"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controles avanzados de gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Mostrar conversión en redes de prueba"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Está otorgando acceso clave de $2 al complemento \"$1\". Esto es irrevocable y le otorga a \"$1\" el control de sus cuentas y activos de $2. Asegúrese de que confía en \"$1\" antes de continuar.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Este complemento solicita los siguientes permisos:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Actualizar complemento"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 necesita una versión más reciente de su complemento.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Complementos"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Ocurrió un error con $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Cargando información de transacción..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Un complemento solo se ejecutará si está habilitado"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "La IU especificada por el complemento no es válida."
|
||||
"message": "La IU especificada por el complemento no es válida.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Algunas redes pueden presentar riesgos de seguridad y/o privacidad. Comprenda los riesgos antes de agregar y utilizar una red."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "El símbolo debe tener 11 caracteres o menos."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Error al sincronizar"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Sincronización en progreso"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronizar con dispositivo móvil"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Asegúrese de que nadie vea su pantalla cuando escanee este código"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Los datos se sincronizaron correctamente. ¡Disfrute de la aplicación móvil de MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Puede sincronizar sus cuentas y su información con el dispositivo móvil. Abra la aplicación móvil de MetaMask, vaya a \"Configuración\" y presione \"Sincronizar desde la extensión del explorador\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Si acaba de abrir la aplicación móvil de MetaMask por primera vez, siga los pasos que aparecen en el teléfono."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Escanear este código con la aplicación móvil de MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronizar con dispositivo móvil"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% de aumento"
|
||||
},
|
||||
|
36
app/_locales/es_419/messages.json
generated
@ -1369,9 +1369,6 @@
|
||||
"missingToken": {
|
||||
"message": "¿No ve su token?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "La función 'Sincronizar con la extensión' está temporalmente desactivada. Si desea utilizar su cartera de extensión en MetaMask móvil, haga lo siguiente en la aplicación móvil: vuelva a las opciones de configuración de la cartera y seleccione la opción 'Importar con frase secreta de recuperación'. Use la frase secreta de su cartera de extensión para importar su cartera al móvil."
|
||||
},
|
||||
"mustSelectOne": {
|
||||
"message": "Debe seleccionar al menos 1 token."
|
||||
},
|
||||
@ -2070,12 +2067,6 @@
|
||||
"show": {
|
||||
"message": "Mostrar"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controles avanzados de gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Mostrar conversión en redes de prueba"
|
||||
},
|
||||
@ -2555,33 +2546,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "El símbolo debe tener 11 caracteres o menos."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Error al sincronizar"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Sincronización en progreso"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronizar con dispositivo móvil"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Asegúrese de que nadie vea su pantalla cuando escanee este código"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Los datos se sincronizaron correctamente. ¡Disfrute de la aplicación móvil de MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Puede sincronizar sus cuentas y su información con el dispositivo móvil. Abra la aplicación móvil de MetaMask, vaya a \"Configuración\" y presione \"Sincronizar desde la extensión del explorador\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Si acaba de abrir la aplicación móvil de MetaMask por primera vez, siga los pasos que aparecen en el teléfono."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Escanear este código con la aplicación móvil de MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronizar con dispositivo móvil"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% de aumento"
|
||||
},
|
||||
|
27
app/_locales/et/messages.json
generated
@ -668,12 +668,6 @@
|
||||
"settings": {
|
||||
"message": "Seaded"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Täiustatud gaasijuhikud"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Valige see, et kuvada gaasi hinda ja piirangut otse saatmise ning kinnitamise kuval."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Kuva teisendus Testnetsis"
|
||||
},
|
||||
@ -737,27 +731,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Sümbol peab olema 11 tähemärki või vähem."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Mobiiliga sünkroonimine"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Veenduge, et keegi teine ei vaata selle koodi skannimisel teie ekraani"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Teie andmed on edukalt sünkroonitud. Nautige MetaMaski mobiilirakendust!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Saate sünkroonida oma kontod ja teabe oma mobiiliseadmega. Avage MetaMaski mobiilirakendus, avage \"Settings\" (Seaded) ja puudutage valikut \"Sync from Browser Extension\" (Sünkroonimine lehitseja laiendusest)"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Järgige MetaMaski mobiilirakenduse esmakordsel avamisel telefonis esitatud samme."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Skanneerige see kood MetaMaski mobiilirakendusega"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Mobiiliga sünkroonimine"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Teenusetingimused"
|
||||
},
|
||||
|
27
app/_locales/fa/messages.json
generated
@ -678,12 +678,6 @@
|
||||
"settings": {
|
||||
"message": "تنظیمات"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "کنترول های پیشرفته گاز"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "این را انتخاب نمایید تا قیمت گاز را نشان داده و کنترول ها را بصورت مستقیم در صفحات ارسال و تأیید محدود نماید."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "نمایش تغییرات Testnets"
|
||||
},
|
||||
@ -747,27 +741,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "نماد باید 11 کاراکتر یا کمتر باشد."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "همگام سازی با موبایل"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "مطمئن شوید که هیچکس هنگامیکه این کود را سکن میکنید، به صفحه شما نمیبیند"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "دیتای شما موفقانه همگام سازی شد. از اپلیکیشن موبایل MetaMask لذت ببرید!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "شما میتوانید حساب ها و معلومات خویش را با دستگاه موبایل تان همگام بسازید. اپلیکیشن MetaMask را باز نموده به \"Settings\" رفته و بالای \"Sync from Browser Extension\" کلیک کنید"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "در صورتیکه شما اپلیکیشن موبایل MetaMask را برای بار اول باز کرده اید، فقط گام ها را در موبایل تان دنبال نمایید."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "این کود را با اپلیکیشن موبایل MetaMask سکن نمایید"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "همگام سازی با موبایل"
|
||||
},
|
||||
"terms": {
|
||||
"message": "شرایط استفاده"
|
||||
},
|
||||
|
27
app/_locales/fi/messages.json
generated
@ -675,12 +675,6 @@
|
||||
"settings": {
|
||||
"message": "Asetukset"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Bensan lisävalvonta"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Valitse tämä näyttääksesi gas-hinta ja rajoittaaksesi säätimiä suoraan lähetä- ja vahvista-ruuduissa."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Näytä vaihtokurssi koeverkoissa"
|
||||
},
|
||||
@ -744,27 +738,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbolin on oltava 11 merkkiä tai vähemmän."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synkronoi mobiililaitteelle"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Varmista, ettei kukaan muu katsele näyttöäsi, kun skannaat koodin"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Tietojesi synkronoiminen onnistui. Nauti MetaMask-mobiilisovelluksesta!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Voit synkronoida tilisi ja tietosi mobiililaitteidesi kesken. Avaa MetaMaskin mobiilisovellus, siirry \"Asetukset\"-osioon ja napauta \"Synkronoi selainlaajennuksesta\" -vaihtoehtoa"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Jos avaat MetaMaskin mobiilisovelluksen vasta ensimmäistä kertaa, noudata vain puhelimesi ilmoittamia vaiheita."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Lue tämä koodi MetaMask-mobiilisovelluksellasi"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synkronoi mobiililaitteen kanssa"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Käyttöehdot"
|
||||
},
|
||||
|
27
app/_locales/fil/messages.json
generated
@ -602,12 +602,6 @@
|
||||
"settings": {
|
||||
"message": "Mga Setting"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Mga advanced na kontrol sa gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Piliin ito para ipakita ang presyo ng gas at limitahan ang mga kontrol nang direkta sa screen ng pagpapadala at pagkumpirma."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Ipakita ang Conversion sa mga Testnet"
|
||||
},
|
||||
@ -665,27 +659,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Ang simbolo ay dapat na 11 character o mas kaunti."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "I-sync sa mobile"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Tiyaking walang ibang taong tumitingin sa iyong screen kapag sina-scan mo ang code na ito"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Matagumpay na na-sync ang iyong data. I-enjoy ang MetaMask mobile app!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Maaari mong i-sync ang iyong mga account at impormasyon sa iyong mobile device. Buksan ang MetaMask mobile app, pumunta sa \"Settings\" at mag-tap sa \"Sync from Browser Extension\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Kung bubuksan mo ang MetaMask Mobile app sa unang pagkakataon, sundin lang ang mga hakbang sa iyong telepono."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "I-scan ang code na ito sa iyong MetaMask mobile app"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "I-sync sa mobile"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Mga Tuntunin ng Paggamit"
|
||||
},
|
||||
|
59
app/_locales/fr/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Afficher sur Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Développez votre expérience web3 avec les Snaps MetaMask"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Agrandir la vue"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Données malformées"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Gérez vos Snaps installés"
|
||||
},
|
||||
"max": {
|
||||
"message": "Max."
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Vous ne voyez pas votre jeton ?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "La fonction « Synchronisation avec l’extension » est temporairement désactivée. Si vous souhaitez utiliser votre portefeuille d’extension sur MetaMask mobile : sur votre application mobile, revenez aux options de configuration du portefeuille et sélectionnez l’option « Importation avec la phrase secrète de récupération ». Utilisez la phrase secrète de votre portefeuille d’extension pour importer celui-ci sur votre mobile."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "D’autres à venir..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Le nonce est supérieur au nonce suggéré de $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "Ce NFT ne peut pas être ajouté, car les informations de propriété ne correspondent pas. Vérifiez que votre saisie est correcte."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Afficher"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Contrôles de carburant avancés"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Sélectionnez cette option pour afficher le prix du carburant et les contrôles des limites directement sur les écrans d’envoi et de confirmation."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Afficher la conversion sur les testnets"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Vous autorisez $2 à accéder à la clé du snap « $1 ». Cette action est irréversible et accorde à « $1 » le contrôle de vos comptes et actifs $2. Assurez-vous que vous faites confiance à « $1 » avant de continuer.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Ce snap demande les autorisations suivantes :"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Mettre à jour Snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 a besoin d’une version plus récente de votre snap.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snaps"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Une erreur s’est produite avec $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Chargement de l’aperçu de transaction…"
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Un snap ne s’exécute que s’il est activé"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "L’interface utilisateur (IU) spécifiée par le snap n’est pas valide."
|
||||
"message": "L’interface utilisateur (IU) spécifiée par le snap n’est pas valide.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Certains réseaux peuvent présenter des risques pour la sécurité et/ou la vie privée. Informez-vous sur les risques avant d’ajouter et d’utiliser un réseau."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Le symbole doit comporter 11 caractères ou moins."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Échec de la synchronisation"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Synchronisation en cours"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synchroniser avec le mobile"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Assurez-vous que personne d’autre ne regarde votre écran lorsque vous scannez ce code."
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Vos données ont été synchronisées avec succès. Profitez de l’application mobile MetaMask !"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Vous pouvez synchroniser vos comptes et vos informations avec votre appareil mobile. Ouvrez l’application mobile MetaMask, allez dans « Paramètres » et appuyez sur « Synchroniser depuis l’extension de navigateur »"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Si vous ouvrez l’application MetaMask Mobile pour la première fois, suivez simplement les étapes dans votre téléphone."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Scannez ce code avec votre application mobile MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synchroniser avec le mobile"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "Augmentation de 10 %"
|
||||
},
|
||||
|
27
app/_locales/he/messages.json
generated
@ -675,12 +675,6 @@
|
||||
"settings": {
|
||||
"message": "הגדרות"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "אמצעי שליטה מתקדמים בדלק"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "בחר/י באפשרות זו כדי להציג אמצעי שליטה במחיר הדלק וההגבלה (limit) ישירות במסכי השליחה והאישור."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "הצג המרה -Testnets"
|
||||
},
|
||||
@ -744,27 +738,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "הסמל חייב להיות 11 תווים או פחות."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "סנכרן עם הנייד"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "ודא/י כי איש אינו מסתכל על המסך שלך בזמן סריקת קוד זה"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "הנתונים שלך סונכרנו בהצלחה. תיהנה/י מאפליקציית MetaMask לטלפונים ניידים! "
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "באפשרותך לסנכרן את החשבונות והמידע שלך עם המכשיר הנייד שלך. יש לפתוח את האפליקציה לנייד של MetaMask, לעבור אל \"הגדרות\" וללחוץ על \"סנכרון מהרחבה לדפדפן\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "אם את/ה פותח/ת את אפליקציית MetaMask Mobile בפעם הראשונה, פשוט בצע/י את השלבים בטלפון שלך."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "יש לסרוק קוד QR זה באמצעות אפליקציית MetaMask לטלפון נייד"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "סנכרן עם הנייד"
|
||||
},
|
||||
"terms": {
|
||||
"message": "תנאי שימוש"
|
||||
},
|
||||
|
59
app/_locales/hi/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Etherscan पर देखें"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "MetaMask स्नैप्स के साथ अपने web3 अनुभव का विस्तार करें"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "दृश्य का विस्तार करें"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "विकृत डेटा"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "अपने इंस्टाल किए गए स्नैप्स मैनेज करें"
|
||||
},
|
||||
"max": {
|
||||
"message": "अधिकतम"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "क्या अपना टोकन नहीं देख रहे हैं?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "'एक्सटेंशन के साथ सिंक' फीचर अस्थायी रूप से अक्षम है। यदि आप MetaMask मोबाइल पर अपने एक्सटेंशन वॉलेट का उपयोग करना चाहते हैं, तो अपने मोबाइल ऐप पर: वॉलेट सेटअप ऑप्शन पर वापस जाएं और 'सीक्रेट रिकवरी फ्रेज के साथ इम्पोर्ट करें' विकल्प चुनें। फिर अपने वॉलेट को मोबाइल में इम्पोर्ट करने के लिए अपने एक्सटेंशन वॉलेट के सीक्रेट फ्रेज का उपयोग करें।"
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "और अधिक जल्द ही आ रहा..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "नॉन्स $1 के सुझाए गए नॉन्स से अधिक है",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "एनएफटी"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "एनएफटी जोड़ा नहीं जा सकता क्योंकि स्वामित्व विवरण मेल नहीं खा रहे हैं। सुनिश्चित करें कि आपने सही जानकारी दर्ज की है।"
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "दिखाएं"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "उन्नत गैस नियंत्रण"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "गैस मूल्य और सीमा नियंत्रण को सीधे भेजने और पुष्टि करने की स्क्रीन पर दिखाने के लिए इसका चयन करें।"
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "टेस्ट नेटवर्क पर रूपांतरण दिखाएं"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "आप स्नैप \"$1\" के लिए $2 कुंजी का एक्सेस प्रदान कर रहे हैं। यह अपरिवर्तनीय है और आपके $2 खातों और संपत्तियों पर \"$1\" नियंत्रण प्रदान करता है। आगे बढ़ने से पहले सुनिश्चित करें कि आप \"$1\" पर भरोसा करते हैं।",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "ये स्नैप निम्नलिखित अनुमतियों हेतु अनुरोध कर रहा है:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "स्नैप अपडेट करें"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 को आपके स्नैप के नए वर्जन की जरूरत है।",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "स्नैप्स"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "$1: $2 के साथ त्रुटि हुई",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "ट्रांजैक्शन इनसाइट लोड हो रही है..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "कोई स्नैप तभी चलेगा जब उसे सक्षम किया गया हो"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "स्नैप द्वारा विनिर्दिष्टत UI अमान्य है।"
|
||||
"message": "स्नैप द्वारा विनिर्दिष्टत UI अमान्य है।",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "कुछ नेटवर्क सुरक्षा और/या गोपनीयता संबंधी जोखिम पैदा कर सकते हैं। नेटवर्क जोड़ने और उपयोग करने से पहले जोखिमों को समझें।"
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "प्रतीक 11 वर्ण या उससे कम का होना चाहिए।"
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "सिंक विफल"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "सिंक प्रगति पर है"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "मोबाइल के साथ सिंक करें"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "सुनिश्चित करें कि जब आप इस कोड को स्कैन कर रहे हों, तो आपकी स्क्रीन को कोई और न देख रहा हो"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "आपका डेटा सफलतापूर्वक सिंक कर लिया गया है। MetaMask मोबाइल ऐप का आनंद लें!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "आप अपने खाते और जानकारी को अपने मोबाइल डिवाइस के साथ सिंक कर सकते हैं। MetaMask मोबाइल ऐप खोलें, \"सेटिंग\" पर जाएं और \"ब्राउजर एक्सटेंशन से सिंक करें\" पर टैप करें"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "यदि आप पहली बार MetaMask मोबाइल ऐप खोलते हैं, तो बस अपने फोन में दिए गए चरणों का पालन करें।"
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "इस कोड को अपने MetaMask मोबाइल ऐप से स्कैन करें"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "मोबाइल के साथ सिंक करें"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% बढ़ोत्तरी"
|
||||
},
|
||||
|
27
app/_locales/hr/messages.json
generated
@ -671,12 +671,6 @@
|
||||
"settings": {
|
||||
"message": "Postavke"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Napredno upravljanje gorivom"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Odaberite ovu stavku za prikaz cijene goriva i izravno ograničite kontrole prilikom slanja i potvrđivanja zaslona."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Prikaži konverziju na usluzi Testnets"
|
||||
},
|
||||
@ -740,27 +734,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbol mora biti 11 znakova ili manje."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sinkroniziraj s mobilnim telefonom"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pazite da nitko ne gleda u vaš zaslon dok skenirate ovaj kôd"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Vaši su podatci uspješno sinkronizirani. Uživajte u mobilnoj aplikaciji MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Možete sinkronizirati svoje račune i informacije s vašim mobilnim telefonom. Otvorite mobilnu aplikaciju MetaMask, idite u stavku „Postavke” i dodirnite „Sinkroniziraj iz dodatka preglednika”"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Kad otvorite mobilnu aplikaciju MetaMask po prvi puta, pridržavajte se koraka koji se prikazuju na telefonu."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Skenirajte ovaj kôd uporabom mobilne aplikacije MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sinkroniziraj s mobilnim telefonom"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Odredbe uporabe"
|
||||
},
|
||||
|
27
app/_locales/hu/messages.json
generated
@ -671,12 +671,6 @@
|
||||
"settings": {
|
||||
"message": "Beállítások"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Speciális gázszabályzók"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Jelöld meg ezt a gázárak és korlátozásellenőrzés mutatásához közvetlenül a küldési és megerősítési képernyőkön."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Konverzió mutatása Testnetsen"
|
||||
},
|
||||
@ -740,27 +734,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "A szimbólum 0 és 12 karakter között kell legyen."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Szinkronizálás telefonnal"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Győződjön meg arról, hogy senki nem látja a képernyőt, amikor beolvassa ezt a kódot"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Adatai szinkronizálása sikerült. Élvezze a MetaMask mobilalkalmazást!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Szinkronizálhatja fiókjait és adatait a mobilkészülékkel. Nyissa meg a MetaMask mobilalkalmazást, lépjen a \"Beállítások\" elemre, majd koppintson a \"Szinkronizálás a böngésző bővítményéből\" elemre."
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Ha most először nyitja meg a MetaMask mobilalkalmazást, kövesse a telefonon megadott lépéseket."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Olvasd be ezt a kódot MetaMask mobilalkalmazásoddal"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Szinkronizálás mobillal"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Használati feltételek"
|
||||
},
|
||||
|
59
app/_locales/id/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Lihat di Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Perluas pengalaman web3 Anda dengan MetaMask Snaps"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Perluas tampilan"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Format data salah"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Kelola Snap yang Anda instal"
|
||||
},
|
||||
"max": {
|
||||
"message": "Maks"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Tidak melihat token Anda?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "Fitur 'Sinkronkan dengan ekstensi' dinonaktifkan untuk sementara waktu. Jika Anda ingin menggunakan dompet ekstensi Anda di ponsel MetaMask, maka pada aplikasi seluler Anda: kembali ke opsi pengaturan dompet dan pilih opsi 'Impor dengan Frasa Pemulihan Rahasia'. Gunakan frasa rahasia dompet ekstensi Anda untuk mengimpor dompet Anda ke ponsel nantinya."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Selanjutnya akan segera hadir..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Nonce lebih tinggi dari nonce $1 yang disarankan",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "NFT tidak dapat ditambahkan karena detail kepemilikan tidak cocok. Pastikan Anda telah memasukkan informasi yang benar."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Tampil"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Kontrol gas lanjutan"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Pilih ini untuk menampilkan biaya gas dan kontrol batas secara langsung di layar kirim dan konfirmasi."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Tampilkan konversi di jaringan uji"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Anda memberikan $2 akses kunci ke snap \"$1\". Tindakan ini tidak dapat dibatalkan dan memberikan kendali \"$1\" atas akun dan aset $2 Anda. Sebelum melanjutkan, pastikan \"$1\" aman.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Snap ini meminta izin berikut:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Perbarui Snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 memerlukan versi snap yang lebih baru.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snap"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Terjadi kesalahan dengan $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Memuat wawasan transaksi..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Snap hanya akan beroperasi jika diaktifkan"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "UI yang ditentukan oleh snap tidak valid."
|
||||
"message": "UI yang ditentukan oleh snap tidak valid.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Beberapa jaringan dapat menimbulkan risiko keamanan dan/atau privasi. Pahami risikonya sebelum menambahkan & menggunakan jaringan."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbol harus terdiri dari 11 karakter atau kurang."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Sinkronisasi gagal"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Sinkronisasi sedang berlangsung"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sinkronkan dengan seluler"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pastikan tidak ada orang lain yang melihat layar Anda saat memindai kode ini"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Data Anda telah berhasil disinkronkan. Nikmati aplikasi seluler MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Anda dapat menyinkronkan akun dan informasi dengan perangkat seluler Anda. Buka aplikasi seluler MetaMask, buka \"Pengaturan\" dan ketuk \"Sinkronkan dari Ekstensi Peramban\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Jika Anda baru membuka aplikasi seluler MetaMask untuk pertama kali, cukup ikuti langkah-langkah yang ada di ponsel Anda."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Pindai kode ini dengan aplikasi seluler MetaMask Anda"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sinkronkan dengan seluler"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "Meningkat 10%"
|
||||
},
|
||||
|
27
app/_locales/it/messages.json
generated
@ -1470,12 +1470,6 @@
|
||||
"settings": {
|
||||
"message": "Impostazioni"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controlli gas avanzati"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Seleziona per visualizzare i controlli su prezzo e limite del gas nelle schermate di invio e conferma."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Mostra conversione nelle reti di test"
|
||||
},
|
||||
@ -1776,27 +1770,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Il simbolo deve essere lungo tra 0 e 12 caratteri."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronizza con dispositivo mobile"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Assicurati che nessun'altro stia guardando al tuo schermo quando scansioni questo codice"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "I tuoi dati sono stati sincronizzati con successo. Goditi l'app di MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Puoi sincronizzare i tuoi account e le tue informazioni con il tuo dispositivo mobile. Apri l'app di MetaMask, vai su \"Impostazioni\" e tocca \"Sincronizza da Estensione sul Browser\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Se hai appena aperto l'app di MetaMask per la prima volta, segui i passaggi sul tuo telefono."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Scansiona questo codice con l'app di MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronizza con dispositivo mobile"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Termini di Uso"
|
||||
},
|
||||
|
59
app/_locales/ja/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Etherscanで表示"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "MetaMask Snaps で web3 エクスペリエンスを拡張"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "ビューを展開"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "不正な形式のデータ"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "インストールされたスナップの管理"
|
||||
},
|
||||
"max": {
|
||||
"message": "最大"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "トークンが見当たりませんか?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "「拡張機能と同期」機能は一時的に無効になっています。拡張ウォレットをMetaMaskモバイルで使用する場合は、モバイルアプリでウォレットの設定オプションに戻り、「シークレットリカバリーフレーズでインポート」オプションを選択します。拡張ウォレットのシークレットフレーズを使用して、ウォレットをモバイルにインポートします。"
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "さらに近日追加予定..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "ナンスが提案され$1よりも大きいです",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "所有者情報が一致していないため、NFT を追加できません。入力された情報が正しいことを確認してください。"
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "表示"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "高度なガスコントロール"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "これを選択すると、ガス代と限度額のコントロールが送金画面と確認画面に直接表示されます。"
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "テストネット上に変換を表示"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "スナップ「$1」に $2 へのキーアクセスを許可しようとしています。この操作は取り消し不能であり、$2 アカウントとアセットのコントロールを「$1」に許可することになります。続行する前に、必ず「$1」が信頼できることを確認してください。",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "このスナップが次のパーミッションをリクエストしています:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "スナップを更新"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 に新しいバージョンのスナップが必要です。",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "スナップ"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "$1 でエラーが発生しました: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "トランザクションインサイトを読み込み中..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "スナップは有効になっている場合にのみ実行されます"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "スナップにより指定された UI が無効です。"
|
||||
"message": "スナップにより指定された UI が無効です。",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "ネットワークによっては、セキュリティやプライバシーの面でリスクが伴う可能性があります。ネットワークを追加・使用する前にリスクを理解するようにしてください。"
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "シンボルは11文字以下にする必要があります。"
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "同期に失敗しました"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "同期中"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "モバイルと同期"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "このコードをスキャンするとき、画面を誰にも見られていないことを確認してください"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "データの同期に成功しました。MetaMaskモバイルアプリをご活用ください!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "アカウントと情報を、モバイルデバイスと同期させることができます。MetaMaskモバイルアプリを開き、「設定」に進み、「ブラウザ拡張機能から同期」をタップします。"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "MetaMaskモバイルアプリを初めて開く場合は、スマートフォンを以下のステップに従って操作してください。"
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "MetaMaskモバイルアプリでこのコードをスキャンします"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "モバイルと同期"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% の増加"
|
||||
},
|
||||
|
27
app/_locales/kn/messages.json
generated
@ -678,12 +678,6 @@
|
||||
"settings": {
|
||||
"message": "ಸೆಟ್ಟಿಂಗ್ಗಳು"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "ಸುಧಾರಿತ ಗ್ಯಾಸ್ ನಿಯಂತ್ರಣಗಳು"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "ಕಳುಹಿಸುವ ಮತ್ತು ಖಚಿತಪಡಿಸುವ ಪರದೆಯ ಮೇಲೆ ನೇರವಾಗಿ ಗ್ಯಾಸ್ ಬೆಲೆ ಮತ್ತು ಮಿತಿಯ ನಿಯಂತ್ರಣಗಳನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Testnets ನಲ್ಲಿ ಪರಿವರ್ತನೆಯನ್ನು ತೋರಿಸಿ"
|
||||
},
|
||||
@ -747,27 +741,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "ಚಿಹ್ನೆಯು 0 ಮತ್ತು 12 ಅಕ್ಷರಗಳ ನಡುವೆ ಇರಬೇಕು."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "ಮೊಬೈಲ್ನೊಂದಿಗೆ ಸಿಂಕ್ ಮಾಡಿ"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "ನೀವು ಈ ಕೋಡ್ ಅನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತಿರುವಾಗ ಯಾರೂ ನಿಮ್ಮ ಪರದೆಯ ಕಡೆಗೆ ನೋಡುತ್ತಿಲ್ಲವೇ ಎಂಬುದನ್ನು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ."
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "ನಿಮ್ಮ ಡೇಟಾ ಯಶಸ್ವಿಯಾಗಿ ಸಿಂಕ್ ಆಗಿದೆ. MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಆನಂದಿಸಿ!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "ನಿಮ್ಮ ಮೊಬೈಲ್ ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಖಾತೆಗಳು ಮತ್ತು ಮಾಹಿತಿಯನ್ನು ನೀವು ಸಿಂಕ್ ಮಾಡಬಹುದಾಗಿದೆ. MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ತೆರೆಯಿರಿ, \"ಸೆಟ್ಟಿಂಗ್ಗಳಿಗೆ\" ಹೋಗಿ ಮತ್ತು \"ಬ್ರೌಸರ್ ವಿಸ್ತರಣೆಯಿಂದ ಸಿಂಕ್ ಮಾಡಿ\" ಟ್ಯಾಪ್ ಮಾಡಿ"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "ನೀವು ಮೊದಲ ಬಾರಿಗೆ MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ತೆರೆದರೆ, ನಿಮ್ಮ ಫೋನ್ನಲ್ಲಿರುವ ಹಂತಗಳನ್ನು ಅನುಸರಿಸಿ."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "ನಿಮ್ಮ MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಮೂಲಕ ಈ ಕೋಡ್ ಅನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿ"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "ಮೊಬೈಲ್ನೊಂದಿಗೆ ಸಿಂಕ್ ಮಾಡಿ"
|
||||
},
|
||||
"terms": {
|
||||
"message": "ಬಳಕೆಯ ನಿಯಮಗಳು"
|
||||
},
|
||||
|
59
app/_locales/ko/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Etherscan에서 보기"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "MetaMask 스냅으로 web3 경험을 확대하세요"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "보기 확장"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "잘못된 데이터"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "설치된 스냅을 관리하세요"
|
||||
},
|
||||
"max": {
|
||||
"message": "최대"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "토큰이 보이지 않나요?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "'확장 프로그램과 동기화' 기능이 일시적으로 비활성화됩니다. MetaMask 모바일에서 확장 지갑을 사용하려면 모바일 앱에서 지갑 설정 옵션으로 돌아가 '비밀 복구 구문 가져오기' 옵션을 선택하세요. 확장 지갑의 비밀 구문을 사용하시면 지갑을 모바일로 가져올 수 있습니다."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "더 추가 예정..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "임시값이 권장 임시값인 $1보다 큽니다.",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT\n"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "소유권 정보가 일치하지 않아 NFT를 추가할 수 없습니다. 올바른 정보를 입력했는지 확인하세요."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "보기"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "고급 가스 제어 기능"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "이 항목을 선택하면 보내기 및 확인 화면에서 바로 가스 가격과 한도 조절을 확인할 수 있습니다."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "테스트넷에 전환 표시"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "'$1' 스냅 이용에 필요한 $2 키 액세스 권한을 부여하고 있습니다. 이 작업은 사용자의 $2 계정과 자산에 '$1' 제어 권한을 부여하며 취소가 불가능합니다. '$1의 신뢰성을 확인한 후에 진행하세요.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "이 스냅이 다음 권한을 요청하고 있습니다."
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "스냅 업데이트"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1에는 스냅의 새 버전이 필요합니다",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "스냅"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "$1 관련 오류 발생: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "거래 인사이트를 가져오는 중..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "스냅은 활성화된 상태에서만 작동합니다."
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "스냅에서 지정한 UI가 올바르지 않습니다."
|
||||
"message": "스냅에서 지정한 UI가 올바르지 않습니다.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "네트워크에 따라 보안이나 개인 정보 유출의 위험이 있을 수 있습니다. 네트워크 추가 및 사용 이전에 위험 요소를 파악하세요."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "기호는 11자 이하여야 합니다."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "동기화 실패"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "동기화 진행 중"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "모바일과 동기화"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "이 코드를 스캔할 때는 다른 사람이 화면을 보지 못하게 하세요"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "데이터가 동기화되었습니다. MetaMask 모바일 앱을 마음껏 이용하세요!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "계정과 정보를 모바일 장치와 동기화할 수 있습니다. MetaMask 모바일 앱을 열고 \"설정\"으로 이동하여 \"브라우저 확장에서 동기화\"를 탭합니다."
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "MetaMask 모바일 앱을 처음 여는 경우라면 휴대폰에 나타나는 지시사항을 따르세요."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "MetaMask 모바일 앱으로 이 코드를 스캔하세요"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "모바일과 동기화"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% 인상"
|
||||
},
|
||||
|
27
app/_locales/lt/messages.json
generated
@ -678,12 +678,6 @@
|
||||
"settings": {
|
||||
"message": "Nustatymai"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Išplėstiniai dujų valdikliai"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Pasirinkite tai, kad būtų rodoma dujų kaina, ir ribokite valdymo elementus tiesiogiai siuntimo ir patvirtinimo ekranuose."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Rodyti keitimą „Testnet“"
|
||||
},
|
||||
@ -747,27 +741,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbolis turi būti ne ilgesnis nei 11 simbolių."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sinchronizuoti su mobiliuoju"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pasirūpinkite, kad jums nuskaitant šį kodą niekas nežiūrėtų į jūsų ekraną."
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Jūsų duomenys sėkmingai sinchronizuoti. Mėgaukitės „MetaMask“ mobiliąja programa! "
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Galite sinchronizuoti paskyrą ir informaciją su savo mobiliuoju įrenginiu. Atverkite „MetaMask“ mobiliąją programą, eikite į „Nuostatos“ ir palieskite „Sinchronizuoti iš naršyklės plėtinio“"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Jeigu „MetaMask“ mobiliąją programą atveriate tik pirmą kartą, tiesiog sekite veiksmus telefone."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Nuskaitykite šį kodą su savo „MetaMask“ mobiliąja programa"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sinchronizuoti su mobiliuoju"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Naudojimo sąlygos"
|
||||
},
|
||||
|
27
app/_locales/lv/messages.json
generated
@ -674,12 +674,6 @@
|
||||
"settings": {
|
||||
"message": "Iestatījumi"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Papildu Gas vadīklas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Atlasiet šo, lai parādītu Gas cenu un ierobežotu kontroles iespējas tieši sūtīšanas un apstiprināšanas ekrānos."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Rādīt konversiju testa tīklos"
|
||||
},
|
||||
@ -743,27 +737,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbolā nedrīkst būt vairāk par 11 rakstzīmēm."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sinhronizēt ar tālruni"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pārliecinieties, ka neviens cits neskatās jūsu ekrānā, kad skenējat šo kodu."
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Jūsu dati sekmīgi sinhronizēti. Patīkamu MetaMask mobilās lietotnes lietošanu!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Jūs varat sinhronizēt savus kontus un informāciju ar mobilo ierīci. Atveriet MetaMask mobilo lietotni, ejiet uz \"Iestatījumi\" un pieskarieties pie \"Sinhronizēt no pārlūka paplašinājuma\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Ja esat pirmoreiz atvēris MetaMask mobilo lietotni, vienkārši sekojiet norādēm tālrunī."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Noskenējiet šo kodu ar MetaMask mobilo lietotni"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sinhronizēt ar tālruni"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Lietošanas noteikumi"
|
||||
},
|
||||
|
27
app/_locales/ms/messages.json
generated
@ -658,12 +658,6 @@
|
||||
"settings": {
|
||||
"message": "Tetapan"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Kawalan gas lanjutan"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Pilih ini untuk menunjukkan harga gas dan kawalan had terus di skrin hantar dan sahkan."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Tunjukkan Penukaran di Testnets"
|
||||
},
|
||||
@ -727,27 +721,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbol mestilah 11 aksara atau kurang."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Segerakkan dengan telefon mudah alih"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pastikan tiada orang lain melihat skrin anda ketika anda mengimbas kod ini"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Data anda berjaya disegerakkan. Nikmati aplikasi mudah alih MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Anda boleh menyegerakkan akaun dan maklumat anda dengan peranti mudah alih anda. Buka ap mudah alih MetaMask, pergi ke \"Tetapan\" dan ketik \"Segerakkan daripada Sambungan Pelayar\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Jika ini kali pertama anda membuka aplikasi mudah alih MetaMask, anda cuma perlu ikuti langkah-langkah di telefon anda."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Imbas kod ini dengan aplikasi mudah alih MetaMask anda"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Segerakkan dengan mudah alih"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Syarat-syarat Penggunaan"
|
||||
},
|
||||
|
27
app/_locales/no/messages.json
generated
@ -659,12 +659,6 @@
|
||||
"settings": {
|
||||
"message": "Innstillinger"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Avanserte datakraftskontroller"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Velg dette for å vise bensinpris og begrensningskontroller direkte på send- og bekreftskjermbildene."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Vis konvertering på Testnets "
|
||||
},
|
||||
@ -725,27 +719,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbolet må være 11 tegn eller færre."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synkroniser med mobil "
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pass på at ingen andre ser på skjermen din mens du skanner denne koden "
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Dataene dine er blitt synkronisert med suksess. Kos deg med mobilappen for MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Du kan synkronisere kontoene og informasjonen din med den mobile enheten din. Åpne mobilappen for MetaMask, gå til \"Innstillinger\" og trykk på \"Synkronisering fra nettleserutvidelse\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Hvis du åpner mobilappen for MetaMask for første gang, følger du bare trinnene på telefonen."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Skann denne koden med din mobilapp for MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synkronisér med mobil"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Brukervilkår"
|
||||
},
|
||||
|
27
app/_locales/ph/messages.json
generated
@ -1320,12 +1320,6 @@
|
||||
"settings": {
|
||||
"message": "Mga Setting"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Mga advanced na kontrol sa gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Ipakita ang Conversion sa Testnets"
|
||||
},
|
||||
@ -1717,27 +1711,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Dapat ay 11 character o mas kaunti ang simbolo."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "I-sync sa mobile"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Tiyaking walang ibang nakakakita sa iyong screen kapag na-scan mo ang code na ito"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Matagumpay na na-sync ang iyong data. I-enjoy ang MetaMask mobile app!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Puwede mong i-sync ang iyong mga account at impormasyon sa mobile device mo. Buksan ang MetaMask mobile app, pumunta sa \"Mga Setting\" at mag-tap sa \"I-sync mula sa Browser Extension\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Kung unang pagkakataon mong bubuksan ang MetaMask Mobile app, sundin lang ang mga hakbang sa iyong telepono."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "I-scan ang code na ito gamit ang iyong MetaMask mobile app"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "I-sync sa mobile"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Mga Tuntunin ng Paggamit"
|
||||
},
|
||||
|
27
app/_locales/pl/messages.json
generated
@ -672,12 +672,6 @@
|
||||
"settings": {
|
||||
"message": "Ustawienia"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Zaawansowana kontrola gazu"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Wybierz tę opcję, aby móc zmienić cenę i limit gazu bezpośrednio na ekranach wysyłania i potwierdzania."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Pokaż przeliczanie w sieciach testowych"
|
||||
},
|
||||
@ -738,27 +732,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbol musi mieć maksymalnie 11 znaków."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synchronizuj z telefonem"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Upewnij się, że nikt inny nie patrzy na Twój ekran podczas skanowania tego kodu"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Twoje dane zostały zsynchronizowane. Miłego korzystania z aplikacji mobilnej MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Możesz synchronizować swoje konta i informacje z urządzeniem mobilnym. Otwórz aplikację mobilną MetaMask, przejdź do „Ustawień” i wybierz opcję „Synchronizuj z rozszerzeniem przeglądarki”."
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Jeśli po raz pierwszy otwierasz aplikację MetaMask Mobile, postępuj zgodnie z instrukcjami w telefonie."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Zeskanuj ten kod za pomocą aplikacji mobilnej MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synchronizuj z telefonem"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Regulamin"
|
||||
},
|
||||
|
59
app/_locales/pt/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Ver no Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Expanda sua experiência web3 com os snaps da MetaMask"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Expandir exibição"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Dados inválidos"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Gerencie seus snaps instalados"
|
||||
},
|
||||
"max": {
|
||||
"message": "Máximo"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Não está vendo seu token?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "A funcionalidade \"Sincronizar com a extensão\" está temporariamente desativada. Se você quer usar sua carteira de extensão na MetaMask mobile, então, no seu app mobile: volte às opções de configuração da carteira e selecione a opção \"Importar com frase de recuperação secreta\". Use a frase secreta da sua carteira de extensão para, então, importar a sua carteira no celular."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Mais em breve..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Nonce é maior que o nonce sugerido de $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "O NFT não pôde ser adicionado, pois os dados de propriedade não coincidem. Certifique-se de ter inserido as informações corretas."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Exibir"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controles avançados de gás"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Mostrar conversão nas redes de teste"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Você está concedendo ao snap \"$1\" acesso à sua chave $2. Isso é irrevogável e concede a \"$1\" controle de suas contas e ativos $2. Certifique-se de que confia em \"$1\" antes de prosseguir.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Esse snap está solicitando as seguintes permissões:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Atualizar snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 precisa de uma versão mais nova do seu snap.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snaps"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Ocorreu um erro com $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Carregando insight da transação..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "O snap só será executado se estiver ativado"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "A IU especificada pelo snap é inválida."
|
||||
"message": "A IU especificada pelo snap é inválida.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Algumas redes podem representar riscos de segurança e/ou privacidade. Tenha os riscos em mente antes de adicionar e usar uma rede."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "O símbolo deve ter 11 caracteres ou menos."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Falha na sincronização"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Sincronização em andamento"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronizar com dispositivo móvel"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Ao escanear esse código, verifique se não há mais ninguém olhando para a sua tela"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Seus dados foram sincronizados. Curta o app da MetaMask para dispositivos móveis!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Você pode sincronizar suas contas e informações com o seu dispositivo móvel. Abra o aplicativo da MetaMask para dispositivos móveis, acesse \"Configurações\" e toque em \"Sincronizar pela extensão do navegador\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Se você tiver acabado de abrir o app da MetaMask para dispositivos móveis pela primeira vez, basta seguir as etapas no seu telefone."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Escaneie esse código com seu app da MetaMask para dispositivos móveis"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronizar com dispositivo móvel"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% de aumento"
|
||||
},
|
||||
|
36
app/_locales/pt_BR/messages.json
generated
@ -1369,9 +1369,6 @@
|
||||
"missingToken": {
|
||||
"message": "Não está vendo o seu token?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "A funcionalidade \"Sincronizar com a extensão\" está temporariamente desativada. Se você quer usar sua carteira de extensão na MetaMask mobile, então, no seu app mobile: volte às opções de configuração da carteira e selecione a opção \"Importar com frase de recuperação secreta\". Use a frase secreta da sua carteira de extensão para, então, importar a sua carteira no celular."
|
||||
},
|
||||
"mustSelectOne": {
|
||||
"message": "Selecione pelo menos 1 token."
|
||||
},
|
||||
@ -2070,12 +2067,6 @@
|
||||
"show": {
|
||||
"message": "Mostrar"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controles avançados de gás"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Mostrar conversão nas redes de teste"
|
||||
},
|
||||
@ -2555,33 +2546,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "O símbolo deve ter até 11 caracteres."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Falha na sincronização"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Sincronização em andamento"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronizar com dispositivo móvel"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Ao escanear esse código, verifique se não há mais ninguém olhando para a sua tela"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Seus dados foram sincronizados. Curta o app da MetaMask para dispositivos móveis!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Você pode sincronizar suas contas e informações com o seu dispositivo móvel. Abra o aplicativo da MetaMask para dispositivos móveis, acesse \"Configurações\" e toque em \"Sincronizar pela extensão do navegador\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Se você tiver acabado de abrir o app da MetaMask para dispositivos móveis pela primeira vez, basta seguir as etapas no seu telefone."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Escaneie esse código com seu app da MetaMask para dispositivos móveis"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronizar com dispositivo móvel"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% de aumento"
|
||||
},
|
||||
|
27
app/_locales/ro/messages.json
generated
@ -665,12 +665,6 @@
|
||||
"settings": {
|
||||
"message": "Setări"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Controale avansate pentru gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Selectați aceasta pentru a arăta prețul gasului și comenzile de limitare direct pe ecranele de trimitere și confirmare."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Afișează conversiile pe rețelele de test (testnets)"
|
||||
},
|
||||
@ -734,27 +728,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbolul trebuie să fie de 11 caractere sau mai puțin."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sincronizați cu dispozitivul mobil"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Asigurați-vă că nimeni altcineva nu poate vedea ecranul dvs. când scanați acest cod"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Datele dvs. au fost sincronizate cu succes. Bucurați-vă de aplicația mobilă MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Vă puteți sincroniza conturile și informațiile cu dispozitivul dvs. mobil. Deschideți aplicația mobilă MetaMask, mergeți la „Setări” și atingeți „Sincronizare de la extensia de browser”"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Dacă deschideți aplicația pentru mobil MetaMask pentru prima oară, urmați pașii afișați pe telefonul dvs."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Scanați acest cod folosind aplicația dvs. mobilă MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sincronizați cu mobilul"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Termeni și condiții"
|
||||
},
|
||||
|
59
app/_locales/ru/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Посмотреть на Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Расширьте свои возможности web3 с помощью MetaMask Snaps"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Развернуть представление"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Искаженные данные"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Управляйте установленными снапами"
|
||||
},
|
||||
"max": {
|
||||
"message": "Макс."
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Не видите свой токен?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "Функция «Синхронизация с расширением» временно отключена. Если вы хотите использовать свой кошелек из расширения браузера в мобильной версии MetaMask, тогда в мобильном приложении вернитесь к параметрам настройки кошелька и выберите параметр «Импортировать с помощью секретной фразы для восстановления». Используйте секретную фразу своего кошелька из расширения, чтобы импортировать кошелек на мобильное устройство."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Скоро появится больше..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Одноразовый номер больше, чем предложенный одноразовый номер $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "Невозможно добавить NFT, так как сведения о владельце не совпадают. Убедитесь, что вы ввели правильную информацию."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Показать"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Расширенное управление газом"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Выберите это, чтобы отображать цену газа и управление лимитами непосредственно на экранах отправки и подтверждения."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Показывать конвертацию в тестовых сетях"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Вы предоставляете ключ доступа $2 к привязке \"$1\". Это действие нельзя отменить, и оно предоставляет \"$1\" управление всеми счетами и активами $2. Перед тем как продолжить, убедитесь, что доверяете \"$1\".",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Этот снап запрашивает следующие разрешения:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Обновить снап"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 нужна более новая версия вашей привязки.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Снапы"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Произошла ошибка с $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Загрузка аналитики по транзакции..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Снап будет работать только в том случае, если он включен"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Пользовательский интерфейс, указанный привязкой, недействителен."
|
||||
"message": "Пользовательский интерфейс, указанный привязкой, недействителен.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Некоторые сети могут представлять угрозу безопасности и/или конфиденциальности. Прежде чем добавлять и использовать сеть, ознакомьтесь с рисками."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Символ должен состоять из 11 или менее знаков."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Ошибка синхронизации"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Выполняется синхронизация"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Синхронизировать с мобильным устройством"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Убедитесь, что никто не смотрит на ваш экран, когда вы сканируете этот код"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Ваши данные успешно синхронизированы. Наслаждайтесь мобильным приложением MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Вы можете синхронизировать свои счета и информацию со своим мобильным устройством. Откройте мобильное приложение MetaMask, перейдите в раздел «Настройки» и нажмите «Синхронизировать из расширения браузера»."
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Если вы открываете приложение MetaMask Mobile в первый раз, просто следуйте инструкциям на телефоне."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Отсканируйте этот код с помощью мобильного приложения MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Синхронизировать с мобильным устройством"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "Увеличение на 10%"
|
||||
},
|
||||
|
27
app/_locales/sk/messages.json
generated
@ -650,12 +650,6 @@
|
||||
"settings": {
|
||||
"message": "Nastavení"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Pokročilé ovládacie prvky GAS"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Vyberte túto možnosť vtedy, keď chcete priamo na obrazovke odosielania a potvrdenia zobraziť ceny za GAS a ovládacie prvky limitov."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Zobraziť konverziu na Testnets"
|
||||
},
|
||||
@ -716,27 +710,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbol musí být mezi 0 a 12 znaky."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synchronizácia s mobilom"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Pri skenovaní tohto kódu sa uistite, že sa nikto iný nedíva na vašu obrazovku"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Vaše údaje boli úspešne synchronizované. Užite si mobilnú aplikáciu MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Svoje účty a informácie môžete synchronizovať so svojim mobilným zariadením. Otvorte mobilnú aplikáciu MetaMask, prejdite na „Nastavenia“ a kliknite na „Synchronizovať z rozšírenia prehliadača“."
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Ak otvoríte mobilnú aplikáciu MetaMask prvýkrát, postupujte podľa pokynov v telefóne."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Naskenujte tento kód pomocou mobilnej aplikácie MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synchronizácia s mobilom"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Podmínky použití"
|
||||
},
|
||||
|
27
app/_locales/sl/messages.json
generated
@ -666,12 +666,6 @@
|
||||
"settings": {
|
||||
"message": "Nastavitve"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Napredno krmiljenje plina"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Izberite to možnost, če želite prikazati ceno plina in omejiti nadzor neposredno na zaslonih za pošiljanje in potrditev."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Pokažite pretvorbo na testnih omrežjih"
|
||||
},
|
||||
@ -735,27 +729,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbol mora biti največ 11 znakov ali manj."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sinhroniziraj z mobilnimi telefonom"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Ko skenirate to kodo, naj nihče ne kuka na vaš zaslon"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Vaši podatki so uspešno sinhronizirani. Uživajte v mobilni aplikaciji MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Račune in podatke lahko sinhronizirate s svojo mobilno napravo. Odprite mobilno aplikacijo MetaMask, pojdite na \"Nastavitve\" in tapnite \"Sinhroniziraj z razširitvijo brskalnika\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Če ste sedaj prvič odprli aplikacijo MetaMask Mobile, samo sledite korakom v telefonu."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Skenirajte to kodo z mobilno aplikacijo MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sinhroniziraj z mobilnimi telefonom"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Pogoji uporabe"
|
||||
},
|
||||
|
27
app/_locales/sr/messages.json
generated
@ -669,12 +669,6 @@
|
||||
"settings": {
|
||||
"message": "Podešavanja"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Napredne kontrole gasa"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Izaberite ovo kako biste prikazali cenu gasa i kontrole limita direktno na ekranima za slanje i potvrđivanje."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Prikažite konverzije na Testnet"
|
||||
},
|
||||
@ -738,27 +732,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Simbol mora biti 11 znakova ili manje."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Sinhronizacija sa mobilnim telefonom"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Obratite pažnju da niko drugi ne gleda u vaš ekran kad skenirate ovaj kod"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Vaši podaci su uspešno sinhronizovani. Uživajte u mobilnoj aplikaciji MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Možete sinhronizovati vaše naloge i informacije sa svojim mobilnim uređajem. Otvorite mobilnu aplikaciju MetaMask, idite na \"Podešavanja\" i pritisnite \"Sinhronizuj iz ekstenzije za pregledač\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Ako prvi put otvorite aplikaciju MetaMask Mobile, samo sledite korake u svom telefonu."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Skenirajte ovaj kod uz pomoć svoje MetaMask mobilne aplikacije"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Sinhronizujte sa mobilnim uređajem"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Uslovi korišćenja"
|
||||
},
|
||||
|
27
app/_locales/sv/messages.json
generated
@ -662,12 +662,6 @@
|
||||
"settings": {
|
||||
"message": "Inställningar"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Avancerade gaskontroller"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Välj detta för att visa gas-pris och gränskontroller direkt på skicka och bekräfta-skärmarna."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Visa omvandling på testnätverk"
|
||||
},
|
||||
@ -728,27 +722,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbolen måste vara 11 tecken eller färre."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Synka med mobil"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Försäkra dig om att ingen tittar på din skärm när du skannar denna kod"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Din data har nu synkats. Ha det så kul med MetaMasks mobil-app!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Du kan nu synka dina konton och din information med din mobil-enhet. Öppna MetaMasks mobil-app, gå till \"Inställningar\" och tryck på \"Synka från webbläsartillägg\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Om du öppnar MetaMask mobilapplikation för första gången behöver du bara följa stegen som visas i telefonen."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Skanna den här koden med din MetaMask mobilapplikation"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Synka med mobil"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Användarvillkor"
|
||||
},
|
||||
|
27
app/_locales/sw/messages.json
generated
@ -656,12 +656,6 @@
|
||||
"settings": {
|
||||
"message": "Mipangilio"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Udhibiti wa juu wa gesi"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Chagua hii ili uonyeshe bei ya gesi na punguza vidhibiti moja kwa moja kwenye skrini za tuma na thibitisha."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Onyesha Ubadilishaji kwenye Testnets"
|
||||
},
|
||||
@ -725,27 +719,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Alama lazima iwe na herufi 11 au chache."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Oanisha na simu"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Hakikisha hakuna mtu mwingine anayeangalia kwenye skrini yako unapokuwa unakagua msimbo huu."
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Umefanikiwa kuoanisha data yako. Furahia programu yako ya simu ya MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Unaweza kuoanisha akaunti zako na taarifa kwa kifaa chako cha simu ya mkononi. Fungua programu ya simu ya MetaMask, kisha nenda kwenye \"Mipangilio\" na bofya kwenye \"Oanisha kutoka kwenye Kiendelezi cha Kivinjari\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Ikiwa ndio umefungua tu programu ya simu ya MetaMask kwa mara ya kwanza, fuata hatua kwenye simu yako."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Kagua msimbo huu kwa kutumia programu yako ya simu ya MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Oanisha na simu"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Masharti ya Matumizi"
|
||||
},
|
||||
|
6
app/_locales/th/messages.json
generated
@ -344,9 +344,6 @@
|
||||
"settings": {
|
||||
"message": "การตั้งค่า"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "การควบคุม Gas ขั้นสูง"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "แสดงคีย์ส่วนตัว"
|
||||
},
|
||||
@ -380,9 +377,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "สัญลักษณ์จะต้องมีความยาว 11 ตัวอักษร"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "ซิงค์ข้อมูลของคุณเรียบร้อยแล้ว ใช้แอพ MetaMask ให้สนุกนะ!"
|
||||
},
|
||||
"terms": {
|
||||
"message": "ข้อตกลงในการใช้งาน"
|
||||
},
|
||||
|
59
app/_locales/tl/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Tingnan sa Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Palawakin ang iyong karanasan sa web3 gamit ang MetaMask Snaps"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "I-expand ang view"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Pangit na datos"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Pamahalaan ang iyong mga naka-install na snap"
|
||||
},
|
||||
"max": {
|
||||
"message": "Max"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Hindi mo ba nakikita ang iyong mga token?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "Ang feature na 'I-sync gamit ang extension' ay pansamantalang hindi gumagana. Kung gusto mong gamitin ang iyong extension wallet sa MetaMask mobile, pagkatapos ay sa iyong mobile app: bumalik sa mga opsyon sa pag-setup ng wallet at piliin ang opsyong 'Mag-import gamit ang Secret Recovery Phrase'. Gamitin ang lihim na parirala ng iyong extension wallet upang pagkatapos ay i-import ang iyong wallet sa mobile."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Marami pang parating..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Mas mataas ang noncesa iminumungkahing nonce na $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "Hindi maidaragdag ang NFT dahil hindi tumutugma ang mga detalye ng pagmamay-ari. Siguraduhing tamang impormasyon ang iyong nailagay."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Ipakita"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Mga advanced na kontrol sa gas"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Ipakita ang Conversion sa Testnets"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Binibigyan mo ang $2 ng key access sa snap na \"$1\". Hindi na ito mababawi at nagbibigay ito sa \"$1\" ng kontrol sa iyong mga $2 account at asset. Tiyaking pinagkakatiwalaan mo ang \"$1\" bago magpatuloy.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Hinihiling ng snap na ito ang mga sumusunod na pahintulot:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "I-update ang snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "Kailangan ng $1 ng bagong bersyon ng iyong snap.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Mga Snap"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Nagkaroon ng error sa $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Naglo-load ng insight sa transaksyon..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Tatakbo lamang ang snap kapag pinagana ito"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Ang UI na tinukoy sa pamamagitan ng snap ay hindi wasto."
|
||||
"message": "Ang UI na tinukoy sa pamamagitan ng snap ay hindi wasto.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Maaaring magdulot ang ilang network ng mga panganib sa seguridad at/o pagkapribado. Unawain ang mga panganib bago idagdag o gamitin ang isang network."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Dapat ay 11 character o mas kaunti ang simbolo."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Bigong ma-sync"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Kasalukuyang nagsi-sync"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "I-sync sa mobile"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Tiyaking walang ibang nakakakita sa iyong screen kapag na-scan mo ang code na ito"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Matagumpay na na-sync ang iyong data. I-enjoy ang MetaMask mobile app!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Puwede mong i-sync ang iyong mga account at impormasyon sa mobile device mo. Buksan ang MetaMask mobile app, pumunta sa \"Mga Setting\" at i-tap ang \"I-sync mula sa Browser Extension\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Kung unang pagkakataon mong bubuksan ang MetaMask Mobile app, sundin lang ang mga hakbang sa iyong telepono."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "I-scan ang code na ito gamit ang iyong MetaMask mobile app"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "I-sync sa mobile"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "10% na dagdag"
|
||||
},
|
||||
|
59
app/_locales/tr/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Etherscan'de görüntüle"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "MetaMask Snap'leri ile web3 deneyimini genişlet"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Görünümü genişlet"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Hatalı biçimlendirilmiş veri"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Yüklü snap'lerini yönet"
|
||||
},
|
||||
"max": {
|
||||
"message": "Maksimum"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Tokeninizi görmüyor musunuz?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "\"Uzantı ile senkronize et\" özelliği geçici olarak devre dışı bırakılmış. MetaMask mobilde uzantı cüzdanınızı kullanmak istiyorsanız mobil uygulamaya gidin: cüzdan kurulum ayarlarına geri dönün ve \"Gizli Kurtarma İfadesi ile İçe Aktar\" seçeneğini seçin. Ardından cüzdanınızı mobil uygulamada içe aktarmak için uzantı cüzdanınızın gizli ifadesini kullanın."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Daha fazlası çok yakında..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Geçici anahtar, önerilen $1 geçici anahtarından daha büyük",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "Sahiplik bilgileri eşleşmediği için NFT eklenemiyor. Doğru bilgileri girdiğinizden emin olun."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Göster"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Gelişmiş gaz kontrolleri"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Gaz fiyatı ve limit kontrollerini doğrudan gönder ve onayla ekranlarında göstermek için bunu seçin."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Test ağlarında dönüşümü göster"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "\"$1\" için $2 anahtar erişimi veriyorsunuz. Bu iptal edilemez ve $2 hesaplarınıza ve varlıklarınıza \"$1\" kontrolü verir. İlerlemeden önce \"$1\" alanına güvendiğinizden emin olun.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Bu ek, aşağıdaki izinleri istiyor:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Snap'i güncelle"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 için daha yeni bir snap sürümü gerekli.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snap'ler"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "$1: $2 ile ilgili bir hata oldu",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "İşlem ayrıntıları yükleniyor..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Bir snap yalnızca etkinleştirilmişse çalışır"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Snap tarafından belirtilen Kullanıcı Arayüzü geçersiz."
|
||||
"message": "Snap tarafından belirtilen Kullanıcı Arayüzü geçersiz.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Bazı ağlar güvenlik ve/veya gizlilik riskleri teşkil edebilir. Bir ağ eklemeden ve kullanmadan önce riskleri anlayın."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Sembol en fazla 11 karakter olmalıdır."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Senkronizasyon başarısız oldu"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Senkronizasyon sürüyor"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Mobil ile senkronize et"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Bu kodu tararken hiç kimsenin ekranınıza bakmadığından emin olun"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Verileriniz başarılı bir şekilde senkronize edildi. MetaMask mobil uygulamasının tadını çıkarın!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Hesaplarınızı ve bilgilerinizi mobil cihazınızla senkronize edebilirsiniz. MetaMask mobil uygulamasını açın, \"Ayarlar\" kısmına gidin ve \"Tarayıcı Uzantısından Senkronize Et\" seçeneğine dokunun"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Metamask Mobil uygulamasını ilk defa açıyorsanız telefonunuzdaki adımları izleyin."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "MetaMask mobil uygulamanızla bu kodu tarayın"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Mobil ile senkronize et"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "%10 artış"
|
||||
},
|
||||
|
27
app/_locales/uk/messages.json
generated
@ -678,12 +678,6 @@
|
||||
"settings": {
|
||||
"message": "Налаштування"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Розширене керування газом"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Виберіть цей параметр, щоб відображати регулятори ціни й ліміту газу на екранах надсилання й підтвердження."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Показати бесіду у Testnet"
|
||||
},
|
||||
@ -747,27 +741,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Символ повинен містити 11 символів або менше."
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Синхронізувати з мобільним пристроєм"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Переконайтесь, що ніхто не дивиться на ваш екран, коли скануєте цей код"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Ваші дані були успішно синхронізовані. Насолоджуйтесь мобільним застосунком MetaMask!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Ви можете синхронізувати ваші облікові записи та інформацію з вашим мобільним пристроєм. Відкрийте мобільний застосунок MetaMask, перейдіть до \"Налаштування\" та клацніть на \"Синхронізувати з розширення браузера\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Якщо ви відкрили мобільний застосунок MetaMask вперше, просто слідуйте крокам у вашому телефоні."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Відскануйте цей код за допомогою мобільної програми MetaMask"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Синхронізувати з мобільним"
|
||||
},
|
||||
"terms": {
|
||||
"message": "Умови використання"
|
||||
},
|
||||
|
59
app/_locales/vi/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "Xem trên Etherscan"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "Mở rộng trải nghiệm web3 của bạn với MetaMask Snap"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Mở rộng cửa sổ xem"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "Dữ liệu không đúng định dạng"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "Quản lý các Snap đã cài đặt"
|
||||
},
|
||||
"max": {
|
||||
"message": "Tối đa"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "Không thấy token của mình?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "Tính năng 'Đồng bộ với tiện ích' tạm thời bị tắt. Nếu bạn muốn sử dụng ví tiện ích trên thiết bị di động MetaMask, thì trên ứng dụng di động: hãy quay lại các tùy chọn thiết lập ví và chọn phương án 'Nhập bằng Cụm Mật Khẩu Khôi Phục Bí Mật'. Sử dụng cụm mật khẩu bí mật của ví tiện ích để nhập ví của bạn vào thiết bị di động."
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "Sắp có thêm..."
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Số chỉ dùng một lần lớn hơn số chỉ dùng một lần gợi ý là $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "NFT"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "Không thể thêm NFT vì thông tin quyền sở hữu không trùng khớp. Đảm bảo bạn đã nhập đúng thông tin."
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "Hiển thị"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Quyền kiểm soát gas nâng cao"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Chọn tùy chọn này để hiển thị các quyền kiểm soát giá gas và giới hạn ngay trên màn hình gửi và xác nhận."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Hiển thị tỷ lệ quy đổi trên các mạng thử nghiệm"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "Bạn đang cấp quyền truy cập khóa $2 cho Snap \"$1\". Hành động này không thể hủy bỏ và sẽ cấp quyền kiểm soát tài khoản và tài sản $2 của bạn cho \"$1\". Đảm bảo bạn tin tưởng \"$1\" trước khi tiếp tục.",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "Snap này đang yêu cầu các quyền sau:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "Cập nhật Snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1 cần một phiên bản Snap mới hơn.",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snap"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "Đã xảy ra lỗi với $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Đang tải thông tin chi tiết về giao dịch..."
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Snap chỉ hoạt động khi đã bật"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Giao diện người dùng được chỉ định bởi snap không hợp lệ."
|
||||
"message": "Giao diện người dùng được chỉ định bởi snap không hợp lệ.",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Một số mạng có thể gây ra rủi ro về bảo mật và/hoặc quyền riêng tư. Bạn cần hiểu rõ các rủi ro này trước khi thêm và sử dụng mạng."
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Ký hiệu không được dài quá 11 ký tự."
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "Đồng bộ thất bại"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "Đang đồng bộ"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "Đồng bộ với thiết bị di động"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "Đảm bảo rằng không có ai nhìn vào màn hình của bạn khi quét mã này"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "Đã đồng bộ thành công dữ liệu của bạn. Tận hưởng ứng dụng MetaMask trên thiết bị di động!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "Bạn có thể đồng bộ tài khoản và thông tin của mình với thiết bị di động. Mở ứng dụng MetaMask trên thiết bị di động, chuyển đến phần \"Cài đặt\" và nhấn vào \"Đồng bộ với tiện ích trình duyệt\""
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "Nếu mới mở ứng dụng MetaMask trên thiết bị di động lần đầu tiên, bạn chỉ cần làm theo các bước hướng dẫn trên điện thoại."
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "Quét mã này bằng ứng dụng MetaMask trên thiết bị di động"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "Đồng bộ với thiết bị di động"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "Tăng 10%"
|
||||
},
|
||||
|
59
app/_locales/zh_CN/messages.json
generated
@ -1317,9 +1317,6 @@
|
||||
"etherscanViewOn": {
|
||||
"message": "在 Etherscan 上查看"
|
||||
},
|
||||
"expandExperience": {
|
||||
"message": "扩展MetaMask Snap的web3体验"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "展开视图"
|
||||
},
|
||||
@ -1952,9 +1949,6 @@
|
||||
"malformedData": {
|
||||
"message": "格式错误的数据"
|
||||
},
|
||||
"manageSnaps": {
|
||||
"message": "管理已安装的Snap"
|
||||
},
|
||||
"max": {
|
||||
"message": "最大"
|
||||
},
|
||||
@ -2027,9 +2021,6 @@
|
||||
"missingToken": {
|
||||
"message": "没有看到您的代币?"
|
||||
},
|
||||
"mobileSyncWarning": {
|
||||
"message": "“与扩展程序同步”功能暂时被禁用。如果您想要在 MetaMask 移动设备上使用您的扩展程序钱包,那么在您的移动应用程序上:返回钱包设置选项并选择“使用账户助记词导入”选项。使用您的扩展程序钱包的助记词来将您的钱包导入移动设备。"
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "更多即将到来……"
|
||||
},
|
||||
@ -2188,9 +2179,6 @@
|
||||
"message": "Nonce 高于建议的 nouce 值 $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nft": {
|
||||
"message": "非同质化代币(NFT)"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
"message": "由于所有权信息不匹配,无法添加NFT。请确保所输入的信息正确无误。"
|
||||
},
|
||||
@ -3253,12 +3241,6 @@
|
||||
"show": {
|
||||
"message": "显示"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "高级燃料控制"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "选择此项可直接在发送和确认界面显示燃料价格和上限控制。"
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "在测试网络上显示转换"
|
||||
},
|
||||
@ -3358,23 +3340,12 @@
|
||||
"message": "您正在向snap \"$1\"授予$2的密钥访问权限。此操作不可撤销,并会向\"$1\"授予对您的$2账户和资产的控制权。在继续之前,请确保您信任\"$1\"。",
|
||||
"description": "The first parameter is the name of the snap and the second one is the protocol"
|
||||
},
|
||||
"snapRequestsPermission": {
|
||||
"message": "此Snap正在请求以下权限:"
|
||||
},
|
||||
"snapUpdate": {
|
||||
"message": "更新Snap"
|
||||
},
|
||||
"snapUpdateExplanation": {
|
||||
"message": "$1需要更新版本的snap。",
|
||||
"description": "$1 is the dapp that is requesting an update to the snap."
|
||||
},
|
||||
"snaps": {
|
||||
"message": "Snap"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "$1 发生错误:$2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "正在加载交易洞察……"
|
||||
},
|
||||
@ -3391,7 +3362,8 @@
|
||||
"message": "Snap仅在启用后才会运行"
|
||||
},
|
||||
"snapsUIError": {
|
||||
"message": "Snap指定的用户界面无效。"
|
||||
"message": "Snap指定的用户界面无效。",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "某些网络可能会带来安全和/或隐私风险。在添加和使用网络之前,请先了解风险。"
|
||||
@ -3965,33 +3937,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "符号不得超过11个字符。"
|
||||
},
|
||||
"syncFailed": {
|
||||
"message": "同步失败"
|
||||
},
|
||||
"syncInProgress": {
|
||||
"message": "同步进行中"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "与移动设备同步"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "扫描此代码时,请确保附近没有其他人在看您的屏幕"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "您的数据已同步成功。尽情体验 MetaMask 移动应用程序吧!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "您可以将您的账户和信息与您的移动设备同步。打开 MetaMask 移动应用程序,进入“设置”,点击“从浏览器扩展程序同步”"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "如果您是首次启用 MetaMask 移动应用程序,请通过个人手机完成如下操作。"
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "使用 MetaMask 移动应用程序扫描此代码"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "与移动设备同步"
|
||||
},
|
||||
"tenPercentIncreased": {
|
||||
"message": "增加10%"
|
||||
},
|
||||
|
27
app/_locales/zh_TW/messages.json
generated
@ -1245,12 +1245,6 @@
|
||||
"settings": {
|
||||
"message": "設定"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "顯示進階 gas 控制選項"
|
||||
},
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "選擇此項會在傳送或確認畫面顯示可微調 gas 價格以及 gas 上限的功能"
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "在測試網上顯示匯率"
|
||||
},
|
||||
@ -1393,27 +1387,6 @@
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "符號不得超過 11 個字元。"
|
||||
},
|
||||
"syncWithMobile": {
|
||||
"message": "和行動裝置同步"
|
||||
},
|
||||
"syncWithMobileBeCareful": {
|
||||
"message": "掃描代碼時確保沒有其他人在看你的螢幕"
|
||||
},
|
||||
"syncWithMobileComplete": {
|
||||
"message": "你的資料已成功同步。開始活用 MetaMask 行動應用程式!"
|
||||
},
|
||||
"syncWithMobileDesc": {
|
||||
"message": "你可以用行動裝置同步帳戶與資訊。開啟 MetaMask 行動應用程式,至「設定」並點擊「自瀏覽器擴充功能同步」"
|
||||
},
|
||||
"syncWithMobileDescNewUsers": {
|
||||
"message": "如果您是第一次開啟 MetaMask 行動應用程式,只要跟著手機上的指示操作即可。"
|
||||
},
|
||||
"syncWithMobileScanThisCode": {
|
||||
"message": "用您的 MetaMask 行動應用程式 掃描此條碼"
|
||||
},
|
||||
"syncWithMobileTitle": {
|
||||
"message": "和行動裝置同步"
|
||||
},
|
||||
"terms": {
|
||||
"message": "使用條款"
|
||||
},
|
||||
|
@ -1,4 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M127.666 220C127.666 220 206.331 193.236 206.331 94.9394V59.5566C206.331 55.6784 203.315 52.5662 199.532 52.3268C185.264 51.6086 151.414 48.7359 127.666 36C103.965 48.7359 70.0669 51.6086 55.7989 52.2789C52.0164 52.5662 49 55.6784 49 59.5566V94.9394C49 193.236 127.666 220 127.666 220Z" fill="#00AEEF"/>
|
||||
<path d="M159.457 107.867C158.5 97.1419 149.307 93.4552 137.624 92.3539L137.672 77.3677L128.623 77.4156L128.671 91.923C126.277 91.923 123.739 91.923 121.298 91.9709V77.272L112.153 77.3199L112.201 92.2103C110.237 92.2582 93.9106 92.2582 93.9106 92.2582V101.786H105.497L105.45 149.665L95.2991 149.713L95.2034 160.63C95.2034 160.63 109.567 160.582 111.674 160.582L111.722 175.664L120.723 175.712L120.819 160.726C123.309 160.773 125.702 160.821 128.049 160.869L128.001 175.712H137.146L137.098 160.726C152.323 159.959 163.048 156.177 164.485 141.909C165.634 130.37 160.271 125.199 151.605 123.092C156.872 120.411 160.176 115.719 159.457 107.867ZM146.578 139.946C146.482 151.198 127.426 149.809 121.25 149.761L121.202 129.796C127.378 129.843 146.674 128.216 146.578 139.946ZM142.556 111.793C142.46 122.039 126.564 120.746 121.489 120.698L121.441 102.6C126.516 102.6 142.604 101.116 142.556 111.793Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1308.89 1311"><defs><style>.cls-1{fill:#1c46a0;}</style></defs><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path class="cls-1" d="M655.5,873.5c-120.47,0-218.12-97.72-218-218.22-.31-120,96.7-217.48,216.67-217.79h2.33c95.93.43,177.22,62.81,205.93,149.2a36.9,36.9,0,0,0,35,25.3h192a9,9,0,0,0,9-9V553.46a4,4,0,0,1,4-4h46.78a4,4,0,0,1,4,4V603a9,9,0,0,0,9,9h27.36a9,9,0,0,0,9-9V553.46a4,4,0,0,1,4-4h46.78a4,4,0,0,1,4,4V603a9,9,0,0,0,9,9h37.5a9,9,0,0,0,9-9c0-.25,0-.51,0-.76C1281.78,265.28,999.83.21,655.91,0,294-.22,0,293.61,0,655.51S293.48,1311,655.5,1311c344.1,0,626.27-265.14,653.36-602.26a9,9,0,0,0-8.23-9.71c-.26,0-.51,0-.77,0H897.45a37.08,37.08,0,0,0-35.11,25.53C833.46,811.1,751.77,873.5,655.5,873.5Z"/></g></g></svg>
|
Before Width: | Height: | Size: 805 B |
@ -1,40 +0,0 @@
|
||||
<svg width="426" height="149" viewBox="0 0 426 149" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M52.5095 1.29006C43.9873 3.00709 35.8923 6.40352 28.6963 11.2813C18.1341 18.4704 9.8993 28.5855 5.00166 40.3863C3.29958 44.4987 2.02478 48.7753 1.19735 53.1485C-0.416047 61.4971 -0.382513 70.0805 1.29607 78.4162C3.02032 86.9352 6.41862 95.0271 11.2939 102.223C18.4808 112.795 28.5956 121.041 40.3989 125.95C48.441 129.265 57.0602 130.954 65.7588 130.92L65.5021 65.4631L65.2059 0C60.9419 0.0171994 56.6897 0.449264 52.5095 1.29006Z" fill="#3E125B"/>
|
||||
<path d="M11.2949 102.229C13.6717 105.733 16.3806 109 19.384 111.984L65.457 65.4894L11.2949 102.229Z" fill="#974AC5"/>
|
||||
<path d="M65.5036 65.4631L10.9598 29.2629C8.62988 32.7795 6.64606 36.5135 5.03613 40.4126L65.5036 65.4631Z" fill="#682E8B"/>
|
||||
<path d="M65.5029 65.4631L4.94982 40.3994C3.24775 44.5119 1.97294 48.7884 1.14551 53.1616L65.5029 65.4631Z" fill="#6F3294"/>
|
||||
<path d="M65.5038 65.4631L19.003 19.3901C16.0098 22.4139 13.3162 25.7203 10.96 29.2629L65.5038 65.4631Z" fill="#612982"/>
|
||||
<path d="M65.5038 65.4631L1.29785 78.4162C2.13137 82.5544 3.36581 86.6016 4.98369 90.5005L65.5038 65.4631Z" fill="#8841B2"/>
|
||||
<path d="M28.6977 11.2813C25.2025 13.6611 21.9448 16.3722 18.9697 19.377L65.4705 65.45L28.6977 11.2813Z" fill="#592479"/>
|
||||
<path d="M65.5029 65.4631L0.0332031 65.7527C0.0615207 70.0087 0.502373 74.2519 1.34957 78.4228L65.5029 65.4631Z" fill="#813CA9"/>
|
||||
<path d="M1.17186 53.1616C0.380059 57.3116 -0.0122564 61.5279 0.000291701 65.7527L65.47 65.4631L1.17186 53.1616Z" fill="#76379E"/>
|
||||
<path d="M65.5033 65.4631L40.4528 4.97589C36.34 6.67669 32.4121 8.7939 28.7305 11.2945L65.5033 65.4631Z" fill="#511E6F"/>
|
||||
<path d="M65.5026 65.4631L65.2065 0C60.9526 0.0279982 56.7116 0.468856 52.543 1.31637L65.5026 65.4631Z" fill="#3E125B"/>
|
||||
<path d="M52.5098 1.2901C48.3693 2.12268 44.3198 3.35714 40.4189 4.97594L65.5024 65.4632L52.5098 1.2901Z" fill="#481965"/>
|
||||
<path d="M65.5036 65.4631L19.4307 111.957C22.4526 114.955 25.7591 117.65 29.3034 120.007L65.5036 65.4631Z" fill="#9E4DCF"/>
|
||||
<path d="M65.5029 65.4631L29.3027 120.007C32.8193 122.333 36.5507 124.317 40.4458 125.931L65.5029 65.4631Z" fill="#A851D9"/>
|
||||
<path d="M65.5029 65.4631L5.01562 90.5071C6.71387 94.6198 8.82891 98.5477 11.3276 102.229L65.5029 65.4631Z" fill="#9045BC"/>
|
||||
<path d="M65.5029 65.4631L40.4326 125.957C44.5354 127.653 48.8006 128.925 53.1619 129.755L65.5029 65.4631Z" fill="#B056E3"/>
|
||||
<path d="M53.1611 129.755C57.3131 130.548 61.5319 130.938 65.7588 130.92L65.5021 65.4631L53.1611 129.755Z" fill="#B155E5"/>
|
||||
<path d="M144.077 70.5772C142.352 62.0582 138.954 53.9663 134.079 46.7707C126.897 36.2016 116.79 27.9559 104.994 23.0431C96.9516 19.7279 88.3323 18.039 79.6338 18.0738L79.9234 83.5369L80.2196 149C84.4734 148.972 88.7144 148.531 92.8831 147.684C101.406 145.963 109.501 142.562 116.696 137.679C127.258 130.49 135.493 120.375 140.391 108.574C142.093 104.462 143.368 100.185 144.195 95.812C145.799 87.4639 145.757 78.8828 144.07 70.5509L144.077 70.5772Z" fill="#17125B"/>
|
||||
<path d="M134.079 46.7706C131.701 43.276 128.992 40.0184 125.99 37.0427L79.917 83.5369L134.079 46.7706Z" fill="#534AC5"/>
|
||||
<path d="M79.9043 83.5369L134.448 119.737C136.778 116.221 138.762 112.487 140.372 108.587L79.8779 83.5237L79.9043 83.5369Z" fill="#342E8B"/>
|
||||
<path d="M79.9033 83.5369L140.397 108.601C142.099 104.488 143.374 100.212 144.201 95.8384L79.9033 83.5369Z" fill="#393294"/>
|
||||
<path d="M79.9033 83.5369L126.404 129.61C129.397 126.586 132.091 123.28 134.447 119.737L79.9033 83.5369Z" fill="#302982"/>
|
||||
<path d="M79.9033 83.5369L144.076 70.5772C143.243 66.439 142.008 62.3918 140.391 58.4929L79.9033 83.504V83.5369Z" fill="#4A41B2"/>
|
||||
<path d="M116.676 137.706C120.171 135.33 123.428 132.623 126.404 129.623L79.9033 83.55L116.676 137.706Z" fill="#2B2479"/>
|
||||
<path d="M79.9045 83.5369L145.374 83.2473C145.346 78.9913 144.905 74.7481 144.058 70.5772L79.8848 83.5369H79.9045Z" fill="#453CA9"/>
|
||||
<path d="M144.201 95.8383C144.993 91.6884 145.386 87.4721 145.373 83.2473L79.9033 83.5369L144.201 95.8383Z" fill="#3E379E"/>
|
||||
<path d="M79.9033 83.5369L104.954 144.024C109.067 142.323 112.994 140.206 116.676 137.706L79.9033 83.5369Z" fill="#241E6F"/>
|
||||
<path d="M79.9033 83.5369L80.1995 149C84.4533 148.972 88.6944 148.531 92.863 147.684L79.9033 83.5106V83.5369Z" fill="#17125B"/>
|
||||
<path d="M92.863 147.71C97.0035 146.877 101.053 145.643 104.954 144.024L79.9033 83.5369L92.863 147.71Z" fill="#1E1965"/>
|
||||
<path d="M79.9033 83.5369L125.976 37.0427C122.954 34.0455 119.648 31.3496 116.104 28.9931L79.9033 83.5369Z" fill="#564DCF"/>
|
||||
<path d="M79.9038 83.5369L116.104 28.9931C112.587 26.6671 108.856 24.6834 104.961 23.0695L79.8906 83.5633L79.9038 83.5369Z" fill="#5D51D9"/>
|
||||
<path d="M79.9033 83.5368L140.391 58.5258C138.692 54.4131 136.577 50.4852 134.079 46.8035L79.9033 83.5698V83.5368Z" fill="#4E45BC"/>
|
||||
<path d="M79.9033 83.5369L104.993 23.0431C100.88 21.3428 96.6039 20.068 92.2311 19.2388L79.9231 83.5369H79.9033Z" fill="#6156E3"/>
|
||||
<path d="M92.2129 19.2388C88.061 18.4459 83.8422 18.0558 79.6152 18.0738L79.9048 83.5369L92.2129 19.2388Z" fill="#6055E5"/>
|
||||
<path d="M236.239 53.1287L229.052 57.6438C222.786 49.4034 214.267 45.3051 203.495 45.3489C194.886 45.3884 187.749 48.1901 182.084 53.754C179.347 56.3511 177.183 59.4911 175.73 62.9729C174.277 66.4546 173.567 70.2014 173.646 73.9734C173.655 79.0806 175.032 84.092 177.635 88.4864C180.184 92.9382 183.931 96.5864 188.448 99.0174C193.166 101.537 198.45 102.811 203.797 102.716C214.184 102.673 222.646 98.504 229.184 90.2109L236.424 94.9234C233.06 100.028 228.383 104.132 222.885 106.804C217.18 109.634 210.7 111.064 203.442 111.095C192.275 111.148 182.992 107.637 175.594 100.564C168.196 93.4908 164.466 84.8664 164.405 74.6908C164.342 67.986 166.098 61.3894 169.486 55.6035C172.831 49.7784 177.725 44.9942 183.624 41.7816C189.813 38.4238 196.756 36.6999 203.797 36.7728C208.397 36.7424 212.972 37.4468 217.349 38.8592C221.33 40.081 225.081 41.9512 228.453 44.3946C231.589 46.775 234.233 49.7412 236.239 53.1287Z" fill="#1C1C1C"/>
|
||||
<path d="M244.499 38.4116L253.595 38.3722L253.74 71.3736C253.775 79.4297 254.234 84.9717 255.116 87.9993C256.332 92.1854 258.986 95.8077 262.612 98.2275C266.285 100.72 270.66 101.957 275.736 101.94C280.813 101.922 285.111 100.678 288.63 98.2078C292.071 95.8806 294.643 92.4778 295.943 88.5325C296.82 85.8032 297.259 80.0199 297.259 71.1827L297.114 38.1813L306.401 38.1352L306.553 72.8282C306.601 82.5693 305.504 89.9146 303.262 94.8642C301.117 99.6901 297.557 103.75 293.053 106.507C288.494 109.316 282.773 110.737 275.888 110.773C269.003 110.808 263.246 109.436 258.617 106.659C254.042 103.938 250.417 99.8723 248.238 95.0156C245.943 90.0178 244.773 82.4969 244.729 72.453L244.499 38.4116Z" fill="#1C1C1C"/>
|
||||
<path d="M316.966 38.0892L326.253 38.0497L326.299 48.3766C329.05 44.2958 331.959 41.2397 335.026 39.2081C337.859 37.2557 341.209 36.1903 344.649 36.1475C347.496 36.2288 350.274 37.0451 352.712 38.517L348.006 46.2046C346.497 45.4875 344.863 45.0737 343.194 44.9869C340.162 44.9869 337.245 46.2463 334.441 48.7649C331.637 51.2836 329.511 55.1581 328.063 60.3885C326.953 64.421 326.426 72.5649 326.483 84.8203L326.588 108.726L317.295 108.772L316.966 38.0892Z" fill="#1C1C1C"/>
|
||||
<path d="M359.057 37.9048L368.732 37.8588L392.696 89.2762L415.904 37.6481L425.645 37.6086L393.605 108.429H391.913L359.057 37.9048Z" fill="#1C1C1C"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 5.9 KiB |
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 247.16 47.67" style="enable-background:new 0 0 247.16 47.67;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#C2A882;}
|
||||
.st1{fill:#00292B;}
|
||||
.st2{fill:#00FFD4;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st1" d="M62.59,11.28l7.63,17.12h-15.3L62.59,11.28z M60.08,3.45L41.64,44.22h6.24l4.61-10.29h20.16l4.56,10.29h6.46
|
||||
L65.22,3.45H60.08z"/>
|
||||
<g>
|
||||
<path class="st1" d="M10.4,9.36h11.22c5.82,0,9.16,1.89,9.16,6.31c0,4.42-3.62,6.77-9.22,6.77H10.4V9.36z M36.81,15.67
|
||||
c0-7.54-5.83-11.94-14.86-11.94H4.43v40.5h5.97v-16.2h11.55C30.99,28.02,36.81,23.27,36.81,15.67"/>
|
||||
<path class="st1" d="M101.22,22.44V9.36h11.22c5.82,0,9.16,1.9,9.16,6.31c0,4.42-3.62,6.77-9.22,6.77H101.22z M116.54,27.71
|
||||
c6.85-1.16,11.09-5.56,11.09-12.04c0-7.54-5.83-11.94-14.85-11.94H95.25v40.5h5.97v-16.2h8.13l11.61,16.2h7.37L116.54,27.71z"/>
|
||||
</g>
|
||||
<polygon class="st0" points="144.19,3.73 144.19,44.22 150.16,44.22 150.16,28.02 168.37,28.02 168.37,22.44 150.16,22.44
|
||||
150.16,9.36 173.74,9.36 173.74,3.73 "/>
|
||||
<rect x="187.22" y="3.73" class="st0" width="5.97" height="40.5"/>
|
||||
<polygon class="st0" points="236.87,3.73 236.87,33.43 213.52,3.73 208.23,3.73 208.23,44.22 214.09,44.22 214.09,13.8
|
||||
238.05,44.22 242.72,44.22 242.72,3.73 "/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,26 +0,0 @@
|
||||
<svg width="168" height="168" viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="168" height="169">
|
||||
<path d="M84 0C130.392 0 168 37.6081 168 84C168 105.195 160.15 124.557 147.199 139.336L151.846 145.077C156.735 151.117 155.803 159.977 149.763 164.866C143.939 169.58 135.493 168.882 130.513 163.41L129.974 162.783L125.384 157.115C113.169 164.044 99.0465 168 84 168C37.6081 168 0 130.392 0 84C0 37.6081 37.6081 0 84 0ZM84 28.14C53.1494 28.14 28.14 53.1494 28.14 84C28.14 114.851 53.1494 139.86 84 139.86C92.32 139.86 100.215 138.041 107.309 134.779L101.414 127.503C96.5249 121.463 97.4575 112.603 103.497 107.714C109.321 102.999 117.767 103.698 122.747 109.17L123.286 109.797L129.094 116.974C135.863 107.733 139.86 96.3331 139.86 84C139.86 53.1494 114.851 28.14 84 28.14Z" fill="#242424"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M168 67.2L0 100.8V168H168V67.2Z" fill="url(#paint0_linear)"/>
|
||||
<ellipse cx="14.7" cy="94.5" rx="14.7" ry="14.7" fill="#FCE001"/>
|
||||
<path d="M-9.96887 -9.94773L176.881 -9.94771L176.881 64.7921L-9.96886 102.162L-9.96887 -9.94773Z" fill="url(#paint1_linear)"/>
|
||||
<path d="M168 73.5C168 81.6186 161.419 88.2 153.3 88.2C145.182 88.2 138.6 81.6186 138.6 73.5C138.6 65.3815 145.182 58.8 153.3 58.8C161.419 58.8 168 65.3815 168 73.5Z" fill="#FF39E8"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="150" y1="84" x2="6.5" y2="118" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF38E7"/>
|
||||
<stop offset="1" stop-color="#00C9FA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="-23.0611" y1="66.1912" x2="180" y2="7" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.25" stop-color="#FCE000"/>
|
||||
<stop offset="1" stop-color="#00DFA1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="168" height="168" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB |
3
app/images/icons/ethereum.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 417 417">
|
||||
<path d="m80.5 212.3 64 37.8 64 37.9 127.9-75.7L208.5 0l-128 212.3z"/><path d="m336.5 236.6-128 75.6-128-75.6 128 180.3z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 196 B |
@ -127,10 +127,6 @@ chrome.runtime.onMessage.addListener(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
globalThis.isFirstTimeProfileLoaded = true;
|
||||
});
|
||||
|
||||
/*
|
||||
* This content script is injected programmatically because
|
||||
* MAIN world injection does not work properly via manifest
|
||||
|
@ -24,11 +24,11 @@ import {
|
||||
} from '../../shared/constants/app';
|
||||
import { SECOND } from '../../shared/constants/time';
|
||||
import {
|
||||
REJECT_NOTFICIATION_CLOSE,
|
||||
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||
EVENT,
|
||||
EVENT_NAMES,
|
||||
TRAITS,
|
||||
REJECT_NOTIFICATION_CLOSE,
|
||||
REJECT_NOTIFICATION_CLOSE_SIG,
|
||||
MetaMetricsEventCategory,
|
||||
MetaMetricsEventName,
|
||||
MetaMetricsUserTrait,
|
||||
} from '../../shared/constants/metametrics';
|
||||
import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils';
|
||||
import { isManifestV3 } from '../../shared/modules/mv3.utils';
|
||||
@ -223,7 +223,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
|
||||
* @property {object} provider - The current selected network provider.
|
||||
* @property {string} provider.rpcUrl - The address for the RPC API, if using an RPC API.
|
||||
* @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks.
|
||||
* @property {string} network - A stringified number of the current network ID.
|
||||
* @property {string} networkId - The stringified number of the current network ID.
|
||||
* @property {string} networkStatus - Either "unknown", "available", "unavailable", or "blocked", depending on the status of the currently selected network.
|
||||
* @property {object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values.
|
||||
* @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string.
|
||||
* @property {TransactionMeta[]} currentNetworkTxList - An array of transactions associated with the currently selected network.
|
||||
@ -267,7 +268,23 @@ async function initialize() {
|
||||
await DesktopManager.init(platform.getVersion());
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
|
||||
setupController(initState, initLangCode);
|
||||
let isFirstMetaMaskControllerSetup;
|
||||
if (isManifestV3) {
|
||||
const sessionData = await browser.storage.session.get([
|
||||
'isFirstMetaMaskControllerSetup',
|
||||
]);
|
||||
|
||||
isFirstMetaMaskControllerSetup =
|
||||
sessionData?.isFirstMetaMaskControllerSetup === undefined;
|
||||
await browser.storage.session.set({ isFirstMetaMaskControllerSetup });
|
||||
}
|
||||
|
||||
setupController(
|
||||
initState,
|
||||
initLangCode,
|
||||
{},
|
||||
isFirstMetaMaskControllerSetup,
|
||||
);
|
||||
if (!isManifestV3) {
|
||||
await loadPhishingWarningPage();
|
||||
}
|
||||
@ -409,8 +426,14 @@ export async function loadStateFromPersistence() {
|
||||
* @param {object} initState - The initial state to start the controller with, matches the state that is emitted from the controller.
|
||||
* @param {string} initLangCode - The region code for the language preferred by the current user.
|
||||
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
|
||||
* @param isFirstMetaMaskControllerSetup
|
||||
*/
|
||||
export function setupController(initState, initLangCode, overrides) {
|
||||
export function setupController(
|
||||
initState,
|
||||
initLangCode,
|
||||
overrides,
|
||||
isFirstMetaMaskControllerSetup,
|
||||
) {
|
||||
//
|
||||
// MetaMask Controller
|
||||
//
|
||||
@ -436,6 +459,7 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
},
|
||||
localStore,
|
||||
overrides,
|
||||
isFirstMetaMaskControllerSetup,
|
||||
});
|
||||
|
||||
setupEnsIpfsResolver({
|
||||
@ -555,6 +579,10 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
if (message.name === WORKER_KEEP_ALIVE_MESSAGE) {
|
||||
// To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI.
|
||||
remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE });
|
||||
|
||||
controller.appStateController.setServiceWorkerLastActiveTime(
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -656,14 +684,6 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
controller.messageManager.on(
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
controller.personalMessageManager.on(
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
controller.decryptMessageManager.on(
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
@ -672,7 +692,7 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
controller.typedMessageManager.on(
|
||||
controller.signController.hub.on(
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
@ -708,23 +728,17 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
|
||||
function getUnapprovedTransactionCount() {
|
||||
const unapprovedTxCount = controller.txController.getUnapprovedTxCount();
|
||||
const { unapprovedMsgCount } = controller.messageManager;
|
||||
const { unapprovedPersonalMsgCount } = controller.personalMessageManager;
|
||||
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager;
|
||||
const { unapprovedEncryptionPublicKeyMsgCount } =
|
||||
controller.encryptionPublicKeyManager;
|
||||
const { unapprovedTypedMessagesCount } = controller.typedMessageManager;
|
||||
const pendingApprovalCount =
|
||||
controller.approvalController.getTotalApprovalCount();
|
||||
const waitingForUnlockCount =
|
||||
controller.appStateController.waitingForUnlock.length;
|
||||
return (
|
||||
unapprovedTxCount +
|
||||
unapprovedMsgCount +
|
||||
unapprovedPersonalMsgCount +
|
||||
unapprovedDecryptMsgCount +
|
||||
unapprovedEncryptionPublicKeyMsgCount +
|
||||
unapprovedTypedMessagesCount +
|
||||
pendingApprovalCount +
|
||||
waitingForUnlockCount
|
||||
);
|
||||
@ -747,36 +761,13 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
).forEach((txId) =>
|
||||
controller.txController.txStateManager.setTxStatusRejected(txId),
|
||||
);
|
||||
controller.messageManager.messages
|
||||
.filter((msg) => msg.status === 'unapproved')
|
||||
.forEach((tx) =>
|
||||
controller.messageManager.rejectMsg(
|
||||
tx.id,
|
||||
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||
),
|
||||
);
|
||||
controller.personalMessageManager.messages
|
||||
.filter((msg) => msg.status === 'unapproved')
|
||||
.forEach((tx) =>
|
||||
controller.personalMessageManager.rejectMsg(
|
||||
tx.id,
|
||||
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||
),
|
||||
);
|
||||
controller.typedMessageManager.messages
|
||||
.filter((msg) => msg.status === 'unapproved')
|
||||
.forEach((tx) =>
|
||||
controller.typedMessageManager.rejectMsg(
|
||||
tx.id,
|
||||
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||
),
|
||||
);
|
||||
controller.signController.rejectUnapproved(REJECT_NOTIFICATION_CLOSE_SIG);
|
||||
controller.decryptMessageManager.messages
|
||||
.filter((msg) => msg.status === 'unapproved')
|
||||
.forEach((tx) =>
|
||||
controller.decryptMessageManager.rejectMsg(
|
||||
tx.id,
|
||||
REJECT_NOTFICIATION_CLOSE,
|
||||
REJECT_NOTIFICATION_CLOSE,
|
||||
),
|
||||
);
|
||||
controller.encryptionPublicKeyManager.messages
|
||||
@ -784,7 +775,7 @@ export function setupController(initState, initLangCode, overrides) {
|
||||
.forEach((tx) =>
|
||||
controller.encryptionPublicKeyManager.rejectMsg(
|
||||
tx.id,
|
||||
REJECT_NOTFICIATION_CLOSE,
|
||||
REJECT_NOTIFICATION_CLOSE,
|
||||
),
|
||||
);
|
||||
|
||||
@ -880,11 +871,13 @@ async function openPopup() {
|
||||
const addAppInstalledEvent = () => {
|
||||
if (controller) {
|
||||
controller.metaMetricsController.updateTraits({
|
||||
[TRAITS.INSTALL_DATE_EXT]: new Date().toISOString().split('T')[0], // yyyy-mm-dd
|
||||
[MetaMetricsUserTrait.InstallDateExt]: new Date()
|
||||
.toISOString()
|
||||
.split('T')[0], // yyyy-mm-dd
|
||||
});
|
||||
controller.metaMetricsController.addEventBeforeMetricsOptIn({
|
||||
category: EVENT.CATEGORIES.APP,
|
||||
event: EVENT_NAMES.APP_INSTALLED,
|
||||
category: MetaMetricsEventCategory.App,
|
||||
event: MetaMetricsEventName.AppInstalled,
|
||||
properties: {},
|
||||
});
|
||||
return;
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,10 @@ import {
|
||||
AssetType,
|
||||
TokenStandard,
|
||||
} from '../../../shared/constants/transaction';
|
||||
import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics';
|
||||
import {
|
||||
MetaMetricsEventCategory,
|
||||
MetaMetricsEventName,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
|
||||
// By default, poll every 3 minutes
|
||||
const DEFAULT_INTERVAL = MINUTE * 3;
|
||||
@ -167,8 +170,8 @@ export default class DetectTokensController {
|
||||
|
||||
if (tokensWithBalance.length > 0) {
|
||||
this._trackMetaMetricsEvent({
|
||||
event: EVENT_NAMES.TOKEN_DETECTED,
|
||||
category: EVENT.CATEGORIES.WALLET,
|
||||
event: MetaMetricsEventName.TokenDetected,
|
||||
category: MetaMetricsEventCategory.Wallet,
|
||||
properties: {
|
||||
tokens: eventTokensDetails,
|
||||
token_standard: TokenStandard.ERC20,
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -15,7 +15,7 @@ import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
|
||||
import {
|
||||
METAMETRICS_ANONYMOUS_ID,
|
||||
METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
TRAITS,
|
||||
MetaMetricsUserTrait,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import { isManifestV3 } from '../../../shared/modules/mv3.utils';
|
||||
@ -692,38 +692,44 @@ export default class MetaMetricsController {
|
||||
const { traits, previousUserTraits } = this.store.getState();
|
||||
/** @type {MetaMetricsTraits} */
|
||||
const currentTraits = {
|
||||
[TRAITS.ADDRESS_BOOK_ENTRIES]: sum(
|
||||
[MetaMetricsUserTrait.AddressBookEntries]: sum(
|
||||
Object.values(metamaskState.addressBook).map(size),
|
||||
),
|
||||
[TRAITS.INSTALL_DATE_EXT]: traits[TRAITS.INSTALL_DATE_EXT] || '',
|
||||
[TRAITS.LEDGER_CONNECTION_TYPE]: metamaskState.ledgerTransportType,
|
||||
[TRAITS.NETWORKS_ADDED]: Object.values(
|
||||
[MetaMetricsUserTrait.InstallDateExt]:
|
||||
traits[MetaMetricsUserTrait.InstallDateExt] || '',
|
||||
[MetaMetricsUserTrait.LedgerConnectionType]:
|
||||
metamaskState.ledgerTransportType,
|
||||
[MetaMetricsUserTrait.NetworksAdded]: Object.values(
|
||||
metamaskState.networkConfigurations,
|
||||
).map((networkConfiguration) => networkConfiguration.chainId),
|
||||
[TRAITS.NETWORKS_WITHOUT_TICKER]: Object.values(
|
||||
[MetaMetricsUserTrait.NetworksWithoutTicker]: Object.values(
|
||||
metamaskState.networkConfigurations,
|
||||
)
|
||||
.filter(({ ticker }) => !ticker)
|
||||
.map(({ chainId }) => chainId),
|
||||
[TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useNftDetection,
|
||||
[TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities)
|
||||
.length,
|
||||
[TRAITS.NUMBER_OF_NFT_COLLECTIONS]: this._getAllUniqueNFTAddressesLength(
|
||||
[MetaMetricsUserTrait.NftAutodetectionEnabled]:
|
||||
metamaskState.useNftDetection,
|
||||
[MetaMetricsUserTrait.NumberOfAccounts]: Object.values(
|
||||
metamaskState.identities,
|
||||
).length,
|
||||
[MetaMetricsUserTrait.NumberOfNftCollections]:
|
||||
this._getAllUniqueNFTAddressesLength(metamaskState.allNfts),
|
||||
[MetaMetricsUserTrait.NumberOfNfts]: this._getAllNFTsFlattened(
|
||||
metamaskState.allNfts,
|
||||
),
|
||||
[TRAITS.NUMBER_OF_NFTS]: this._getAllNFTsFlattened(metamaskState.allNfts)
|
||||
.length,
|
||||
[TRAITS.NUMBER_OF_TOKENS]: this._getNumberOfTokens(metamaskState),
|
||||
[TRAITS.OPENSEA_API_ENABLED]: metamaskState.openSeaEnabled,
|
||||
[TRAITS.THREE_BOX_ENABLED]: false, // deprecated, hard-coded as false
|
||||
[TRAITS.THEME]: metamaskState.theme || 'default',
|
||||
[TRAITS.TOKEN_DETECTION_ENABLED]: metamaskState.useTokenDetection,
|
||||
).length,
|
||||
[MetaMetricsUserTrait.NumberOfTokens]:
|
||||
this._getNumberOfTokens(metamaskState),
|
||||
[MetaMetricsUserTrait.OpenseaApiEnabled]: metamaskState.openSeaEnabled,
|
||||
[MetaMetricsUserTrait.ThreeBoxEnabled]: false, // deprecated, hard-coded as false
|
||||
[MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default',
|
||||
[MetaMetricsUserTrait.TokenDetectionEnabled]:
|
||||
metamaskState.useTokenDetection,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
[TRAITS.DESKTOP_ENABLED]: metamaskState.desktopEnabled || false,
|
||||
[MetaMetricsUserTrait.DesktopEnabled]:
|
||||
metamaskState.desktopEnabled || false,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
[TRAITS.SECURITY_PROVIDERS]: metamaskState.transactionSecurityCheckEnabled
|
||||
? ['opensea']
|
||||
: [],
|
||||
[MetaMetricsUserTrait.SecurityProviders]:
|
||||
metamaskState.transactionSecurityCheckEnabled ? ['opensea'] : [],
|
||||
};
|
||||
|
||||
if (!previousUserTraits) {
|
||||
|
@ -5,7 +5,7 @@ import { createSegmentMock } from '../lib/segment';
|
||||
import {
|
||||
METAMETRICS_ANONYMOUS_ID,
|
||||
METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
TRAITS,
|
||||
MetaMetricsUserTrait,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import waitUntilCalled from '../../../test/lib/wait-until-called';
|
||||
import {
|
||||
@ -15,7 +15,6 @@ import {
|
||||
} from '../../../shared/constants/network';
|
||||
import * as Utils from '../lib/util';
|
||||
import MetaMetricsController from './metametrics';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
const segment = createSegmentMock(2, 10000);
|
||||
|
||||
@ -72,17 +71,17 @@ function getMockNetworkController() {
|
||||
},
|
||||
network: 'loading',
|
||||
};
|
||||
const on = sinon.stub().withArgs(NETWORK_EVENTS.NETWORK_DID_CHANGE);
|
||||
const onNetworkDidChange = sinon.stub();
|
||||
const updateState = (newState) => {
|
||||
state = { ...state, ...newState };
|
||||
on.getCall(0).args[1]();
|
||||
onNetworkDidChange.getCall(0).args[0]();
|
||||
};
|
||||
return {
|
||||
store: {
|
||||
getState: () => state,
|
||||
updateState,
|
||||
},
|
||||
on,
|
||||
onNetworkDidChange,
|
||||
};
|
||||
}
|
||||
|
||||
@ -136,10 +135,8 @@ function getMetaMetricsController({
|
||||
segment: segmentInstance || segment,
|
||||
getCurrentChainId: () =>
|
||||
networkController.store.getState().provider.chainId,
|
||||
onNetworkDidChange: networkController.on.bind(
|
||||
networkController,
|
||||
NETWORK_EVENTS.NETWORK_DID_CHANGE,
|
||||
),
|
||||
onNetworkDidChange:
|
||||
networkController.onNetworkDidChange.bind(networkController),
|
||||
preferencesStore,
|
||||
version: '0.0.1',
|
||||
environment: 'test',
|
||||
@ -956,22 +953,26 @@ describe('MetaMetricsController', function () {
|
||||
});
|
||||
|
||||
assert.deepEqual(traits, {
|
||||
[TRAITS.ADDRESS_BOOK_ENTRIES]: 3,
|
||||
[TRAITS.INSTALL_DATE_EXT]: '',
|
||||
[TRAITS.LEDGER_CONNECTION_TYPE]: 'web-hid',
|
||||
[TRAITS.NETWORKS_ADDED]: [CHAIN_IDS.MAINNET, CHAIN_IDS.GOERLI, '0xaf'],
|
||||
[TRAITS.NETWORKS_WITHOUT_TICKER]: ['0xaf'],
|
||||
[TRAITS.NFT_AUTODETECTION_ENABLED]: false,
|
||||
[TRAITS.NUMBER_OF_ACCOUNTS]: 2,
|
||||
[TRAITS.NUMBER_OF_NFT_COLLECTIONS]: 3,
|
||||
[TRAITS.NUMBER_OF_NFTS]: 4,
|
||||
[TRAITS.NUMBER_OF_TOKENS]: 5,
|
||||
[TRAITS.OPENSEA_API_ENABLED]: true,
|
||||
[TRAITS.THREE_BOX_ENABLED]: false,
|
||||
[TRAITS.THEME]: 'default',
|
||||
[TRAITS.TOKEN_DETECTION_ENABLED]: true,
|
||||
[TRAITS.DESKTOP_ENABLED]: false,
|
||||
[TRAITS.SECURITY_PROVIDERS]: [],
|
||||
[MetaMetricsUserTrait.AddressBookEntries]: 3,
|
||||
[MetaMetricsUserTrait.InstallDateExt]: '',
|
||||
[MetaMetricsUserTrait.LedgerConnectionType]: 'web-hid',
|
||||
[MetaMetricsUserTrait.NetworksAdded]: [
|
||||
CHAIN_IDS.MAINNET,
|
||||
CHAIN_IDS.GOERLI,
|
||||
'0xaf',
|
||||
],
|
||||
[MetaMetricsUserTrait.NetworksWithoutTicker]: ['0xaf'],
|
||||
[MetaMetricsUserTrait.NftAutodetectionEnabled]: false,
|
||||
[MetaMetricsUserTrait.NumberOfAccounts]: 2,
|
||||
[MetaMetricsUserTrait.NumberOfNftCollections]: 3,
|
||||
[MetaMetricsUserTrait.NumberOfNfts]: 4,
|
||||
[MetaMetricsUserTrait.NumberOfTokens]: 5,
|
||||
[MetaMetricsUserTrait.OpenseaApiEnabled]: true,
|
||||
[MetaMetricsUserTrait.ThreeBoxEnabled]: false,
|
||||
[MetaMetricsUserTrait.Theme]: 'default',
|
||||
[MetaMetricsUserTrait.TokenDetectionEnabled]: true,
|
||||
[MetaMetricsUserTrait.DesktopEnabled]: false,
|
||||
[MetaMetricsUserTrait.SecurityProviders]: [],
|
||||
});
|
||||
});
|
||||
|
||||
@ -1018,10 +1019,10 @@ describe('MetaMetricsController', function () {
|
||||
});
|
||||
|
||||
assert.deepEqual(updatedTraits, {
|
||||
[TRAITS.ADDRESS_BOOK_ENTRIES]: 4,
|
||||
[TRAITS.NUMBER_OF_ACCOUNTS]: 3,
|
||||
[TRAITS.NUMBER_OF_TOKENS]: 1,
|
||||
[TRAITS.OPENSEA_API_ENABLED]: false,
|
||||
[MetaMetricsUserTrait.AddressBookEntries]: 4,
|
||||
[MetaMetricsUserTrait.NumberOfAccounts]: 3,
|
||||
[MetaMetricsUserTrait.NumberOfTokens]: 1,
|
||||
[MetaMetricsUserTrait.OpenseaApiEnabled]: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { NetworkClientType } from './create-network-client';
|
||||
import { testsForProviderType } from './provider-api-tests/shared-tests';
|
||||
|
||||
describe('createNetworkClient', () => {
|
||||
testsForProviderType(NetworkClientType.Infura);
|
||||
testsForProviderType(NetworkClientType.Custom);
|
||||
});
|
191
app/scripts/controllers/network/create-network-client.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import {
|
||||
createAsyncMiddleware,
|
||||
createScaffoldMiddleware,
|
||||
JsonRpcEngine,
|
||||
mergeMiddleware,
|
||||
JsonRpcMiddleware,
|
||||
} from 'json-rpc-engine';
|
||||
import {
|
||||
createBlockCacheMiddleware,
|
||||
createBlockRefMiddleware,
|
||||
createBlockRefRewriteMiddleware,
|
||||
createBlockTrackerInspectorMiddleware,
|
||||
createInflightCacheMiddleware,
|
||||
createFetchMiddleware,
|
||||
createRetryOnEmptyMiddleware,
|
||||
} from '@metamask/eth-json-rpc-middleware';
|
||||
import {
|
||||
providerFromEngine,
|
||||
providerFromMiddleware,
|
||||
SafeEventEmitterProvider,
|
||||
} from '@metamask/eth-json-rpc-provider';
|
||||
import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura';
|
||||
import type { Hex } from '@metamask/utils/dist';
|
||||
import { PollingBlockTracker } from 'eth-block-tracker/dist';
|
||||
import { SECOND } from '../../../../shared/constants/time';
|
||||
import {
|
||||
BUILT_IN_INFURA_NETWORKS,
|
||||
BuiltInInfuraNetwork,
|
||||
} from '../../../../shared/constants/network';
|
||||
|
||||
export enum NetworkClientType {
|
||||
Custom = 'custom',
|
||||
Infura = 'infura',
|
||||
}
|
||||
|
||||
type CustomNetworkConfiguration = {
|
||||
chainId: Hex;
|
||||
rpcUrl: string;
|
||||
type: NetworkClientType.Custom;
|
||||
};
|
||||
|
||||
type InfuraNetworkConfiguration = {
|
||||
network: BuiltInInfuraNetwork;
|
||||
infuraProjectId: string;
|
||||
type: NetworkClientType.Infura;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a JSON RPC network client for a specific network.
|
||||
*
|
||||
* @param networkConfig - The network configuration.
|
||||
* @returns
|
||||
*/
|
||||
export function createNetworkClient(
|
||||
networkConfig: CustomNetworkConfiguration | InfuraNetworkConfiguration,
|
||||
): { provider: SafeEventEmitterProvider; blockTracker: PollingBlockTracker } {
|
||||
const rpcApiMiddleware =
|
||||
networkConfig.type === NetworkClientType.Infura
|
||||
? createInfuraMiddleware({
|
||||
network: networkConfig.network,
|
||||
projectId: networkConfig.infuraProjectId,
|
||||
maxAttempts: 5,
|
||||
source: 'metamask',
|
||||
})
|
||||
: createFetchMiddleware({
|
||||
btoa: global.btoa,
|
||||
fetch: global.fetch,
|
||||
rpcUrl: networkConfig.rpcUrl,
|
||||
});
|
||||
|
||||
const rpcProvider = providerFromMiddleware(rpcApiMiddleware);
|
||||
|
||||
const blockTrackerOpts =
|
||||
process.env.IN_TEST && networkConfig.type === 'custom'
|
||||
? { pollingInterval: SECOND }
|
||||
: {};
|
||||
const blockTracker = new PollingBlockTracker({
|
||||
...blockTrackerOpts,
|
||||
provider: rpcProvider,
|
||||
});
|
||||
|
||||
const networkMiddleware =
|
||||
networkConfig.type === NetworkClientType.Infura
|
||||
? createInfuraNetworkMiddleware({
|
||||
blockTracker,
|
||||
network: networkConfig.network,
|
||||
rpcProvider,
|
||||
rpcApiMiddleware,
|
||||
})
|
||||
: createCustomNetworkMiddleware({
|
||||
blockTracker,
|
||||
chainId: networkConfig.chainId,
|
||||
rpcApiMiddleware,
|
||||
});
|
||||
|
||||
const engine = new JsonRpcEngine();
|
||||
|
||||
engine.push(networkMiddleware);
|
||||
|
||||
const provider = providerFromEngine(engine);
|
||||
|
||||
return { provider, blockTracker };
|
||||
}
|
||||
|
||||
function createInfuraNetworkMiddleware({
|
||||
blockTracker,
|
||||
network,
|
||||
rpcProvider,
|
||||
rpcApiMiddleware,
|
||||
}: {
|
||||
blockTracker: PollingBlockTracker;
|
||||
network: BuiltInInfuraNetwork;
|
||||
rpcProvider: SafeEventEmitterProvider;
|
||||
rpcApiMiddleware: JsonRpcMiddleware<unknown, unknown>;
|
||||
}) {
|
||||
return mergeMiddleware([
|
||||
createNetworkAndChainIdMiddleware({ network }),
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightCacheMiddleware(),
|
||||
createBlockRefMiddleware({ blockTracker, provider: rpcProvider }),
|
||||
createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
rpcApiMiddleware,
|
||||
]);
|
||||
}
|
||||
|
||||
function createNetworkAndChainIdMiddleware({
|
||||
network,
|
||||
}: {
|
||||
network: BuiltInInfuraNetwork;
|
||||
}) {
|
||||
if (!BUILT_IN_INFURA_NETWORKS[network]) {
|
||||
throw new Error(`createInfuraClient - unknown network "${network}"`);
|
||||
}
|
||||
|
||||
const { chainId, networkId } = BUILT_IN_INFURA_NETWORKS[network];
|
||||
|
||||
return createScaffoldMiddleware({
|
||||
eth_chainId: chainId,
|
||||
net_version: networkId,
|
||||
});
|
||||
}
|
||||
|
||||
const createChainIdMiddleware = (
|
||||
chainId: string,
|
||||
): JsonRpcMiddleware<unknown, unknown> => {
|
||||
return (req, res, next, end) => {
|
||||
if (req.method === 'eth_chainId') {
|
||||
res.result = chainId;
|
||||
return end();
|
||||
}
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
function createCustomNetworkMiddleware({
|
||||
blockTracker,
|
||||
chainId,
|
||||
rpcApiMiddleware,
|
||||
}: {
|
||||
blockTracker: PollingBlockTracker;
|
||||
chainId: string;
|
||||
rpcApiMiddleware: any;
|
||||
}) {
|
||||
const testMiddlewares = process.env.IN_TEST
|
||||
? [createEstimateGasDelayTestMiddleware()]
|
||||
: [];
|
||||
|
||||
return mergeMiddleware([
|
||||
...testMiddlewares,
|
||||
createChainIdMiddleware(chainId),
|
||||
createBlockRefRewriteMiddleware({ blockTracker }),
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightCacheMiddleware(),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
rpcApiMiddleware,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* For use in tests only.
|
||||
* Adds a delay to `eth_estimateGas` calls.
|
||||
*/
|
||||
function createEstimateGasDelayTestMiddleware() {
|
||||
return createAsyncMiddleware(async (req, _, next) => {
|
||||
if (req.method === 'eth_estimateGas') {
|
||||
await new Promise((resolve) => setTimeout(resolve, SECOND * 2));
|
||||
}
|
||||
return next();
|
||||
});
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||
import {
|
||||
createBlockRefMiddleware,
|
||||
createRetryOnEmptyMiddleware,
|
||||
createBlockCacheMiddleware,
|
||||
createInflightCacheMiddleware,
|
||||
createBlockTrackerInspectorMiddleware,
|
||||
providerFromMiddleware,
|
||||
} from '@metamask/eth-json-rpc-middleware';
|
||||
|
||||
import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura';
|
||||
import { PollingBlockTracker } from 'eth-block-tracker';
|
||||
|
||||
import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network';
|
||||
|
||||
export default function createInfuraClient({ network, projectId }) {
|
||||
const infuraMiddleware = createInfuraMiddleware({
|
||||
network,
|
||||
projectId,
|
||||
maxAttempts: 5,
|
||||
source: 'metamask',
|
||||
});
|
||||
const infuraProvider = providerFromMiddleware(infuraMiddleware);
|
||||
const blockTracker = new PollingBlockTracker({ provider: infuraProvider });
|
||||
|
||||
const networkMiddleware = mergeMiddleware([
|
||||
createNetworkAndChainIdMiddleware({ network }),
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightCacheMiddleware(),
|
||||
createBlockRefMiddleware({ blockTracker, provider: infuraProvider }),
|
||||
createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
infuraMiddleware,
|
||||
]);
|
||||
return { networkMiddleware, blockTracker };
|
||||
}
|
||||
|
||||
function createNetworkAndChainIdMiddleware({ network }) {
|
||||
if (!BUILT_IN_NETWORKS[network]) {
|
||||
throw new Error(`createInfuraClient - unknown network "${network}"`);
|
||||
}
|
||||
|
||||
const { chainId, networkId } = BUILT_IN_NETWORKS[network];
|
||||
|
||||
return createScaffoldMiddleware({
|
||||
eth_chainId: chainId,
|
||||
net_version: networkId,
|
||||
});
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { testsForProviderType } from './provider-api-tests/shared-tests';
|
||||
|
||||
describe('createInfuraClient', () => {
|
||||
testsForProviderType('infura');
|
||||
});
|
@ -1,61 +0,0 @@
|
||||
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||
import {
|
||||
createFetchMiddleware,
|
||||
createBlockRefRewriteMiddleware,
|
||||
createBlockCacheMiddleware,
|
||||
createInflightCacheMiddleware,
|
||||
createBlockTrackerInspectorMiddleware,
|
||||
providerFromMiddleware,
|
||||
} from '@metamask/eth-json-rpc-middleware';
|
||||
import { PollingBlockTracker } from 'eth-block-tracker';
|
||||
import { SECOND } from '../../../../shared/constants/time';
|
||||
|
||||
export default function createJsonRpcClient({ rpcUrl, chainId }) {
|
||||
const blockTrackerOpts = process.env.IN_TEST
|
||||
? { pollingInterval: SECOND }
|
||||
: {};
|
||||
const fetchMiddleware = createFetchMiddleware({ rpcUrl });
|
||||
const blockProvider = providerFromMiddleware(fetchMiddleware);
|
||||
const blockTracker = new PollingBlockTracker({
|
||||
...blockTrackerOpts,
|
||||
provider: blockProvider,
|
||||
});
|
||||
const testMiddlewares = process.env.IN_TEST
|
||||
? [createEstimateGasDelayTestMiddleware()]
|
||||
: [];
|
||||
|
||||
const networkMiddleware = mergeMiddleware([
|
||||
...testMiddlewares,
|
||||
createChainIdMiddleware(chainId),
|
||||
createBlockRefRewriteMiddleware({ blockTracker }),
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightCacheMiddleware(),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
fetchMiddleware,
|
||||
]);
|
||||
|
||||
return { networkMiddleware, blockTracker };
|
||||
}
|
||||
|
||||
function createChainIdMiddleware(chainId) {
|
||||
return (req, res, next, end) => {
|
||||
if (req.method === 'eth_chainId') {
|
||||
res.result = chainId;
|
||||
return end();
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* For use in tests only.
|
||||
* Adds a delay to `eth_estimateGas` calls.
|
||||
*/
|
||||
function createEstimateGasDelayTestMiddleware() {
|
||||
return createAsyncMiddleware(async (req, _, next) => {
|
||||
if (req.method === 'eth_estimateGas') {
|
||||
await new Promise((resolve) => setTimeout(resolve, SECOND * 2));
|
||||
}
|
||||
return next();
|
||||
});
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { testsForProviderType } from './provider-api-tests/shared-tests';
|
||||
|
||||
describe('createJsonRpcClient', () => {
|
||||
testsForProviderType('custom');
|
||||
});
|
@ -1 +1 @@
|
||||
export { default, NETWORK_EVENTS } from './network-controller';
|
||||
export { default, NetworkControllerEventTypes } from './network-controller';
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import EventEmitter from 'events';
|
||||
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
|
||||
import { JsonRpcEngine } from 'json-rpc-engine';
|
||||
import { providerFromEngine } from '@metamask/eth-json-rpc-middleware';
|
||||
import log from 'loglevel';
|
||||
import {
|
||||
createSwappableProxy,
|
||||
createEventEmitterProxy,
|
||||
} from 'swappable-obj-proxy';
|
||||
} from '@metamask/swappable-obj-proxy';
|
||||
import EthQuery from 'eth-query';
|
||||
// ControllerMessenger is referred to in the JSDocs
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { ControllerMessenger } from '@metamask/base-controller';
|
||||
import { v4 as random } from 'uuid';
|
||||
import { hasProperty, isPlainObject } from '@metamask/utils';
|
||||
import { errorCodes } from 'eth-rpc-errors';
|
||||
import {
|
||||
INFURA_PROVIDER_TYPES,
|
||||
BUILT_IN_NETWORKS,
|
||||
@ -17,15 +20,14 @@ import {
|
||||
TEST_NETWORK_TICKER_MAP,
|
||||
CHAIN_IDS,
|
||||
NETWORK_TYPES,
|
||||
NetworkStatus,
|
||||
} from '../../../../shared/constants/network';
|
||||
import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout';
|
||||
import {
|
||||
isPrefixedFormattedHexString,
|
||||
isSafeChainId,
|
||||
} from '../../../../shared/modules/network.utils';
|
||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||
import createInfuraClient from './createInfuraClient';
|
||||
import createJsonRpcClient from './createJsonRpcClient';
|
||||
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
|
||||
import { createNetworkClient } from './create-network-client';
|
||||
|
||||
/**
|
||||
* @typedef {object} NetworkConfiguration
|
||||
@ -36,91 +38,133 @@ import createJsonRpcClient from './createJsonRpcClient';
|
||||
* @property {string} [nickname] - Personalized network name.
|
||||
*/
|
||||
|
||||
const env = process.env.METAMASK_ENV;
|
||||
const fetchWithTimeout = getFetchWithTimeout();
|
||||
function buildDefaultProviderConfigState() {
|
||||
if (process.env.IN_TEST) {
|
||||
return {
|
||||
type: NETWORK_TYPES.RPC,
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
chainId: '0x539',
|
||||
nickname: 'Localhost 8545',
|
||||
ticker: 'ETH',
|
||||
};
|
||||
} else if (
|
||||
process.env.METAMASK_DEBUG ||
|
||||
process.env.METAMASK_ENV === 'test'
|
||||
) {
|
||||
return {
|
||||
type: NETWORK_TYPES.GOERLI,
|
||||
chainId: CHAIN_IDS.GOERLI,
|
||||
ticker: TEST_NETWORK_TICKER_MAP.GOERLI,
|
||||
};
|
||||
}
|
||||
|
||||
let defaultProviderConfigOpts;
|
||||
if (process.env.IN_TEST) {
|
||||
defaultProviderConfigOpts = {
|
||||
type: NETWORK_TYPES.RPC,
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
chainId: '0x539',
|
||||
nickname: 'Localhost 8545',
|
||||
};
|
||||
} else if (process.env.METAMASK_DEBUG || env === 'test') {
|
||||
defaultProviderConfigOpts = {
|
||||
type: NETWORK_TYPES.GOERLI,
|
||||
chainId: CHAIN_IDS.GOERLI,
|
||||
ticker: TEST_NETWORK_TICKER_MAP.GOERLI,
|
||||
};
|
||||
} else {
|
||||
defaultProviderConfigOpts = {
|
||||
return {
|
||||
type: NETWORK_TYPES.MAINNET,
|
||||
chainId: CHAIN_IDS.MAINNET,
|
||||
ticker: 'ETH',
|
||||
};
|
||||
}
|
||||
|
||||
const defaultProviderConfig = {
|
||||
ticker: 'ETH',
|
||||
...defaultProviderConfigOpts,
|
||||
};
|
||||
function buildDefaultNetworkIdState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultNetworkDetailsState = {
|
||||
EIPS: { 1559: undefined },
|
||||
};
|
||||
function buildDefaultNetworkStatusState() {
|
||||
return NetworkStatus.Unknown;
|
||||
}
|
||||
|
||||
export const NETWORK_EVENTS = {
|
||||
// Fired after the actively selected network is changed
|
||||
NETWORK_DID_CHANGE: 'networkDidChange',
|
||||
// Fired when the actively selected network *will* change
|
||||
NETWORK_WILL_CHANGE: 'networkWillChange',
|
||||
// Fired when Infura returns an error indicating no support
|
||||
INFURA_IS_BLOCKED: 'infuraIsBlocked',
|
||||
// Fired when not using an Infura network or when Infura returns no error, indicating support
|
||||
INFURA_IS_UNBLOCKED: 'infuraIsUnblocked',
|
||||
function buildDefaultNetworkDetailsState() {
|
||||
return {
|
||||
EIPS: {
|
||||
1559: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultNetworkConfigurationsState() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the controller.
|
||||
*/
|
||||
const name = 'NetworkController';
|
||||
|
||||
/**
|
||||
* The set of event types that this controller can publish via its messenger.
|
||||
*/
|
||||
export const NetworkControllerEventTypes = {
|
||||
/**
|
||||
* Fired after the current network is changed.
|
||||
*/
|
||||
NetworkDidChange: `${name}:networkDidChange`,
|
||||
/**
|
||||
* Fired when there is a request to change the current network, but no state
|
||||
* changes have occurred yet.
|
||||
*/
|
||||
NetworkWillChange: `${name}:networkWillChange`,
|
||||
/**
|
||||
* Fired after the network is changed to an Infura network, but when Infura
|
||||
* returns an error denying support for the user's location.
|
||||
*/
|
||||
InfuraIsBlocked: `${name}:infuraIsBlocked`,
|
||||
/**
|
||||
* Fired after the network is changed to an Infura network and Infura does not
|
||||
* return an error denying support for the user's location, or after the
|
||||
* network is changed to a custom network.
|
||||
*/
|
||||
InfuraIsUnblocked: `${name}:infuraIsUnblocked`,
|
||||
};
|
||||
|
||||
export default class NetworkController extends EventEmitter {
|
||||
static defaultProviderConfig = defaultProviderConfig;
|
||||
|
||||
/**
|
||||
* Construct a NetworkController.
|
||||
*
|
||||
* @param {object} [options] - NetworkController options.
|
||||
* @param {object} options - Options for this controller.
|
||||
* @param {ControllerMessenger} options.messenger - The controller messenger.
|
||||
* @param {object} [options.state] - Initial controller state.
|
||||
* @param {string} [options.infuraProjectId] - The Infura project ID.
|
||||
* @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController
|
||||
*/
|
||||
constructor({ state = {}, infuraProjectId, trackMetaMetricsEvent } = {}) {
|
||||
constructor({
|
||||
messenger,
|
||||
state = {},
|
||||
infuraProjectId,
|
||||
trackMetaMetricsEvent,
|
||||
} = {}) {
|
||||
super();
|
||||
|
||||
this.messenger = messenger;
|
||||
|
||||
// create stores
|
||||
this.providerStore = new ObservableStore(
|
||||
state.provider || { ...defaultProviderConfig },
|
||||
state.provider || buildDefaultProviderConfigState(),
|
||||
);
|
||||
this.previousProviderStore = new ObservableStore(
|
||||
this.providerStore.getState(),
|
||||
);
|
||||
this.networkStore = new ObservableStore('loading');
|
||||
// We need to keep track of a few details about the current network
|
||||
// Ideally we'd merge this.networkStore with this new store, but doing so
|
||||
// will require a decent sized refactor of how we're accessing network
|
||||
// state. Currently this is only used for detecting EIP 1559 support but
|
||||
// can be extended to track other network details.
|
||||
this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState());
|
||||
this.networkStatusStore = new ObservableStore(
|
||||
buildDefaultNetworkStatusState(),
|
||||
);
|
||||
// We need to keep track of a few details about the current network.
|
||||
// Ideally we'd merge this.networkStatusStore with this new store, but doing
|
||||
// so will require a decent sized refactor of how we're accessing network
|
||||
// state. Currently this is only used for detecting EIP-1559 support but can
|
||||
// be extended to track other network details.
|
||||
this.networkDetails = new ObservableStore(
|
||||
state.networkDetails || {
|
||||
...defaultNetworkDetailsState,
|
||||
},
|
||||
state.networkDetails || buildDefaultNetworkDetailsState(),
|
||||
);
|
||||
|
||||
this.networkConfigurationsStore = new ObservableStore(
|
||||
state.networkConfigurations || {},
|
||||
state.networkConfigurations || buildDefaultNetworkConfigurationsState(),
|
||||
);
|
||||
|
||||
this.store = new ComposedStore({
|
||||
provider: this.providerStore,
|
||||
previousProviderStore: this.previousProviderStore,
|
||||
network: this.networkStore,
|
||||
networkId: this.networkIdStore,
|
||||
networkStatus: this.networkStatusStore,
|
||||
networkDetails: this.networkDetails,
|
||||
networkConfigurations: this.networkConfigurationsStore,
|
||||
});
|
||||
@ -137,11 +181,8 @@ export default class NetworkController extends EventEmitter {
|
||||
throw new Error('Invalid Infura project ID');
|
||||
}
|
||||
this._infuraProjectId = infuraProjectId;
|
||||
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
|
||||
|
||||
this.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
||||
this.lookupNetwork();
|
||||
});
|
||||
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -167,10 +208,12 @@ export default class NetworkController extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check if the block header contains fields that indicate EIP 1559
|
||||
* support (baseFeePerGas).
|
||||
* Determines whether the network supports EIP-1559 by checking whether the
|
||||
* latest block has a `baseFeePerGas` property, then updates state
|
||||
* appropriately.
|
||||
*
|
||||
* @returns {Promise<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();
|
||||
@ -179,15 +222,28 @@ export default class NetworkController extends EventEmitter {
|
||||
if (EIPS[1559] !== undefined) {
|
||||
return EIPS[1559];
|
||||
}
|
||||
const latestBlock = await this._getLatestBlock();
|
||||
const supportsEIP1559 =
|
||||
latestBlock && latestBlock.baseFeePerGas !== undefined;
|
||||
this._setNetworkEIPSupport(1559, supportsEIP1559);
|
||||
const supportsEIP1559 = await this._determineEIP1559Compatibility();
|
||||
this.networkDetails.updateState({
|
||||
EIPS: {
|
||||
...this.networkDetails.getState().EIPS,
|
||||
1559: supportsEIP1559,
|
||||
},
|
||||
});
|
||||
return supportsEIP1559;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures information about the currently selected network — namely,
|
||||
* the network ID and whether the network supports EIP-1559 — and then uses
|
||||
* the results of these requests to determine the status of the network.
|
||||
*/
|
||||
async lookupNetwork() {
|
||||
// Prevent firing when provider is not defined.
|
||||
const { chainId, type } = this.providerStore.getState();
|
||||
let networkChanged = false;
|
||||
let networkId;
|
||||
let supportsEIP1559;
|
||||
let networkStatus;
|
||||
|
||||
if (!this._provider) {
|
||||
log.warn(
|
||||
'NetworkController - lookupNetwork aborted due to missing provider',
|
||||
@ -195,46 +251,102 @@ export default class NetworkController extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const { chainId } = this.providerStore.getState();
|
||||
if (!chainId) {
|
||||
log.warn(
|
||||
'NetworkController - lookupNetwork aborted due to missing chainId',
|
||||
);
|
||||
this._setNetworkState('loading');
|
||||
this._clearNetworkDetails();
|
||||
this._resetNetworkId();
|
||||
this._resetNetworkStatus();
|
||||
this._resetNetworkDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ping the RPC endpoint so we can confirm that it works
|
||||
const initialNetwork = this.networkStore.getState();
|
||||
const { type } = this.providerStore.getState();
|
||||
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
|
||||
|
||||
if (isInfura) {
|
||||
this._checkInfuraAvailability(type);
|
||||
} else {
|
||||
this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED);
|
||||
const listener = () => {
|
||||
networkChanged = true;
|
||||
this.messenger.unsubscribe(
|
||||
NetworkControllerEventTypes.NetworkDidChange,
|
||||
listener,
|
||||
);
|
||||
};
|
||||
this.messenger.subscribe(
|
||||
NetworkControllerEventTypes.NetworkDidChange,
|
||||
listener,
|
||||
);
|
||||
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
this._getNetworkId(),
|
||||
this._determineEIP1559Compatibility(),
|
||||
]);
|
||||
networkId = results[0];
|
||||
supportsEIP1559 = results[1];
|
||||
networkStatus = NetworkStatus.Available;
|
||||
} catch (error) {
|
||||
if (hasProperty(error, 'code')) {
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = JSON.parse(error.message);
|
||||
} catch {
|
||||
// error.message must not be JSON
|
||||
}
|
||||
|
||||
if (
|
||||
isPlainObject(responseBody) &&
|
||||
responseBody.error === INFURA_BLOCKED_KEY
|
||||
) {
|
||||
networkStatus = NetworkStatus.Blocked;
|
||||
} else if (error.code === errorCodes.rpc.internal) {
|
||||
networkStatus = NetworkStatus.Unknown;
|
||||
} else {
|
||||
networkStatus = NetworkStatus.Unavailable;
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
'NetworkController - could not determine network status',
|
||||
error,
|
||||
);
|
||||
networkStatus = NetworkStatus.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
let networkVersion;
|
||||
let networkVersionError;
|
||||
try {
|
||||
networkVersion = await this._getNetworkId();
|
||||
} catch (error) {
|
||||
networkVersionError = error;
|
||||
}
|
||||
if (initialNetwork !== this.networkStore.getState()) {
|
||||
if (networkChanged) {
|
||||
// If the network has changed, then `lookupNetwork` either has been or is
|
||||
// in the process of being called, so we don't need to go further.
|
||||
return;
|
||||
}
|
||||
this.messenger.unsubscribe(
|
||||
NetworkControllerEventTypes.NetworkDidChange,
|
||||
listener,
|
||||
);
|
||||
|
||||
if (networkVersionError) {
|
||||
this._setNetworkState('loading');
|
||||
// keep network details in sync with network state
|
||||
this._clearNetworkDetails();
|
||||
this.networkStatusStore.putState(networkStatus);
|
||||
|
||||
if (networkStatus === NetworkStatus.Available) {
|
||||
this.networkIdStore.putState(networkId);
|
||||
this.networkDetails.updateState({
|
||||
EIPS: {
|
||||
...this.networkDetails.getState().EIPS,
|
||||
1559: supportsEIP1559,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this._setNetworkState(networkVersion);
|
||||
// look up EIP-1559 support
|
||||
await this.getEIP1559Compatibility();
|
||||
this._resetNetworkId();
|
||||
this._resetNetworkDetails();
|
||||
}
|
||||
|
||||
if (isInfura) {
|
||||
if (networkStatus === NetworkStatus.Available) {
|
||||
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
|
||||
} else if (networkStatus === NetworkStatus.Blocked) {
|
||||
this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked);
|
||||
}
|
||||
} else {
|
||||
// Always publish infuraIsUnblocked regardless of network status to
|
||||
// prevent consumers from being stuck in a blocked state if they were
|
||||
// previously connected to an Infura network that was blocked
|
||||
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +401,7 @@ export default class NetworkController extends EventEmitter {
|
||||
|
||||
rollbackToPreviousProvider() {
|
||||
const config = this.previousProviderStore.getState();
|
||||
this.providerStore.updateState(config);
|
||||
this.providerStore.putState(config);
|
||||
this._switchNetwork(config);
|
||||
}
|
||||
|
||||
@ -297,13 +409,38 @@ export default class NetworkController extends EventEmitter {
|
||||
// Private
|
||||
//
|
||||
|
||||
/**
|
||||
* Method to return the latest block for the current network
|
||||
*
|
||||
* @returns {object} Block header
|
||||
*/
|
||||
_getLatestBlock() {
|
||||
const { provider } = this.getProviderAndBlockTracker();
|
||||
const ethQuery = new EthQuery(provider);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ethQuery.sendAsync(
|
||||
{ method: 'eth_getBlockByNumber', params: ['latest', false] },
|
||||
(error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the network ID for the current selected network
|
||||
*
|
||||
* @returns {string} The network ID for the current network.
|
||||
*/
|
||||
async _getNetworkId() {
|
||||
const ethQuery = new EthQuery(this._provider);
|
||||
const { provider } = this.getProviderAndBlockTracker();
|
||||
const ethQuery = new EthQuery(provider);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
ethQuery.sendAsync({ method: 'net_version' }, (error, result) => {
|
||||
if (error) {
|
||||
@ -316,49 +453,24 @@ export default class NetworkController extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return the latest block for the current network
|
||||
*
|
||||
* @returns {object} Block header
|
||||
* Clears the stored network ID.
|
||||
*/
|
||||
_getLatestBlock() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { provider } = this.getProviderAndBlockTracker();
|
||||
const ethQuery = new EthQuery(provider);
|
||||
ethQuery.sendAsync(
|
||||
{ method: 'eth_getBlockByNumber', params: ['latest', false] },
|
||||
(err, block) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(block);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_setNetworkState(network) {
|
||||
this.networkStore.putState(network);
|
||||
_resetNetworkId() {
|
||||
this.networkIdStore.putState(buildDefaultNetworkIdState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set EIP support indication in the networkDetails store
|
||||
*
|
||||
* @param {number} EIPNumber - The number of the EIP to mark support for
|
||||
* @param {boolean} isSupported - True if the EIP is supported
|
||||
* Resets network status to the default ("unknown").
|
||||
*/
|
||||
_setNetworkEIPSupport(EIPNumber, isSupported) {
|
||||
this.networkDetails.updateState({
|
||||
EIPS: {
|
||||
[EIPNumber]: isSupported,
|
||||
},
|
||||
});
|
||||
_resetNetworkStatus() {
|
||||
this.networkStatusStore.putState(buildDefaultNetworkStatusState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset EIP support to default (no support)
|
||||
* Clears details previously stored for the network.
|
||||
*/
|
||||
_clearNetworkDetails() {
|
||||
this.networkDetails.putState({ ...defaultNetworkDetailsState });
|
||||
_resetNetworkDetails() {
|
||||
this.networkDetails.putState(buildDefaultNetworkDetailsState());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -367,68 +479,42 @@ export default class NetworkController extends EventEmitter {
|
||||
* @param config
|
||||
*/
|
||||
_setProviderConfig(config) {
|
||||
this.previousProviderStore.updateState(this.providerStore.getState());
|
||||
this.providerStore.updateState(config);
|
||||
this.previousProviderStore.putState(this.providerStore.getState());
|
||||
this.providerStore.putState(config);
|
||||
this._switchNetwork(config);
|
||||
}
|
||||
|
||||
async _checkInfuraAvailability(network) {
|
||||
const rpcUrl = `https://${network}.infura.io/v3/${this._infuraProjectId}`;
|
||||
|
||||
let networkChanged = false;
|
||||
this.once(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
||||
networkChanged = true;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(rpcUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_blockNumber',
|
||||
params: [],
|
||||
id: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (networkChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED);
|
||||
} else {
|
||||
const responseMessage = await response.json();
|
||||
if (networkChanged) {
|
||||
return;
|
||||
}
|
||||
if (responseMessage.error === INFURA_BLOCKED_KEY) {
|
||||
this.emit(NETWORK_EVENTS.INFURA_IS_BLOCKED);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`MetaMask - Infura availability check failed`, err);
|
||||
}
|
||||
/**
|
||||
* Retrieves the latest block from the currently selected network; if the
|
||||
* block has a `baseFeePerGas` property, then we know that the network
|
||||
* supports EIP-1559; otherwise it doesn't.
|
||||
*
|
||||
* @returns {Promise<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 }) {
|
||||
// infura type-based endpoints
|
||||
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
|
||||
if (isInfura) {
|
||||
this._configureInfuraProvider(type, this._infuraProjectId);
|
||||
this._configureInfuraProvider({
|
||||
type,
|
||||
infuraProjectId: this._infuraProjectId,
|
||||
});
|
||||
// url-based rpc endpoints
|
||||
} else if (type === NETWORK_TYPES.RPC) {
|
||||
this._configureStandardProvider(rpcUrl, chainId);
|
||||
@ -439,25 +525,23 @@ export default class NetworkController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_configureInfuraProvider(type, projectId) {
|
||||
_configureInfuraProvider({ type, infuraProjectId }) {
|
||||
log.info('NetworkController - configureInfuraProvider', type);
|
||||
const networkClient = createInfuraClient({
|
||||
const { provider, blockTracker } = createNetworkClient({
|
||||
network: type,
|
||||
projectId,
|
||||
infuraProjectId,
|
||||
type: 'infura',
|
||||
});
|
||||
this._setNetworkClient(networkClient);
|
||||
this._setProviderAndBlockTracker({ provider, blockTracker });
|
||||
}
|
||||
|
||||
_configureStandardProvider(rpcUrl, chainId) {
|
||||
log.info('NetworkController - configureStandardProvider', rpcUrl);
|
||||
const networkClient = createJsonRpcClient({ rpcUrl, chainId });
|
||||
this._setNetworkClient(networkClient);
|
||||
}
|
||||
|
||||
_setNetworkClient({ networkMiddleware, blockTracker }) {
|
||||
const engine = new JsonRpcEngine();
|
||||
engine.push(networkMiddleware);
|
||||
const provider = providerFromEngine(engine);
|
||||
const { provider, blockTracker } = createNetworkClient({
|
||||
chainId,
|
||||
rpcUrl,
|
||||
type: 'custom',
|
||||
});
|
||||
this._setProviderAndBlockTracker({ provider, blockTracker });
|
||||
}
|
||||
|
||||
@ -550,7 +634,7 @@ export default class NetworkController extends EventEmitter {
|
||||
)?.id;
|
||||
|
||||
const newNetworkConfigurationId = oldNetworkConfigurationId || random();
|
||||
this.networkConfigurationsStore.updateState({
|
||||
this.networkConfigurationsStore.putState({
|
||||
...networkConfigurations,
|
||||
[newNetworkConfigurationId]: {
|
||||
...newNetworkConfiguration,
|
||||
@ -561,7 +645,7 @@ export default class NetworkController extends EventEmitter {
|
||||
if (!oldNetworkConfigurationId) {
|
||||
this._trackMetaMetricsEvent({
|
||||
event: 'Custom Network Added',
|
||||
category: EVENT.CATEGORIES.NETWORK,
|
||||
category: MetaMetricsEventCategory.Network,
|
||||
referrer: {
|
||||
url: referrer,
|
||||
},
|
||||
|
@ -0,0 +1,272 @@
|
||||
/* eslint-disable jest/require-top-level-describe, jest/no-export */
|
||||
|
||||
import { withMockedCommunications, withNetworkClient } from './helpers';
|
||||
|
||||
/**
|
||||
* Defines tests which exercise the behavior exhibited by an RPC method that
|
||||
* use `blockHash` in the response data to determine whether the response is
|
||||
* cacheable.
|
||||
*
|
||||
* @param method - The name of the RPC method under test.
|
||||
* @param additionalArgs - Additional arguments.
|
||||
* @param additionalArgs.numberOfParameters - The number of parameters supported
|
||||
* by the method under test.
|
||||
* @param additionalArgs.providerType - The type of provider being tested;
|
||||
* either `infura` or `custom` (default: "infura").
|
||||
*/
|
||||
export function testsForRpcMethodsThatCheckForBlockHashInResponse(
|
||||
method,
|
||||
{ numberOfParameters, providerType },
|
||||
) {
|
||||
if (providerType !== 'infura' && providerType !== 'custom') {
|
||||
throw new Error(
|
||||
`providerType must be either "infura" or "custom", was "${providerType}" instead`,
|
||||
);
|
||||
}
|
||||
|
||||
it('does not hit the RPC endpoint more than once for identical requests and it has a valid blockHash', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResult = { blockHash: '0x1' };
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResult },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual([mockResult, mockResult]);
|
||||
});
|
||||
});
|
||||
|
||||
it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// Note that we have to mock these requests in a specific order. The
|
||||
// first block tracker request occurs because of the first RPC
|
||||
// request. The second block tracker request, however, does not occur
|
||||
// because of the second RPC request, but rather because we call
|
||||
// `clock.runAll()` below.
|
||||
comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' });
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' });
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
async (client) => {
|
||||
const firstResult = await client.makeRpcCall(requests[0]);
|
||||
// Proceed to the next iteration of the block tracker so that a new
|
||||
// block is fetched and the current block is updated.
|
||||
client.clock.runAll();
|
||||
const secondResult = await client.makeRpcCall(requests[1]);
|
||||
return [firstResult, secondResult];
|
||||
},
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reuse the result of a previous request if result.blockHash was null', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = [
|
||||
{ blockHash: null, extra: 'some value' },
|
||||
{ blockHash: '0x100', extra: 'some other value' },
|
||||
];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reuse the result of a previous request if result.blockHash was undefined', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = [
|
||||
{ extra: 'some value' },
|
||||
{ blockHash: '0x100', extra: 'some other value' },
|
||||
];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reuse the result of a previous request if result.blockHash was "0x0000000000000000000000000000000000000000000000000000000000000000"', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = [
|
||||
{
|
||||
blockHash:
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
extra: 'some value',
|
||||
},
|
||||
{ blockHash: '0x100', extra: 'some other value' },
|
||||
];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
|
||||
for (const emptyValue of [null, undefined, '\u003cnil\u003e']) {
|
||||
it(`does not retry an empty response of "${emptyValue}"`, async () => {
|
||||
const request = { method };
|
||||
const mockResult = emptyValue;
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: { result: mockResult },
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = [emptyValue, { blockHash: '0x100' }];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const paramIndex of [...Array(numberOfParameters).keys()]) {
|
||||
it(`does not reuse the result of a previous request with a valid blockHash if parameter at index "${paramIndex}" differs`, async () => {
|
||||
const firstMockParams = [
|
||||
...new Array(numberOfParameters).fill('some value'),
|
||||
];
|
||||
const secondMockParams = firstMockParams.slice();
|
||||
secondMockParams[paramIndex] = 'another value';
|
||||
const requests = [
|
||||
{
|
||||
method,
|
||||
params: firstMockParams,
|
||||
},
|
||||
{ method, params: secondMockParams },
|
||||
];
|
||||
const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual([mockResults[0], mockResults[1]]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
2059
app/scripts/controllers/network/provider-api-tests/block-param.js
Normal file
@ -1,10 +1,7 @@
|
||||
import nock from 'nock';
|
||||
import sinon from 'sinon';
|
||||
import { JsonRpcEngine } from 'json-rpc-engine';
|
||||
import { providerFromEngine } from '@metamask/eth-json-rpc-middleware';
|
||||
import EthQuery from 'eth-query';
|
||||
import createInfuraClient from '../createInfuraClient';
|
||||
import createJsonRpcClient from '../createJsonRpcClient';
|
||||
import { createNetworkClient } from '../create-network-client';
|
||||
|
||||
/**
|
||||
* @typedef {import('nock').Scope} NockScope
|
||||
@ -13,55 +10,6 @@ import createJsonRpcClient from '../createJsonRpcClient';
|
||||
* base URL.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial<JsonRpcRequest>) => Promise<any>, makeRpcCallsInSeries: (requests: Partial<JsonRpcRequest>[]) => Promise<any>}} Client
|
||||
*
|
||||
* Provides methods to interact with the suite of middleware that
|
||||
* `createInfuraClient` or `createJsonRpcClient` exposes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions
|
||||
*
|
||||
* The options bag that `withNetworkClient` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(client: Client) => Promise<any>} WithClientCallback
|
||||
*
|
||||
* The callback that `withNetworkClient` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions
|
||||
*
|
||||
* The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions
|
||||
*
|
||||
* The options to `mockRpcCall`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{mockNextBlockTrackerRequest: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockAllBlockTrackerRequests: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockRpcCall: (options: Omit<MockRpcCallOptions, 'nockScope'>) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications
|
||||
*
|
||||
* Provides methods to mock different kinds of requests to the provider.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions
|
||||
*
|
||||
* The options bag that `Communications` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(comms: Communications) => Promise<any>} WithMockedCommunicationsCallback
|
||||
*
|
||||
* The callback that `mockingCommunications` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A dummy value for the `infuraProjectId` option that `createInfuraClient`
|
||||
* needs. (Infura should not be hit during tests, but just in case, this should
|
||||
@ -82,6 +30,12 @@ const MOCK_RPC_URL = 'http://foo.com';
|
||||
*/
|
||||
const DEFAULT_LATEST_BLOCK_NUMBER = '0x42';
|
||||
|
||||
/**
|
||||
* A reference to the original `setTimeout` function so that we can use it even
|
||||
* when using fake timers.
|
||||
*/
|
||||
const originalSetTimeout = setTimeout;
|
||||
|
||||
/**
|
||||
* If you're having trouble writing a test and you're wondering why the test
|
||||
* keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This
|
||||
@ -103,14 +57,17 @@ function debug(...args) {
|
||||
*/
|
||||
function buildScopeForMockingRequests(rpcUrl) {
|
||||
return nock(rpcUrl).filteringRequestBody((body) => {
|
||||
const copyOfBody = JSON.parse(body);
|
||||
// Some IDs are random, so remove them entirely from the request to make it
|
||||
// possible to mock these requests
|
||||
delete copyOfBody.id;
|
||||
return JSON.stringify(copyOfBody);
|
||||
debug('Nock Received Request: ', body);
|
||||
return body;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions
|
||||
*
|
||||
* The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mocks the next request for the latest block that the block tracker will make.
|
||||
*
|
||||
@ -151,6 +108,12 @@ async function mockAllBlockTrackerRequests({
|
||||
}).persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions
|
||||
*
|
||||
* The options to `mockRpcCall`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mocks a JSON-RPC request sent to the provider with the given response.
|
||||
* Provider type is inferred from the base url set on the nockScope.
|
||||
@ -177,24 +140,38 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) {
|
||||
// eth-query always passes `params`, so even if we don't supply this property,
|
||||
// for consistency with makeRpcCall, assume that the `body` contains it
|
||||
const { method, params = [], ...rest } = request;
|
||||
const httpStatus = response?.httpStatus ?? 200;
|
||||
let completeResponse;
|
||||
let httpStatus = 200;
|
||||
let completeResponse = { id: 2, jsonrpc: '2.0' };
|
||||
if (response !== undefined) {
|
||||
if (response.body === undefined) {
|
||||
completeResponse = { id: 1, jsonrpc: '2.0' };
|
||||
['id', 'jsonrpc', 'result', 'error'].forEach((prop) => {
|
||||
if (response[prop] !== undefined) {
|
||||
completeResponse[prop] = response[prop];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if ('body' in response) {
|
||||
completeResponse = response.body;
|
||||
} else {
|
||||
if (response.error) {
|
||||
completeResponse.error = response.error;
|
||||
} else {
|
||||
completeResponse.result = response.result;
|
||||
}
|
||||
if (response.httpStatus) {
|
||||
httpStatus = response.httpStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
const url = nockScope.basePath.includes('infura.io')
|
||||
? `/v3/${MOCK_INFURA_PROJECT_ID}`
|
||||
: '/';
|
||||
|
||||
debug('Mocking request:', {
|
||||
url,
|
||||
method,
|
||||
params,
|
||||
response,
|
||||
error,
|
||||
...rest,
|
||||
times,
|
||||
});
|
||||
|
||||
let nockRequest = nockScope.post(url, {
|
||||
id: /\d*/u,
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
@ -212,7 +189,17 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) {
|
||||
if (error !== undefined) {
|
||||
return nockRequest.replyWithError(error);
|
||||
} else if (completeResponse !== undefined) {
|
||||
return nockRequest.reply(httpStatus, completeResponse);
|
||||
return nockRequest.reply(httpStatus, (_, requestBody) => {
|
||||
if (response !== undefined && !('body' in response)) {
|
||||
if (response.id === undefined) {
|
||||
completeResponse.id = requestBody.id;
|
||||
} else {
|
||||
completeResponse.id = response.id;
|
||||
}
|
||||
}
|
||||
debug('Nock returning Response', completeResponse);
|
||||
return completeResponse;
|
||||
});
|
||||
}
|
||||
return nockRequest;
|
||||
}
|
||||
@ -240,6 +227,24 @@ function makeRpcCall(ethQuery, request) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions
|
||||
*
|
||||
* The options bag that `Communications` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{mockNextBlockTrackerRequest: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockAllBlockTrackerRequests: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockRpcCall: (options: Omit<MockRpcCallOptions, 'nockScope'>) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications
|
||||
*
|
||||
* Provides methods to mock different kinds of requests to the provider.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(comms: Communications) => Promise<any>} WithMockedCommunicationsCallback
|
||||
*
|
||||
* The callback that `mockingCommunications` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets up request mocks for requests to the provider.
|
||||
*
|
||||
@ -275,6 +280,7 @@ export async function withMockedCommunications(
|
||||
mockAllBlockTrackerRequests({ nockScope, ...localOptions });
|
||||
const curriedMockRpcCall = (localOptions) =>
|
||||
mockRpcCall({ nockScope, ...localOptions });
|
||||
|
||||
const comms = {
|
||||
mockNextBlockTrackerRequest: curriedMockNextBlockTrackerRequest,
|
||||
mockAllBlockTrackerRequests: curriedMockAllBlockTrackerRequests,
|
||||
@ -291,6 +297,71 @@ export async function withMockedCommunications(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial<JsonRpcRequest>) => Promise<any>, makeRpcCallsInSeries: (requests: Partial<JsonRpcRequest>[]) => Promise<any>}} MockNetworkClient
|
||||
*
|
||||
* Provides methods to interact with the suite of middleware that
|
||||
* `createInfuraClient` or `createJsonRpcClient` exposes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some middleware contain logic which retries the request if some condition
|
||||
* applies. This retrying always happens out of band via `setTimeout`, and
|
||||
* because we are stubbing time via Jest's fake timers, we have to manually
|
||||
* advance the clock so that the `setTimeout` handlers get fired. We don't know
|
||||
* when these timers will get created, however, so we have to keep advancing
|
||||
* timers until the request has been made an appropriate number of times.
|
||||
* Unfortunately we don't have a good way to know how many times a request has
|
||||
* been retried, but the good news is that the middleware won't end, and thus
|
||||
* the promise which the RPC call returns won't get fulfilled, until all retries
|
||||
* have been made.
|
||||
*
|
||||
* @param promise - The promise which is returned by the RPC call.
|
||||
* @param clock - A Sinon clock object which can be used to advance to the next
|
||||
* `setTimeout` handler.
|
||||
*/
|
||||
export async function waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
promise,
|
||||
clock,
|
||||
) {
|
||||
let hasPromiseBeenFulfilled = false;
|
||||
let numTimesClockHasBeenAdvanced = 0;
|
||||
|
||||
promise
|
||||
.catch((error) => {
|
||||
// This is used to silence Node.js warnings about the rejection
|
||||
// being handled asynchronously. The error is handled later when
|
||||
// `promise` is awaited, but we log it here anyway in case it gets
|
||||
// swallowed.
|
||||
debug(error);
|
||||
})
|
||||
.finally(() => {
|
||||
hasPromiseBeenFulfilled = true;
|
||||
});
|
||||
|
||||
// `hasPromiseBeenFulfilled` is modified asynchronously.
|
||||
/* eslint-disable-next-line no-unmodified-loop-condition */
|
||||
while (!hasPromiseBeenFulfilled && numTimesClockHasBeenAdvanced < 15) {
|
||||
clock.runAll();
|
||||
await new Promise((resolve) => originalSetTimeout(resolve, 10));
|
||||
numTimesClockHasBeenAdvanced += 1;
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions
|
||||
*
|
||||
* The options bag that `withNetworkClient` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(client: MockNetworkClient) => Promise<any>} WithClientCallback
|
||||
*
|
||||
* The callback that `withNetworkClient` takes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Builds a provider from the middleware (for the provider type) along with a
|
||||
* block tracker, runs the given function with those two things, and then
|
||||
@ -325,6 +396,13 @@ export async function withNetworkClient(
|
||||
);
|
||||
}
|
||||
|
||||
// Faking timers ends up doing two things:
|
||||
// 1. Halting the block tracker (which depends on `setTimeout` to periodically
|
||||
// request the latest block) set up in `eth-json-rpc-middleware`
|
||||
// 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which also
|
||||
// depends on `setTimeout`)
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
||||
// The JSON-RPC client wraps `eth_estimateGas` so that it takes 2 seconds longer
|
||||
// than it usually would to complete. Or at least it should — this doesn't
|
||||
// appear to be working correctly. Unset `IN_TEST` on `process.env` to prevent
|
||||
@ -333,20 +411,21 @@ export async function withNetworkClient(
|
||||
delete process.env.IN_TEST;
|
||||
const clientUnderTest =
|
||||
providerType === 'infura'
|
||||
? createInfuraClient({
|
||||
? createNetworkClient({
|
||||
network: infuraNetwork,
|
||||
projectId: MOCK_INFURA_PROJECT_ID,
|
||||
infuraProjectId: MOCK_INFURA_PROJECT_ID,
|
||||
type: 'infura',
|
||||
})
|
||||
: createJsonRpcClient({ rpcUrl: customRpcUrl, chainId: customChainId });
|
||||
: createNetworkClient({
|
||||
chainId: customChainId,
|
||||
rpcUrl: customRpcUrl,
|
||||
type: 'custom',
|
||||
});
|
||||
process.env.IN_TEST = inTest;
|
||||
|
||||
const { networkMiddleware, blockTracker } = clientUnderTest;
|
||||
const { provider, blockTracker } = clientUnderTest;
|
||||
|
||||
const engine = new JsonRpcEngine();
|
||||
engine.push(networkMiddleware);
|
||||
const provider = providerFromEngine(engine);
|
||||
const ethQuery = new EthQuery(provider);
|
||||
|
||||
const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request);
|
||||
const makeRpcCallsInSeries = async (requests) => {
|
||||
const responses = [];
|
||||
@ -355,12 +434,7 @@ export async function withNetworkClient(
|
||||
}
|
||||
return responses;
|
||||
};
|
||||
// Faking timers ends up doing two things:
|
||||
// 1. Halting the block tracker (which depends on `setTimeout` to periodically
|
||||
// request the latest block) set up in `eth-json-rpc-middleware`
|
||||
// 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which also
|
||||
// depends on `setTimeout`)
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
||||
const client = {
|
||||
blockTracker,
|
||||
clock,
|
||||
|
@ -0,0 +1,968 @@
|
||||
/* eslint-disable jest/require-top-level-describe, jest/no-export */
|
||||
|
||||
import {
|
||||
waitForPromiseToBeFulfilledAfterRunningAllTimers,
|
||||
withMockedCommunications,
|
||||
withNetworkClient,
|
||||
} from './helpers';
|
||||
import {
|
||||
buildFetchFailedErrorMessage,
|
||||
buildInfuraClientRetriesExhaustedErrorMessage,
|
||||
buildJsonRpcEngineEmptyResponseErrorMessage,
|
||||
} from './shared-tests';
|
||||
|
||||
/**
|
||||
* Defines tests which exercise the behavior exhibited by an RPC method which is
|
||||
* assumed to not take a block parameter. Even if it does, the value of this
|
||||
* parameter will not be used in determining how to cache the method.
|
||||
*
|
||||
* @param method - The name of the RPC method under test.
|
||||
* @param additionalArgs - Additional arguments.
|
||||
* @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test.
|
||||
* @param additionalArgs.providerType - The type of provider being tested;
|
||||
* either `infura` or `custom` (default: "infura").
|
||||
*/
|
||||
export function testsForRpcMethodAssumingNoBlockParam(
|
||||
method,
|
||||
{ numberOfParameters, providerType },
|
||||
) {
|
||||
if (providerType !== 'infura' && providerType !== 'custom') {
|
||||
throw new Error(
|
||||
`providerType must be either "infura" or "custom", was "${providerType}" instead`,
|
||||
);
|
||||
}
|
||||
|
||||
it('does not hit the RPC endpoint more than once for identical requests', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = ['first result', 'second result'];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual([mockResults[0], mockResults[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
for (const paramIndex of [...Array(numberOfParameters).keys()]) {
|
||||
it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => {
|
||||
const firstMockParams = [
|
||||
...new Array(numberOfParameters).fill('some value'),
|
||||
];
|
||||
const secondMockParams = firstMockParams.slice();
|
||||
secondMockParams[paramIndex] = 'another value';
|
||||
const requests = [
|
||||
{
|
||||
method,
|
||||
params: firstMockParams,
|
||||
},
|
||||
{ method, params: secondMockParams },
|
||||
];
|
||||
const mockResults = ['some result', 'another result'];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual([mockResults[0], mockResults[1]]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = ['first result', 'second result'];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// Note that we have to mock these requests in a specific order. The
|
||||
// first block tracker request occurs because of the first RPC request.
|
||||
// The second block tracker request, however, does not occur because of
|
||||
// the second RPC request, but rather because we call `clock.runAll()`
|
||||
// below.
|
||||
comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' });
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' });
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
async (client) => {
|
||||
const firstResult = await client.makeRpcCall(requests[0]);
|
||||
// Proceed to the next iteration of the block tracker so that a new
|
||||
// block is fetched and the current block is updated.
|
||||
client.clock.runAll();
|
||||
const secondResult = await client.makeRpcCall(requests[1]);
|
||||
return [firstResult, secondResult];
|
||||
},
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
|
||||
for (const emptyValue of [null, undefined, '\u003cnil\u003e']) {
|
||||
it(`does not retry an empty response of "${emptyValue}"`, async () => {
|
||||
const request = { method };
|
||||
const mockResult = emptyValue;
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: { result: mockResult },
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => {
|
||||
const requests = [{ method }, { method }];
|
||||
const mockResults = [emptyValue, 'some result'];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests),
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual(mockResults);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => {
|
||||
const requests = [{ method }, { method }, { method }];
|
||||
const mockResults = ['first result', 'second result', 'third result'];
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request: requests[0],
|
||||
response: { result: mockResults[0] },
|
||||
delay: 100,
|
||||
});
|
||||
|
||||
comms.mockRpcCall({
|
||||
request: requests[1],
|
||||
response: { result: mockResults[1] },
|
||||
});
|
||||
|
||||
comms.mockRpcCall({
|
||||
request: requests[2],
|
||||
response: { result: mockResults[2] },
|
||||
});
|
||||
|
||||
const results = await withNetworkClient(
|
||||
{ providerType },
|
||||
async (client) => {
|
||||
const resultPromises = [
|
||||
client.makeRpcCall(requests[0]),
|
||||
client.makeRpcCall(requests[1]),
|
||||
client.makeRpcCall(requests[2]),
|
||||
];
|
||||
const firstResult = await resultPromises[0];
|
||||
// The inflight cache middleware uses setTimeout to run the handlers,
|
||||
// so run them now
|
||||
client.clock.runAll();
|
||||
const remainingResults = await Promise.all(resultPromises.slice(1));
|
||||
return [firstResult, ...remainingResults];
|
||||
},
|
||||
);
|
||||
|
||||
expect(results).toStrictEqual([
|
||||
mockResults[0],
|
||||
mockResults[0],
|
||||
mockResults[0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws a custom error if the request to the RPC endpoint returns a 405 response', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
httpStatus: 405,
|
||||
},
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
'The method does not exist / is not available',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// There is a difference in how we are testing the Infura middleware vs. the
|
||||
// custom RPC middleware (or, more specifically, the fetch middleware) because
|
||||
// of what both middleware treat as rate limiting errors. In this case, the
|
||||
// fetch middleware treats a 418 response from the RPC endpoint as such an
|
||||
// error, whereas to the Infura middleware, it is a 429 response.
|
||||
if (providerType === 'infura') {
|
||||
it('throws an undescriptive error if the request to the RPC endpoint returns a 418 response', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { id: 123, method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
httpStatus: 418,
|
||||
},
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
'{"id":123,"jsonrpc":"2.0"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
httpStatus: 429,
|
||||
},
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
'Request is being rate limited',
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('throws a custom error if the request to the RPC endpoint returns a 418 response', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
httpStatus: 418,
|
||||
},
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
'Request is being rate limited.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
httpStatus: 429,
|
||||
},
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
"Non-200 status code: '429'",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
id: 12345,
|
||||
jsonrpc: '2.0',
|
||||
error: 'some error',
|
||||
httpStatus: 420,
|
||||
},
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
const errorMessage =
|
||||
providerType === 'infura'
|
||||
? '{"id":12345,"jsonrpc":"2.0","error":"some error"}'
|
||||
: "Non-200 status code: '420'";
|
||||
await expect(promiseForResult).rejects.toThrow(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
[503, 504].forEach((httpStatus) => {
|
||||
it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
// Here we have the request fail for the first 4 tries, then succeed
|
||||
// on the 5th try.
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
error: 'Some error',
|
||||
httpStatus,
|
||||
},
|
||||
times: 4,
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
result: 'the result',
|
||||
httpStatus: 200,
|
||||
},
|
||||
});
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual('the result');
|
||||
});
|
||||
});
|
||||
|
||||
it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
error: 'Some error',
|
||||
httpStatus,
|
||||
},
|
||||
times: 5,
|
||||
});
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
const err =
|
||||
providerType === 'infura'
|
||||
? buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout')
|
||||
: buildJsonRpcEngineEmptyResponseErrorMessage(method);
|
||||
await expect(promiseForResult).rejects.toThrow(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
// Here we have the request fail for the first 4 tries, then succeed
|
||||
// on the 5th try.
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: 'ETIMEDOUT: Some message',
|
||||
times: 4,
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
result: 'the result',
|
||||
httpStatus: 200,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual('the result');
|
||||
});
|
||||
});
|
||||
|
||||
// Both the Infura and fetch middleware detect ETIMEDOUT errors and will
|
||||
// automatically retry the request to the RPC endpoint in question, but both
|
||||
// produce a different error if the number of retries is exhausted.
|
||||
if (providerType === 'infura') {
|
||||
it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'ETIMEDOUT: Some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
times: 5,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildInfuraClientRetriesExhaustedErrorMessage(errorMessage),
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'ETIMEDOUT: Some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
times: 5,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildJsonRpcEngineEmptyResponseErrorMessage(method),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// The Infura middleware treats a response that contains an ECONNRESET message
|
||||
// as an innocuous error that is likely to disappear on a retry. The custom
|
||||
// RPC middleware, on the other hand, does not specially handle this error.
|
||||
if (providerType === 'infura') {
|
||||
it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
// Here we have the request fail for the first 4 tries, then succeed
|
||||
// on the 5th try.
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: 'ECONNRESET: Some message',
|
||||
times: 4,
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
result: 'the result',
|
||||
httpStatus: 200,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual('the result');
|
||||
});
|
||||
});
|
||||
|
||||
it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'ECONNRESET: Some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
times: 5,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildInfuraClientRetriesExhaustedErrorMessage(errorMessage),
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => {
|
||||
const customRpcUrl = 'http://example.com';
|
||||
|
||||
await withMockedCommunications(
|
||||
{ providerType, customRpcUrl },
|
||||
async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'ECONNRESET: Some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType, customRpcUrl },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildFetchFailedErrorMessage(customRpcUrl, errorMessage),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Both the Infura and fetch middleware will attempt to parse the response
|
||||
// body as JSON, and if this step produces an error, both middleware will also
|
||||
// attempt to retry the request. However, this error handling code is slightly
|
||||
// different between the two. As the error in this case is a SyntaxError, the
|
||||
// Infura middleware will catch it immediately, whereas the custom RPC
|
||||
// middleware will catch it and re-throw a separate error, which it then
|
||||
// catches later.
|
||||
if (providerType === 'infura') {
|
||||
it('retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
// Here we have the request fail for the first 4 tries, then succeed
|
||||
// on the 5th try.
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: 'SyntaxError: Some message',
|
||||
times: 4,
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
result: 'the result',
|
||||
httpStatus: 200,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual('the result');
|
||||
});
|
||||
});
|
||||
|
||||
it('causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'SyntaxError: Some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
times: 5,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildInfuraClientRetriesExhaustedErrorMessage(errorMessage),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'failed to parse response body: some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType, infuraNetwork: comms.infuraNetwork },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage),
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => {
|
||||
const customRpcUrl = 'http://example.com';
|
||||
|
||||
await withMockedCommunications(
|
||||
{ providerType, customRpcUrl },
|
||||
async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'SyntaxError: Some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType, customRpcUrl },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildFetchFailedErrorMessage(customRpcUrl, errorMessage),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
// Here we have the request fail for the first 4 tries, then succeed
|
||||
// on the 5th try.
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: 'failed to parse response body: some message',
|
||||
times: 4,
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
result: 'the result',
|
||||
httpStatus: 200,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual('the result');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'failed to parse response body: some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
times: 5,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildJsonRpcEngineEmptyResponseErrorMessage(method),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Only the custom RPC middleware will detect a "Failed to fetch" error and
|
||||
// attempt to retry the request to the RPC endpoint; the Infura middleware
|
||||
// does not.
|
||||
if (providerType === 'infura') {
|
||||
it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'Failed to fetch: some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType, infuraNetwork: comms.infuraNetwork },
|
||||
async ({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage),
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
// Here we have the request fail for the first 4 tries, then succeed
|
||||
// on the 5th try.
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: 'Failed to fetch: some message',
|
||||
times: 4,
|
||||
});
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: {
|
||||
result: 'the result',
|
||||
httpStatus: 200,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual('the result');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => {
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
const request = { method };
|
||||
const errorMessage = 'Failed to fetch: some message';
|
||||
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
error: errorMessage,
|
||||
times: 5,
|
||||
});
|
||||
const promiseForResult = withNetworkClient(
|
||||
{ providerType },
|
||||
async ({ makeRpcCall, clock }) => {
|
||||
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
||||
makeRpcCall(request),
|
||||
clock,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(promiseForResult).rejects.toThrow(
|
||||
buildJsonRpcEngineEmptyResponseErrorMessage(method),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* eslint-disable jest/require-top-level-describe, jest/no-export */
|
||||
|
||||
import { fill } from 'lodash';
|
||||
import { withMockedCommunications, withNetworkClient } from './helpers';
|
||||
|
||||
/**
|
||||
* Defines tests which exercise the behavior exhibited by an RPC method that
|
||||
* is not handled specially by the network client middleware.
|
||||
*
|
||||
* @param method - The name of the RPC method under test.
|
||||
* @param additionalArgs - Additional arguments.
|
||||
* @param additionalArgs.providerType - The type of provider being tested;
|
||||
* either `infura` or `custom`.
|
||||
* @param additionalArgs.numberOfParameters - The number of parameters that this
|
||||
* RPC method takes.
|
||||
*/
|
||||
export function testsForRpcMethodNotHandledByMiddleware(
|
||||
method,
|
||||
{ providerType, numberOfParameters },
|
||||
) {
|
||||
if (providerType !== 'infura' && providerType !== 'custom') {
|
||||
throw new Error(
|
||||
`providerType must be either "infura" or "custom", was "${providerType}" instead`,
|
||||
);
|
||||
}
|
||||
|
||||
it('attempts to pass the request off to the RPC endpoint', async () => {
|
||||
const request = {
|
||||
method,
|
||||
params: fill(Array(numberOfParameters), 'some value'),
|
||||
};
|
||||
const expectedResult = 'the result';
|
||||
|
||||
await withMockedCommunications({ providerType }, async (comms) => {
|
||||
// The first time a block-cacheable request is made, the latest block
|
||||
// number is retrieved through the block tracker first. It doesn't
|
||||
// matter what this is — it's just used as a cache key.
|
||||
comms.mockNextBlockTrackerRequest();
|
||||
comms.mockRpcCall({
|
||||
request,
|
||||
response: { result: expectedResult },
|
||||
});
|
||||
const actualResult = await withNetworkClient(
|
||||
{ providerType },
|
||||
({ makeRpcCall }) => makeRpcCall(request),
|
||||
);
|
||||
|
||||
expect(actualResult).toStrictEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
}
|
@ -3,7 +3,6 @@ import { normalize as normalizeAddress } from 'eth-sig-util';
|
||||
import { IPFS_DEFAULT_GATEWAY_URL } from '../../../shared/constants/network';
|
||||
import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets';
|
||||
import { ThemeType } from '../../../shared/constants/preferences';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
export default class PreferencesController {
|
||||
/**
|
||||
@ -70,10 +69,10 @@ export default class PreferencesController {
|
||||
...opts.initState,
|
||||
};
|
||||
|
||||
this.network = opts.network;
|
||||
this._onInfuraIsBlocked = opts.onInfuraIsBlocked;
|
||||
this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked;
|
||||
this.store = new ObservableStore(initState);
|
||||
this.store.setMaxListeners(13);
|
||||
this.openPopup = opts.openPopup;
|
||||
this.tokenListController = opts.tokenListController;
|
||||
|
||||
this._subscribeToInfuraAvailability();
|
||||
@ -511,10 +510,11 @@ export default class PreferencesController {
|
||||
//
|
||||
|
||||
_subscribeToInfuraAvailability() {
|
||||
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
|
||||
this._onInfuraIsBlocked(() => {
|
||||
this._setInfuraBlocked(true);
|
||||
});
|
||||
this.network.on(NETWORK_EVENTS.INFURA_IS_UNBLOCKED, () => {
|
||||
|
||||
this._onInfuraIsUnblocked(() => {
|
||||
this._setInfuraBlocked(false);
|
||||
});
|
||||
}
|
||||
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
587
app/scripts/controllers/sign.test.ts
Normal file
@ -0,0 +1,587 @@
|
||||
import {
|
||||
MessageManager,
|
||||
PersonalMessageManager,
|
||||
TypedMessageManager,
|
||||
} from '@metamask/message-manager';
|
||||
import {
|
||||
AbstractMessage,
|
||||
OriginalRequest,
|
||||
} from '@metamask/message-manager/dist/AbstractMessageManager';
|
||||
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
|
||||
import SignController, {
|
||||
SignControllerMessenger,
|
||||
SignControllerOptions,
|
||||
} from './sign';
|
||||
|
||||
jest.mock('@metamask/message-manager', () => ({
|
||||
MessageManager: jest.fn(),
|
||||
PersonalMessageManager: jest.fn(),
|
||||
TypedMessageManager: jest.fn(),
|
||||
}));
|
||||
|
||||
const messageIdMock = '123';
|
||||
const messageIdMock2 = '456';
|
||||
const versionMock = '1';
|
||||
const signatureMock = '0xAABBCC';
|
||||
const stateMock = { test: 123 };
|
||||
const securityProviderResponseMock = { test2: 345 };
|
||||
|
||||
const messageParamsMock = {
|
||||
from: '0x123',
|
||||
origin: 'http://test.com',
|
||||
data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
|
||||
metamaskId: messageIdMock,
|
||||
version: 'V1',
|
||||
};
|
||||
|
||||
const messageParamsMock2 = {
|
||||
from: '0x124',
|
||||
origin: 'http://test4.com',
|
||||
data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA',
|
||||
metamaskId: messageIdMock,
|
||||
};
|
||||
|
||||
const messageMock = {
|
||||
id: messageIdMock,
|
||||
time: 123,
|
||||
status: 'unapproved',
|
||||
type: 'testType',
|
||||
rawSig: undefined,
|
||||
} as any as AbstractMessage;
|
||||
|
||||
const coreMessageMock = {
|
||||
...messageMock,
|
||||
messageParams: messageParamsMock,
|
||||
};
|
||||
|
||||
const stateMessageMock = {
|
||||
...messageMock,
|
||||
msgParams: messageParamsMock,
|
||||
securityProviderResponse: securityProviderResponseMock,
|
||||
};
|
||||
|
||||
const requestMock = {
|
||||
origin: 'http://test2.com',
|
||||
} as OriginalRequest;
|
||||
|
||||
const createMessengerMock = () =>
|
||||
({
|
||||
registerActionHandler: jest.fn(),
|
||||
publish: jest.fn(),
|
||||
call: jest.fn(),
|
||||
} as any as jest.Mocked<SignControllerMessenger>);
|
||||
|
||||
const createMessageManagerMock = <T>() =>
|
||||
({
|
||||
getUnapprovedMessages: jest.fn(),
|
||||
getUnapprovedMessagesCount: jest.fn(),
|
||||
addUnapprovedMessageAsync: jest.fn(),
|
||||
approveMessage: jest.fn(),
|
||||
setMessageStatusSigned: jest.fn(),
|
||||
rejectMessage: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
update: jest.fn(),
|
||||
hub: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
} as any as jest.Mocked<T>);
|
||||
|
||||
const createPreferencesControllerMock = () => ({
|
||||
store: {
|
||||
getState: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
const createKeyringControllerMock = () => ({
|
||||
signMessage: jest.fn(),
|
||||
signPersonalMessage: jest.fn(),
|
||||
signTypedMessage: jest.fn(),
|
||||
});
|
||||
|
||||
describe('SignController', () => {
|
||||
let signController: SignController;
|
||||
|
||||
const messageManagerConstructorMock = MessageManager as jest.MockedClass<
|
||||
typeof MessageManager
|
||||
>;
|
||||
const personalMessageManagerConstructorMock =
|
||||
PersonalMessageManager as jest.MockedClass<typeof PersonalMessageManager>;
|
||||
const typedMessageManagerConstructorMock =
|
||||
TypedMessageManager as jest.MockedClass<typeof TypedMessageManager>;
|
||||
const messageManagerMock = createMessageManagerMock<MessageManager>();
|
||||
const personalMessageManagerMock =
|
||||
createMessageManagerMock<PersonalMessageManager>();
|
||||
const typedMessageManagerMock =
|
||||
createMessageManagerMock<TypedMessageManager>();
|
||||
const messengerMock = createMessengerMock();
|
||||
const preferencesControllerMock = createPreferencesControllerMock();
|
||||
const keyringControllerMock = createKeyringControllerMock();
|
||||
const getStateMock = jest.fn();
|
||||
const securityProviderRequestMock = jest.fn();
|
||||
const metricsEventMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
messageManagerConstructorMock.mockReturnValue(messageManagerMock);
|
||||
personalMessageManagerConstructorMock.mockReturnValue(
|
||||
personalMessageManagerMock,
|
||||
);
|
||||
|
||||
typedMessageManagerConstructorMock.mockReturnValue(typedMessageManagerMock);
|
||||
|
||||
preferencesControllerMock.store.getState.mockReturnValue({
|
||||
disabledRpcMethodPreferences: { eth_sign: true },
|
||||
});
|
||||
|
||||
signController = new SignController({
|
||||
messenger: messengerMock as any,
|
||||
preferencesController: preferencesControllerMock as any,
|
||||
keyringController: keyringControllerMock as any,
|
||||
getState: getStateMock as any,
|
||||
securityProviderRequest: securityProviderRequestMock as any,
|
||||
metricsEvent: metricsEventMock as any,
|
||||
} as SignControllerOptions);
|
||||
});
|
||||
|
||||
describe('unapprovedMsgCount', () => {
|
||||
it('returns value from message manager getter', () => {
|
||||
messageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(10);
|
||||
expect(signController.unapprovedMsgCount).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unapprovedPersonalMessagesCount', () => {
|
||||
it('returns value from personal message manager getter', () => {
|
||||
personalMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(
|
||||
11,
|
||||
);
|
||||
expect(signController.unapprovedPersonalMessagesCount).toBe(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unapprovedTypedMessagesCount', () => {
|
||||
it('returns value from typed message manager getter', () => {
|
||||
typedMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(
|
||||
12,
|
||||
);
|
||||
expect(signController.unapprovedTypedMessagesCount).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetState', () => {
|
||||
it('sets state to initial state', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
signController.update(() => ({
|
||||
unapprovedMsgs: { [messageIdMock]: messageMock } as any,
|
||||
unapprovedPersonalMsgs: { [messageIdMock]: messageMock } as any,
|
||||
unapprovedTypedMessages: { [messageIdMock]: messageMock } as any,
|
||||
unapprovedMsgCount: 1,
|
||||
unapprovedPersonalMsgCount: 2,
|
||||
unapprovedTypedMessagesCount: 3,
|
||||
}));
|
||||
|
||||
signController.resetState();
|
||||
|
||||
expect(signController.state).toEqual({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedPersonalMsgs: {},
|
||||
unapprovedTypedMessages: {},
|
||||
unapprovedMsgCount: 0,
|
||||
unapprovedPersonalMsgCount: 0,
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectUnapproved', () => {
|
||||
beforeEach(() => {
|
||||
const messages = {
|
||||
[messageIdMock]: messageMock,
|
||||
[messageIdMock2]: messageMock,
|
||||
};
|
||||
|
||||
messageManagerMock.getUnapprovedMessages.mockReturnValueOnce(
|
||||
messages as any,
|
||||
);
|
||||
personalMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce(
|
||||
messages as any,
|
||||
);
|
||||
typedMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce(
|
||||
messages as any,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
signController.update(() => ({
|
||||
unapprovedMsgs: messages as any,
|
||||
unapprovedPersonalMsgs: messages as any,
|
||||
unapprovedTypedMessages: messages as any,
|
||||
}));
|
||||
});
|
||||
|
||||
it('rejects all messages in all message managers', () => {
|
||||
signController.rejectUnapproved('Test Reason');
|
||||
|
||||
expect(messageManagerMock.rejectMessage).toHaveBeenCalledTimes(2);
|
||||
expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith(
|
||||
messageIdMock,
|
||||
);
|
||||
expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith(
|
||||
messageIdMock2,
|
||||
);
|
||||
|
||||
expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2);
|
||||
expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
|
||||
messageIdMock,
|
||||
);
|
||||
expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
|
||||
messageIdMock2,
|
||||
);
|
||||
|
||||
expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2);
|
||||
expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
|
||||
messageIdMock,
|
||||
);
|
||||
expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
|
||||
messageIdMock2,
|
||||
);
|
||||
});
|
||||
|
||||
it('fires metrics event with reject reason', () => {
|
||||
signController.rejectUnapproved('Test Reason');
|
||||
|
||||
expect(metricsEventMock).toHaveBeenCalledTimes(6);
|
||||
expect(metricsEventMock).toHaveBeenLastCalledWith({
|
||||
event: 'Test Reason',
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
properties: {
|
||||
action: 'Sign Request',
|
||||
type: messageMock.type,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearUnapproved', () => {
|
||||
it('resets state in all message managers', () => {
|
||||
signController.clearUnapproved();
|
||||
|
||||
const defaultState = {
|
||||
unapprovedMessages: {},
|
||||
unapprovedMessagesCount: 0,
|
||||
};
|
||||
|
||||
expect(messageManagerMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(messageManagerMock.update).toHaveBeenCalledWith(defaultState);
|
||||
|
||||
expect(personalMessageManagerMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(personalMessageManagerMock.update).toHaveBeenCalledWith(
|
||||
defaultState,
|
||||
);
|
||||
|
||||
expect(typedMessageManagerMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(typedMessageManagerMock.update).toHaveBeenCalledWith(defaultState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newUnsignedMessage', () => {
|
||||
it('throws if eth_sign disabled in preferences', async () => {
|
||||
preferencesControllerMock.store.getState.mockReturnValueOnce({
|
||||
disabledRpcMethodPreferences: { eth_sign: false },
|
||||
});
|
||||
|
||||
await expect(
|
||||
signController.newUnsignedMessage(messageParamsMock, requestMock),
|
||||
).rejects.toThrowError(
|
||||
'eth_sign has been disabled. You must enable it in the advanced settings',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if data has wrong length', async () => {
|
||||
await expect(
|
||||
signController.newUnsignedMessage(
|
||||
{ ...messageParamsMock, data: '0xFF' },
|
||||
requestMock,
|
||||
),
|
||||
).rejects.toThrowError('eth_sign requires 32 byte message hash');
|
||||
});
|
||||
|
||||
it('adds message to message manager', async () => {
|
||||
await signController.newUnsignedMessage(messageParamsMock, requestMock);
|
||||
|
||||
expect(
|
||||
messageManagerMock.addUnapprovedMessageAsync,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(messageManagerMock.addUnapprovedMessageAsync).toHaveBeenCalledWith(
|
||||
messageParamsMock,
|
||||
requestMock,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newUnsignedPersonalMessage', () => {
|
||||
it('adds message to personal message manager', async () => {
|
||||
await signController.newUnsignedPersonalMessage(
|
||||
messageParamsMock,
|
||||
requestMock,
|
||||
);
|
||||
|
||||
expect(
|
||||
personalMessageManagerMock.addUnapprovedMessageAsync,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(
|
||||
personalMessageManagerMock.addUnapprovedMessageAsync,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining(messageParamsMock),
|
||||
requestMock,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newUnsignedTypedMessage', () => {
|
||||
it('adds message to typed message manager', async () => {
|
||||
signController.newUnsignedTypedMessage(
|
||||
messageParamsMock,
|
||||
requestMock,
|
||||
versionMock,
|
||||
);
|
||||
|
||||
expect(
|
||||
typedMessageManagerMock.addUnapprovedMessageAsync,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
typedMessageManagerMock.addUnapprovedMessageAsync,
|
||||
).toHaveBeenCalledWith(messageParamsMock, versionMock, requestMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['signMessage', messageManagerMock],
|
||||
['signPersonalMessage', personalMessageManagerMock],
|
||||
['signTypedMessage', typedMessageManagerMock],
|
||||
])('%s', (signMethodName, messageManager) => {
|
||||
beforeEach(() => {
|
||||
messageManager.approveMessage.mockResolvedValueOnce(messageParamsMock2);
|
||||
|
||||
keyringControllerMock[signMethodName].mockResolvedValueOnce(
|
||||
signatureMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('approves message and signs', async () => {
|
||||
await signController[signMethodName](messageParamsMock);
|
||||
|
||||
const keyringControllerExtraArgs =
|
||||
signMethodName === 'signTypedMessage'
|
||||
? [{ version: messageParamsMock.version }]
|
||||
: [];
|
||||
|
||||
expect(keyringControllerMock[signMethodName]).toHaveBeenCalledTimes(1);
|
||||
expect(keyringControllerMock[signMethodName]).toHaveBeenCalledWith(
|
||||
messageParamsMock2,
|
||||
...keyringControllerExtraArgs,
|
||||
);
|
||||
|
||||
expect(messageManager.setMessageStatusSigned).toHaveBeenCalledTimes(1);
|
||||
expect(messageManager.setMessageStatusSigned).toHaveBeenCalledWith(
|
||||
messageParamsMock2.metamaskId,
|
||||
signatureMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns current state', async () => {
|
||||
getStateMock.mockReturnValueOnce(stateMock);
|
||||
expect(await signController[signMethodName](messageParamsMock)).toEqual(
|
||||
stateMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts approval', async () => {
|
||||
await signController[signMethodName](messageParamsMock);
|
||||
|
||||
expect(messengerMock.call).toHaveBeenCalledTimes(1);
|
||||
expect(messengerMock.call).toHaveBeenCalledWith(
|
||||
'ApprovalController:acceptRequest',
|
||||
messageParamsMock.metamaskId,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw if accepting approval throws', async () => {
|
||||
messengerMock.call.mockImplementation(() => {
|
||||
throw new Error('Test Error');
|
||||
});
|
||||
|
||||
await signController[signMethodName](messageParamsMock);
|
||||
});
|
||||
|
||||
it('rejects message on error', async () => {
|
||||
keyringControllerMock[signMethodName].mockReset();
|
||||
keyringControllerMock[signMethodName].mockRejectedValue(
|
||||
new Error('Test Error'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
signController[signMethodName](messageParamsMock),
|
||||
).rejects.toThrow('Test Error');
|
||||
|
||||
expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1);
|
||||
expect(messageManager.rejectMessage).toHaveBeenCalledWith(
|
||||
messageParamsMock.metamaskId,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects approval on error', async () => {
|
||||
keyringControllerMock[signMethodName].mockReset();
|
||||
keyringControllerMock[signMethodName].mockRejectedValue(
|
||||
new Error('Test Error'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
signController[signMethodName](messageParamsMock),
|
||||
).rejects.toThrow('Test Error');
|
||||
|
||||
expect(messengerMock.call).toHaveBeenCalledTimes(1);
|
||||
expect(messengerMock.call).toHaveBeenCalledWith(
|
||||
'ApprovalController:rejectRequest',
|
||||
messageParamsMock.metamaskId,
|
||||
'Cancel',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['cancelMessage', messageManagerMock],
|
||||
['cancelPersonalMessage', personalMessageManagerMock],
|
||||
['cancelTypedMessage', typedMessageManagerMock],
|
||||
])('%s', (cancelMethodName, messageManager) => {
|
||||
it('rejects message using message manager', async () => {
|
||||
signController[cancelMethodName](messageIdMock);
|
||||
|
||||
expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1);
|
||||
expect(messageManager.rejectMessage).toHaveBeenCalledWith(
|
||||
messageParamsMock.metamaskId,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects approval using approval controller', async () => {
|
||||
signController[cancelMethodName](messageIdMock);
|
||||
|
||||
expect(messengerMock.call).toHaveBeenCalledTimes(1);
|
||||
expect(messengerMock.call).toHaveBeenCalledWith(
|
||||
'ApprovalController:rejectRequest',
|
||||
messageParamsMock.metamaskId,
|
||||
'Cancel',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw if rejecting approval throws', async () => {
|
||||
messengerMock.call.mockImplementation(() => {
|
||||
throw new Error('Test Error');
|
||||
});
|
||||
|
||||
await signController[cancelMethodName](messageParamsMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message manager events', () => {
|
||||
it.each([
|
||||
['message manager', messageManagerMock],
|
||||
['personal message manager', personalMessageManagerMock],
|
||||
['typed message manager', typedMessageManagerMock],
|
||||
])('bubbles update badge event from %s', (_, messageManager) => {
|
||||
const mockListener = jest.fn();
|
||||
|
||||
signController.hub.on('updateBadge', mockListener);
|
||||
messageManager.hub.on.mock.calls[0][1]();
|
||||
|
||||
expect(mockListener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['message manager', messageManagerMock, 'eth_sign'],
|
||||
['personal message manager', personalMessageManagerMock, 'personal_sign'],
|
||||
['typed message manager', typedMessageManagerMock, 'eth_signTypedData'],
|
||||
])(
|
||||
'requires approval on unapproved message event from %s',
|
||||
(_, messageManager, methodName) => {
|
||||
messengerMock.call.mockResolvedValueOnce({});
|
||||
|
||||
messageManager.hub.on.mock.calls[1][1](messageParamsMock);
|
||||
|
||||
expect(messengerMock.call).toHaveBeenCalledTimes(1);
|
||||
expect(messengerMock.call).toHaveBeenCalledWith(
|
||||
'ApprovalController:addRequest',
|
||||
{
|
||||
id: messageIdMock,
|
||||
origin: messageParamsMock.origin,
|
||||
type: methodName,
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('updates state on message manager state change', async () => {
|
||||
securityProviderRequestMock.mockResolvedValue(
|
||||
securityProviderResponseMock,
|
||||
);
|
||||
|
||||
await messageManagerMock.subscribe.mock.calls[0][0]({
|
||||
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
|
||||
unapprovedMessagesCount: 3,
|
||||
});
|
||||
|
||||
expect(await signController.state).toEqual({
|
||||
unapprovedMsgs: { [messageIdMock]: stateMessageMock as any },
|
||||
unapprovedPersonalMsgs: {},
|
||||
unapprovedTypedMessages: {},
|
||||
unapprovedMsgCount: 3,
|
||||
unapprovedPersonalMsgCount: 0,
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates state on personal message manager state change', async () => {
|
||||
securityProviderRequestMock.mockResolvedValue(
|
||||
securityProviderResponseMock,
|
||||
);
|
||||
|
||||
await personalMessageManagerMock.subscribe.mock.calls[0][0]({
|
||||
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
|
||||
unapprovedMessagesCount: 4,
|
||||
});
|
||||
|
||||
expect(await signController.state).toEqual({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedPersonalMsgs: { [messageIdMock]: stateMessageMock as any },
|
||||
unapprovedTypedMessages: {},
|
||||
unapprovedMsgCount: 0,
|
||||
unapprovedPersonalMsgCount: 4,
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates state on typed message manager state change', async () => {
|
||||
securityProviderRequestMock.mockResolvedValue(
|
||||
securityProviderResponseMock,
|
||||
);
|
||||
|
||||
await typedMessageManagerMock.subscribe.mock.calls[0][0]({
|
||||
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
|
||||
unapprovedMessagesCount: 5,
|
||||
});
|
||||
|
||||
expect(await signController.state).toEqual({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedPersonalMsgs: {},
|
||||
unapprovedTypedMessages: { [messageIdMock]: stateMessageMock as any },
|
||||
unapprovedMsgCount: 0,
|
||||
unapprovedPersonalMsgCount: 0,
|
||||
unapprovedTypedMessagesCount: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
662
app/scripts/controllers/sign.ts
Normal file
@ -0,0 +1,662 @@
|
||||
import EventEmitter from 'events';
|
||||
import log from 'loglevel';
|
||||
import {
|
||||
MessageManager,
|
||||
MessageParams,
|
||||
MessageParamsMetamask,
|
||||
PersonalMessageManager,
|
||||
PersonalMessageParams,
|
||||
PersonalMessageParamsMetamask,
|
||||
TypedMessageManager,
|
||||
TypedMessageParams,
|
||||
TypedMessageParamsMetamask,
|
||||
} from '@metamask/message-manager';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { bufferToHex } from 'ethereumjs-util';
|
||||
import { KeyringController } from '@metamask/eth-keyring-controller';
|
||||
import {
|
||||
AbstractMessageManager,
|
||||
AbstractMessage,
|
||||
MessageManagerState,
|
||||
AbstractMessageParams,
|
||||
AbstractMessageParamsMetamask,
|
||||
OriginalRequest,
|
||||
} from '@metamask/message-manager/dist/AbstractMessageManager';
|
||||
import {
|
||||
BaseControllerV2,
|
||||
RestrictedControllerMessenger,
|
||||
} from '@metamask/base-controller';
|
||||
import { Patch } from 'immer';
|
||||
import {
|
||||
AcceptRequest,
|
||||
AddApprovalRequest,
|
||||
RejectRequest,
|
||||
} from '@metamask/approval-controller';
|
||||
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import PreferencesController from './preferences';
|
||||
|
||||
const controllerName = 'SignController';
|
||||
const methodNameSign = MESSAGE_TYPE.ETH_SIGN;
|
||||
const methodNamePersonalSign = MESSAGE_TYPE.PERSONAL_SIGN;
|
||||
const methodNameTypedSign = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA;
|
||||
|
||||
const stateMetadata = {
|
||||
unapprovedMsgs: { persist: false, anonymous: false },
|
||||
unapprovedPersonalMsgs: { persist: false, anonymous: false },
|
||||
unapprovedTypedMessages: { persist: false, anonymous: false },
|
||||
unapprovedMsgCount: { persist: false, anonymous: false },
|
||||
unapprovedPersonalMsgCount: { persist: false, anonymous: false },
|
||||
unapprovedTypedMessagesCount: { persist: false, anonymous: false },
|
||||
};
|
||||
|
||||
const getDefaultState = () => ({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedPersonalMsgs: {},
|
||||
unapprovedTypedMessages: {},
|
||||
unapprovedMsgCount: 0,
|
||||
unapprovedPersonalMsgCount: 0,
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
});
|
||||
|
||||
export type CoreMessage = AbstractMessage & {
|
||||
messageParams: AbstractMessageParams;
|
||||
};
|
||||
|
||||
export type StateMessage = Required<AbstractMessage> & {
|
||||
msgParams: Required<AbstractMessageParams>;
|
||||
securityProviderResponse: any;
|
||||
};
|
||||
|
||||
export type SignControllerState = {
|
||||
unapprovedMsgs: Record<string, StateMessage>;
|
||||
unapprovedPersonalMsgs: Record<string, StateMessage>;
|
||||
unapprovedTypedMessages: Record<string, StateMessage>;
|
||||
unapprovedMsgCount: number;
|
||||
unapprovedPersonalMsgCount: number;
|
||||
unapprovedTypedMessagesCount: number;
|
||||
};
|
||||
|
||||
export type GetSignState = {
|
||||
type: `${typeof controllerName}:getState`;
|
||||
handler: () => SignControllerState;
|
||||
};
|
||||
|
||||
export type SignStateChange = {
|
||||
type: `${typeof controllerName}:stateChange`;
|
||||
payload: [SignControllerState, Patch[]];
|
||||
};
|
||||
|
||||
export type SignControllerActions = GetSignState;
|
||||
|
||||
export type SignControllerEvents = SignStateChange;
|
||||
|
||||
type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest;
|
||||
|
||||
export type SignControllerMessenger = RestrictedControllerMessenger<
|
||||
typeof controllerName,
|
||||
SignControllerActions | AllowedActions,
|
||||
SignControllerEvents,
|
||||
AllowedActions['type'],
|
||||
never
|
||||
>;
|
||||
|
||||
export type SignControllerOptions = {
|
||||
messenger: SignControllerMessenger;
|
||||
keyringController: KeyringController;
|
||||
preferencesController: PreferencesController;
|
||||
sendUpdate: () => void;
|
||||
getState: () => any;
|
||||
metricsEvent: (payload: any, options?: any) => void;
|
||||
securityProviderRequest: (
|
||||
requestData: any,
|
||||
methodName: string,
|
||||
) => Promise<any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller for creating signing requests requiring user approval.
|
||||
*/
|
||||
export default class SignController extends BaseControllerV2<
|
||||
typeof controllerName,
|
||||
SignControllerState,
|
||||
SignControllerMessenger
|
||||
> {
|
||||
hub: EventEmitter;
|
||||
|
||||
private _keyringController: KeyringController;
|
||||
|
||||
private _preferencesController: PreferencesController;
|
||||
|
||||
private _getState: () => any;
|
||||
|
||||
private _messageManager: MessageManager;
|
||||
|
||||
private _personalMessageManager: PersonalMessageManager;
|
||||
|
||||
private _typedMessageManager: TypedMessageManager;
|
||||
|
||||
private _messageManagers: AbstractMessageManager<
|
||||
AbstractMessage,
|
||||
AbstractMessageParams,
|
||||
AbstractMessageParamsMetamask
|
||||
>[];
|
||||
|
||||
private _metricsEvent: (payload: any, options?: any) => void;
|
||||
|
||||
private _securityProviderRequest: (
|
||||
requestData: any,
|
||||
methodName: string,
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Construct a Sign controller.
|
||||
*
|
||||
* @param options - The controller options.
|
||||
* @param options.messenger - The restricted controller messenger for the sign controller.
|
||||
* @param options.keyringController - An instance of a keyring controller used to perform the signing operations.
|
||||
* @param options.preferencesController - An instance of a preferences controller to limit operations based on user configuration.
|
||||
* @param options.getState - Callback to retrieve all user state.
|
||||
* @param options.metricsEvent - A function for emitting a metric event.
|
||||
* @param options.securityProviderRequest - A function for verifying a message, whether it is malicious or not.
|
||||
*/
|
||||
constructor({
|
||||
messenger,
|
||||
keyringController,
|
||||
preferencesController,
|
||||
getState,
|
||||
metricsEvent,
|
||||
securityProviderRequest,
|
||||
}: SignControllerOptions) {
|
||||
super({
|
||||
name: controllerName,
|
||||
metadata: stateMetadata,
|
||||
messenger,
|
||||
state: getDefaultState(),
|
||||
});
|
||||
|
||||
this._keyringController = keyringController;
|
||||
this._preferencesController = preferencesController;
|
||||
this._getState = getState;
|
||||
this._metricsEvent = metricsEvent;
|
||||
this._securityProviderRequest = securityProviderRequest;
|
||||
|
||||
this.hub = new EventEmitter();
|
||||
this._messageManager = new MessageManager();
|
||||
this._personalMessageManager = new PersonalMessageManager();
|
||||
this._typedMessageManager = new TypedMessageManager();
|
||||
|
||||
this._messageManagers = [
|
||||
this._messageManager,
|
||||
this._personalMessageManager,
|
||||
this._typedMessageManager,
|
||||
];
|
||||
|
||||
const methodNames = [
|
||||
methodNameSign,
|
||||
methodNamePersonalSign,
|
||||
methodNameTypedSign,
|
||||
];
|
||||
|
||||
this._messageManagers.forEach((messageManager, index) => {
|
||||
this._bubbleEvents(messageManager);
|
||||
|
||||
messageManager.hub.on(
|
||||
'unapprovedMessage',
|
||||
(msgParams: AbstractMessageParamsMetamask) => {
|
||||
this._requestApproval(msgParams, methodNames[index]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this._subscribeToMessageState(
|
||||
this._messageManager,
|
||||
(state, newMessages, messageCount) => {
|
||||
state.unapprovedMsgs = newMessages;
|
||||
state.unapprovedMsgCount = messageCount;
|
||||
},
|
||||
);
|
||||
|
||||
this._subscribeToMessageState(
|
||||
this._personalMessageManager,
|
||||
(state, newMessages, messageCount) => {
|
||||
state.unapprovedPersonalMsgs = newMessages;
|
||||
state.unapprovedPersonalMsgCount = messageCount;
|
||||
},
|
||||
);
|
||||
|
||||
this._subscribeToMessageState(
|
||||
this._typedMessageManager,
|
||||
(state, newMessages, messageCount) => {
|
||||
state.unapprovedTypedMessages = newMessages;
|
||||
state.unapprovedTypedMessagesCount = messageCount;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the number of 'unapproved' Messages in this.messages
|
||||
*
|
||||
* @returns The number of 'unapproved' Messages in this.messages
|
||||
*/
|
||||
get unapprovedMsgCount(): number {
|
||||
return this._messageManager.getUnapprovedMessagesCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the number of 'unapproved' PersonalMessages in this.messages
|
||||
*
|
||||
* @returns The number of 'unapproved' PersonalMessages in this.messages
|
||||
*/
|
||||
get unapprovedPersonalMessagesCount(): number {
|
||||
return this._personalMessageManager.getUnapprovedMessagesCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the number of 'unapproved' TypedMessages in this.messages
|
||||
*
|
||||
* @returns The number of 'unapproved' TypedMessages in this.messages
|
||||
*/
|
||||
get unapprovedTypedMessagesCount(): number {
|
||||
return this._typedMessageManager.getUnapprovedMessagesCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the controller state to the initial state.
|
||||
*/
|
||||
resetState() {
|
||||
this.update(() => getDefaultState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a Dapp uses the eth_sign method, to request user approval.
|
||||
* eth_sign is a pure signature of arbitrary data. It is on a deprecation
|
||||
* path, since this data can be a transaction, or can leak private key
|
||||
* information.
|
||||
*
|
||||
* @param msgParams - The params passed to eth_sign.
|
||||
* @param [req] - The original request, containing the origin.
|
||||
*/
|
||||
async newUnsignedMessage(
|
||||
msgParams: MessageParams,
|
||||
req: OriginalRequest,
|
||||
): Promise<string> {
|
||||
const {
|
||||
// eslint-disable-next-line camelcase
|
||||
disabledRpcMethodPreferences: { eth_sign },
|
||||
} = this._preferencesController.store.getState() as any;
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
if (!eth_sign) {
|
||||
throw ethErrors.rpc.methodNotFound(
|
||||
'eth_sign has been disabled. You must enable it in the advanced settings',
|
||||
);
|
||||
}
|
||||
|
||||
const data = this._normalizeMsgData(msgParams.data);
|
||||
|
||||
// 64 hex + "0x" at the beginning
|
||||
// This is needed because Ethereum's EcSign works only on 32 byte numbers
|
||||
// For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607
|
||||
if (data.length !== 66 && data.length !== 67) {
|
||||
throw ethErrors.rpc.invalidParams(
|
||||
'eth_sign requires 32 byte message hash',
|
||||
);
|
||||
}
|
||||
|
||||
return this._messageManager.addUnapprovedMessageAsync(msgParams, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a dapp uses the personal_sign method.
|
||||
* This is identical to the Geth eth_sign method, and may eventually replace
|
||||
* eth_sign.
|
||||
*
|
||||
* We currently define our eth_sign and personal_sign mostly for legacy Dapps.
|
||||
*
|
||||
* @param msgParams - The params of the message to sign & return to the Dapp.
|
||||
* @param req - The original request, containing the origin.
|
||||
*/
|
||||
async newUnsignedPersonalMessage(
|
||||
msgParams: PersonalMessageParams,
|
||||
req: OriginalRequest,
|
||||
): Promise<string> {
|
||||
return this._personalMessageManager.addUnapprovedMessageAsync(
|
||||
msgParams,
|
||||
req,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a dapp uses the eth_signTypedData method, per EIP 712.
|
||||
*
|
||||
* @param msgParams - The params passed to eth_signTypedData.
|
||||
* @param req - The original request, containing the origin.
|
||||
* @param version
|
||||
*/
|
||||
async newUnsignedTypedMessage(
|
||||
msgParams: TypedMessageParams,
|
||||
req: OriginalRequest,
|
||||
version: string,
|
||||
): Promise<string> {
|
||||
return this._typedMessageManager.addUnapprovedMessageAsync(
|
||||
msgParams,
|
||||
version,
|
||||
req,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signifies user intent to complete an eth_sign method.
|
||||
*
|
||||
* @param msgParams - The params passed to eth_call.
|
||||
* @returns Full state update.
|
||||
*/
|
||||
async signMessage(msgParams: MessageParamsMetamask) {
|
||||
return await this._signAbstractMessage(
|
||||
this._messageManager,
|
||||
methodNameSign,
|
||||
msgParams,
|
||||
async (cleanMsgParams) =>
|
||||
await this._keyringController.signMessage(cleanMsgParams),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signifies a user's approval to sign a personal_sign message in queue.
|
||||
* Triggers signing, and the callback function from newUnsignedPersonalMessage.
|
||||
*
|
||||
* @param msgParams - The params of the message to sign & return to the Dapp.
|
||||
* @returns A full state update.
|
||||
*/
|
||||
async signPersonalMessage(msgParams: PersonalMessageParamsMetamask) {
|
||||
return await this._signAbstractMessage(
|
||||
this._personalMessageManager,
|
||||
methodNamePersonalSign,
|
||||
msgParams,
|
||||
async (cleanMsgParams) =>
|
||||
await this._keyringController.signPersonalMessage(cleanMsgParams),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The method for a user approving a call to eth_signTypedData, per EIP 712.
|
||||
* Triggers the callback in newUnsignedTypedMessage.
|
||||
*
|
||||
* @param msgParams - The params passed to eth_signTypedData.
|
||||
* @returns Full state update.
|
||||
*/
|
||||
async signTypedMessage(msgParams: TypedMessageParamsMetamask) {
|
||||
const { version } = msgParams;
|
||||
|
||||
return await this._signAbstractMessage(
|
||||
this._typedMessageManager,
|
||||
methodNameTypedSign,
|
||||
msgParams,
|
||||
async (cleanMsgParams) => {
|
||||
// For some reason every version after V1 used stringified params.
|
||||
if (version !== 'V1') {
|
||||
// But we don't have to require that. We can stop suggesting it now:
|
||||
if (typeof cleanMsgParams.data === 'string') {
|
||||
cleanMsgParams.data = JSON.parse(cleanMsgParams.data);
|
||||
}
|
||||
}
|
||||
|
||||
return await this._keyringController.signTypedMessage(cleanMsgParams, {
|
||||
version,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel a message submitted via eth_sign.
|
||||
*
|
||||
* @param msgId - The id of the message to cancel.
|
||||
* @returns A full state update.
|
||||
*/
|
||||
cancelMessage(msgId: string) {
|
||||
return this._cancelAbstractMessage(this._messageManager, msgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel a personal_sign type message.
|
||||
*
|
||||
* @param msgId - The ID of the message to cancel.
|
||||
* @returns A full state update.
|
||||
*/
|
||||
cancelPersonalMessage(msgId: string) {
|
||||
return this._cancelAbstractMessage(this._personalMessageManager, msgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel a eth_signTypedData type message.
|
||||
*
|
||||
* @param msgId - The ID of the message to cancel.
|
||||
* @returns A full state update.
|
||||
*/
|
||||
cancelTypedMessage(msgId: string) {
|
||||
return this._cancelAbstractMessage(this._typedMessageManager, msgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject all unapproved messages of any type.
|
||||
*
|
||||
* @param reason - A message to indicate why.
|
||||
*/
|
||||
rejectUnapproved(reason?: string) {
|
||||
this._messageManagers.forEach((messageManager) => {
|
||||
Object.keys(messageManager.getUnapprovedMessages()).forEach(
|
||||
(messageId) => {
|
||||
this._cancelAbstractMessage(messageManager, messageId, reason);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all unapproved messages from memory.
|
||||
*/
|
||||
clearUnapproved() {
|
||||
this._messageManagers.forEach((messageManager) => {
|
||||
messageManager.update({
|
||||
unapprovedMessages: {},
|
||||
unapprovedMessagesCount: 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async _signAbstractMessage<P extends AbstractMessageParams>(
|
||||
messageManager: AbstractMessageManager<
|
||||
AbstractMessage,
|
||||
P,
|
||||
AbstractMessageParamsMetamask
|
||||
>,
|
||||
methodName: string,
|
||||
msgParams: AbstractMessageParamsMetamask,
|
||||
getSignature: (cleanMessageParams: P) => Promise<any>,
|
||||
) {
|
||||
log.info(`MetaMaskController - ${methodName}`);
|
||||
|
||||
const messageId = msgParams.metamaskId as string;
|
||||
|
||||
try {
|
||||
const cleanMessageParams = await messageManager.approveMessage(msgParams);
|
||||
const signature = await getSignature(cleanMessageParams);
|
||||
|
||||
messageManager.setMessageStatusSigned(messageId, signature);
|
||||
|
||||
this._acceptApproval(messageId);
|
||||
|
||||
return this._getState();
|
||||
} catch (error) {
|
||||
log.info(`MetaMaskController - ${methodName} failed.`, error);
|
||||
this._cancelAbstractMessage(messageManager, messageId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private _cancelAbstractMessage(
|
||||
messageManager: AbstractMessageManager<
|
||||
AbstractMessage,
|
||||
AbstractMessageParams,
|
||||
AbstractMessageParamsMetamask
|
||||
>,
|
||||
messageId: string,
|
||||
reason?: string,
|
||||
) {
|
||||
if (reason) {
|
||||
const message = this._getMessage(messageId);
|
||||
|
||||
this._metricsEvent({
|
||||
event: reason,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
properties: {
|
||||
action: 'Sign Request',
|
||||
type: message.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
messageManager.rejectMessage(messageId);
|
||||
this._rejectApproval(messageId);
|
||||
|
||||
return this._getState();
|
||||
}
|
||||
|
||||
private _bubbleEvents(
|
||||
messageManager: AbstractMessageManager<
|
||||
AbstractMessage,
|
||||
AbstractMessageParams,
|
||||
AbstractMessageParamsMetamask
|
||||
>,
|
||||
) {
|
||||
messageManager.hub.on('updateBadge', () => {
|
||||
this.hub.emit('updateBadge');
|
||||
});
|
||||
}
|
||||
|
||||
private _subscribeToMessageState(
|
||||
messageManager: AbstractMessageManager<
|
||||
AbstractMessage,
|
||||
AbstractMessageParams,
|
||||
AbstractMessageParamsMetamask
|
||||
>,
|
||||
updateState: (
|
||||
state: SignControllerState,
|
||||
newMessages: Record<string, StateMessage>,
|
||||
messageCount: number,
|
||||
) => void,
|
||||
) {
|
||||
messageManager.subscribe(
|
||||
async (state: MessageManagerState<AbstractMessage>) => {
|
||||
const newMessages = await this._migrateMessages(
|
||||
state.unapprovedMessages as any,
|
||||
);
|
||||
|
||||
this.update((draftState) => {
|
||||
updateState(draftState, newMessages, state.unapprovedMessagesCount);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async _migrateMessages(
|
||||
coreMessages: Record<string, CoreMessage>,
|
||||
): Promise<Record<string, StateMessage>> {
|
||||
const stateMessages: Record<string, StateMessage> = {};
|
||||
|
||||
for (const messageId of Object.keys(coreMessages)) {
|
||||
const coreMessage = coreMessages[messageId];
|
||||
const stateMessage = await this._migrateMessage(coreMessage);
|
||||
|
||||
stateMessages[messageId] = stateMessage;
|
||||
}
|
||||
|
||||
return stateMessages;
|
||||
}
|
||||
|
||||
private async _migrateMessage(
|
||||
coreMessage: CoreMessage,
|
||||
): Promise<StateMessage> {
|
||||
const { messageParams, ...coreMessageData } = coreMessage;
|
||||
|
||||
// Core message managers use messageParams but frontend uses msgParams with lots of references
|
||||
const stateMessage = {
|
||||
...coreMessageData,
|
||||
rawSig: coreMessage.rawSig as string,
|
||||
msgParams: {
|
||||
...messageParams,
|
||||
origin: messageParams.origin as string,
|
||||
},
|
||||
};
|
||||
|
||||
const messageId = coreMessage.id;
|
||||
const existingMessage = this._getMessage(messageId);
|
||||
|
||||
const securityProviderResponse = existingMessage
|
||||
? existingMessage.securityProviderResponse
|
||||
: await this._securityProviderRequest(stateMessage, stateMessage.type);
|
||||
|
||||
return { ...stateMessage, securityProviderResponse };
|
||||
}
|
||||
|
||||
private _normalizeMsgData(data: string) {
|
||||
if (data.slice(0, 2) === '0x') {
|
||||
// data is already hex
|
||||
return data;
|
||||
}
|
||||
// data is unicode, convert to hex
|
||||
return bufferToHex(Buffer.from(data, 'utf8'));
|
||||
}
|
||||
|
||||
private _getMessage(messageId: string): StateMessage {
|
||||
return {
|
||||
...this.state.unapprovedMsgs,
|
||||
...this.state.unapprovedPersonalMsgs,
|
||||
...this.state.unapprovedTypedMessages,
|
||||
}[messageId];
|
||||
}
|
||||
|
||||
private _requestApproval(
|
||||
msgParams: AbstractMessageParamsMetamask,
|
||||
type: string,
|
||||
) {
|
||||
const id = msgParams.metamaskId as string;
|
||||
const origin = msgParams.origin || controllerName;
|
||||
|
||||
this.messagingSystem
|
||||
.call(
|
||||
'ApprovalController:addRequest',
|
||||
{
|
||||
id,
|
||||
origin,
|
||||
type,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.catch(() => {
|
||||
// Intentionally ignored as promise not currently used
|
||||
});
|
||||
}
|
||||
|
||||
private _acceptApproval(messageId: string) {
|
||||
try {
|
||||
this.messagingSystem.call('ApprovalController:acceptRequest', messageId);
|
||||
} catch (error) {
|
||||
log.info('Failed to accept signature approval request', error);
|
||||
}
|
||||
}
|
||||
|
||||
private _rejectApproval(messageId: string) {
|
||||
try {
|
||||
this.messagingSystem.call(
|
||||
'ApprovalController:rejectRequest',
|
||||
messageId,
|
||||
'Cancel',
|
||||
);
|
||||
} catch (error) {
|
||||
log.info('Failed to reject signature approval request', error);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import {
|
||||
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
|
||||
} from '../../../shared/constants/swaps';
|
||||
import { GasEstimateTypes } from '../../../shared/constants/gas';
|
||||
import { CHAIN_IDS } from '../../../shared/constants/network';
|
||||
import { CHAIN_IDS, NetworkStatus } from '../../../shared/constants/network';
|
||||
import {
|
||||
FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME,
|
||||
FALLBACK_SMART_TRANSACTIONS_DEADLINE,
|
||||
@ -41,7 +41,6 @@ import fetchEstimatedL1Fee from '../../../ui/helpers/utils/optimism/fetchEstimat
|
||||
|
||||
import { Numeric } from '../../../shared/modules/Numeric';
|
||||
import { EtherDenomination } from '../../../shared/constants/common';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
|
||||
const MAX_GAS_LIMIT = 2500000;
|
||||
@ -114,6 +113,7 @@ export default class SwapsController {
|
||||
fetchTradesInfo = defaultFetchTradesInfo,
|
||||
getCurrentChainId,
|
||||
getEIP1559GasFeeEstimates,
|
||||
onNetworkStateChange,
|
||||
}) {
|
||||
this.store = new ObservableStore({
|
||||
swapsState: { ...initialState.swapsState },
|
||||
@ -136,10 +136,14 @@ export default class SwapsController {
|
||||
this.indexOfNewestCallInFlight = 0;
|
||||
|
||||
this.ethersProvider = new Web3Provider(provider);
|
||||
this._currentNetwork = networkController.store.getState().network;
|
||||
networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, (network) => {
|
||||
if (network !== 'loading' && network !== this._currentNetwork) {
|
||||
this._currentNetwork = network;
|
||||
this._currentNetworkId = networkController.store.getState().networkId;
|
||||
onNetworkStateChange(() => {
|
||||
const { networkId, networkStatus } = networkController.store.getState();
|
||||
if (
|
||||
networkStatus === NetworkStatus.Available &&
|
||||
networkId !== this._currentNetworkId
|
||||
) {
|
||||
this._currentNetworkId = networkId;
|
||||
this.ethersProvider = new Web3Provider(provider);
|
||||
}
|
||||
});
|
||||
@ -300,6 +304,7 @@ export default class SwapsController {
|
||||
Object.values(newQuotes).map(async (quote) => {
|
||||
if (quote.trade) {
|
||||
const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee(
|
||||
chainId,
|
||||
{
|
||||
txParams: quote.trade,
|
||||
chainId,
|
||||
|
@ -4,7 +4,11 @@ import sinon from 'sinon';
|
||||
import { BigNumber } from '@ethersproject/bignumber';
|
||||
import { mapValues } from 'lodash';
|
||||
import BigNumberjs from 'bignumber.js';
|
||||
import { CHAIN_IDS, NETWORK_IDS } from '../../../shared/constants/network';
|
||||
import {
|
||||
CHAIN_IDS,
|
||||
NETWORK_IDS,
|
||||
NetworkStatus,
|
||||
} from '../../../shared/constants/network';
|
||||
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
|
||||
import { createTestProviderTools } from '../../../test/stub/provider';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
@ -14,7 +18,6 @@ import {
|
||||
FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER,
|
||||
} from '../../../shared/constants/smartTransactions';
|
||||
import SwapsController, { utils } from './swaps';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
const MOCK_FETCH_PARAMS = {
|
||||
slippage: 3,
|
||||
@ -98,16 +101,11 @@ const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({
|
||||
function getMockNetworkController() {
|
||||
return {
|
||||
store: {
|
||||
getState: () => {
|
||||
return {
|
||||
network: NETWORK_IDS.GOERLI,
|
||||
};
|
||||
},
|
||||
getState: sinon.stub().returns({
|
||||
networkId: NETWORK_IDS.GOERLI,
|
||||
networkStatus: NetworkStatus.Available,
|
||||
}),
|
||||
},
|
||||
on: sinon
|
||||
.stub()
|
||||
.withArgs(NETWORK_EVENTS.NETWORK_DID_CHANGE)
|
||||
.callsArgAsync(1),
|
||||
};
|
||||
}
|
||||
|
||||
@ -158,11 +156,12 @@ const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => {
|
||||
describe('SwapsController', function () {
|
||||
let provider;
|
||||
|
||||
const getSwapsController = () => {
|
||||
const getSwapsController = (_provider = provider) => {
|
||||
return new SwapsController({
|
||||
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
|
||||
networkController: getMockNetworkController(),
|
||||
provider,
|
||||
onNetworkStateChange: sinon.stub(),
|
||||
provider: _provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
fetchTradesInfo: fetchTradesInfoStub,
|
||||
@ -209,9 +208,11 @@ describe('SwapsController', function () {
|
||||
|
||||
it('should replace ethers instance when network changes', function () {
|
||||
const networkController = getMockNetworkController();
|
||||
const onNetworkStateChange = sinon.stub();
|
||||
const swapsController = new SwapsController({
|
||||
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
|
||||
networkController,
|
||||
onNetworkStateChange,
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
@ -219,9 +220,13 @@ describe('SwapsController', function () {
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
});
|
||||
const currentEthersInstance = swapsController.ethersProvider;
|
||||
const onNetworkDidChange = networkController.on.getCall(0).args[1];
|
||||
const changeNetwork = onNetworkStateChange.getCall(0).args[0];
|
||||
|
||||
onNetworkDidChange(NETWORK_IDS.MAINNET);
|
||||
networkController.store.getState.returns({
|
||||
networkId: NETWORK_IDS.MAINNET,
|
||||
networkStatus: NetworkStatus.Available,
|
||||
});
|
||||
changeNetwork(NETWORK_IDS.MAINNET);
|
||||
|
||||
const newEthersInstance = swapsController.ethersProvider;
|
||||
assert.notStrictEqual(
|
||||
@ -233,9 +238,11 @@ describe('SwapsController', function () {
|
||||
|
||||
it('should not replace ethers instance when network changes to loading', function () {
|
||||
const networkController = getMockNetworkController();
|
||||
const onNetworkStateChange = sinon.stub();
|
||||
const swapsController = new SwapsController({
|
||||
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
|
||||
networkController,
|
||||
onNetworkStateChange,
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
@ -243,9 +250,13 @@ describe('SwapsController', function () {
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
});
|
||||
const currentEthersInstance = swapsController.ethersProvider;
|
||||
const onNetworkDidChange = networkController.on.getCall(0).args[1];
|
||||
const changeNetwork = onNetworkStateChange.getCall(0).args[0];
|
||||
|
||||
onNetworkDidChange('loading');
|
||||
networkController.store.getState.returns({
|
||||
networkId: null,
|
||||
networkStatus: NetworkStatus.Unknown,
|
||||
});
|
||||
changeNetwork('loading');
|
||||
|
||||
const newEthersInstance = swapsController.ethersProvider;
|
||||
assert.strictEqual(
|
||||
@ -257,9 +268,11 @@ describe('SwapsController', function () {
|
||||
|
||||
it('should not replace ethers instance when network changes to the same network', function () {
|
||||
const networkController = getMockNetworkController();
|
||||
const onNetworkStateChange = sinon.stub();
|
||||
const swapsController = new SwapsController({
|
||||
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
|
||||
networkController,
|
||||
onNetworkStateChange,
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
@ -267,9 +280,13 @@ describe('SwapsController', function () {
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
});
|
||||
const currentEthersInstance = swapsController.ethersProvider;
|
||||
const onNetworkDidChange = networkController.on.getCall(0).args[1];
|
||||
const changeNetwork = onNetworkStateChange.getCall(0).args[0];
|
||||
|
||||
onNetworkDidChange(NETWORK_IDS.GOERLI);
|
||||
networkController.store.getState.returns({
|
||||
networkId: NETWORK_IDS.GOERLI,
|
||||
networkStatus: NetworkStatus.Available,
|
||||
});
|
||||
changeNetwork(NETWORK_IDS.GOERLI);
|
||||
|
||||
const newEthersInstance = swapsController.ethersProvider;
|
||||
assert.strictEqual(
|
||||
@ -705,6 +722,72 @@ describe('SwapsController', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('calls returns the correct quotes on the optimism chain', async function () {
|
||||
fetchTradesInfoStub.resetHistory();
|
||||
const OPTIMISM_MOCK_FETCH_METADATA = {
|
||||
...MOCK_FETCH_METADATA,
|
||||
chainId: CHAIN_IDS.OPTIMISM,
|
||||
};
|
||||
const optimismProviderResultStub = {
|
||||
// 1 gwei
|
||||
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||
// by default, all accounts are external accounts (not contracts)
|
||||
eth_getCode: '0x',
|
||||
eth_call:
|
||||
'0x000000000000000000000000000000000000000000000000000103c18816d4e8',
|
||||
};
|
||||
const optimismProvider = createTestProviderTools({
|
||||
scaffold: optimismProviderResultStub,
|
||||
networkId: 10,
|
||||
chainId: 10,
|
||||
}).provider;
|
||||
|
||||
swapsController = getSwapsController(optimismProvider);
|
||||
|
||||
fetchTradesInfoStub.resolves(getMockQuotes());
|
||||
|
||||
// Make it so approval is not required
|
||||
sandbox
|
||||
.stub(swapsController, '_getERC20Allowance')
|
||||
.resolves(BigNumber.from(1));
|
||||
|
||||
const [newQuotes] = await swapsController.fetchAndSetQuotes(
|
||||
MOCK_FETCH_PARAMS,
|
||||
OPTIMISM_MOCK_FETCH_METADATA,
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(newQuotes[TEST_AGG_ID_BEST], {
|
||||
...getMockQuotes()[TEST_AGG_ID_BEST],
|
||||
sourceTokenInfo: undefined,
|
||||
destinationTokenInfo: {
|
||||
symbol: 'FOO',
|
||||
decimals: 18,
|
||||
},
|
||||
isBestQuote: true,
|
||||
// TODO: find a way to calculate these values dynamically
|
||||
gasEstimate: 2000000,
|
||||
gasEstimateWithRefund: '0xb8cae',
|
||||
savings: {
|
||||
fee: '-0.061067',
|
||||
metaMaskFee: '0.5050505050505050505',
|
||||
performance: '6',
|
||||
total: '5.4338824949494949495',
|
||||
medianMetaMaskFee: '0.44444444444444444444',
|
||||
},
|
||||
ethFee: '0.113822',
|
||||
multiLayerL1TradeFeeTotal: '0x0103c18816d4e8',
|
||||
overallValueOfQuote: '49.886178',
|
||||
metaMaskFeeInEth: '0.5050505050505050505',
|
||||
ethValueOfTokens: '50',
|
||||
});
|
||||
assert.strictEqual(
|
||||
fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS, {
|
||||
...OPTIMISM_MOCK_FETCH_METADATA,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('performs the allowance check', async function () {
|
||||
fetchTradesInfoStub.resolves(getMockQuotes());
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import EventEmitter from 'safe-event-emitter';
|
||||
import EventEmitter from '@metamask/safe-event-emitter';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import { bufferToHex, keccak, toBuffer, isHexString } from 'ethereumjs-util';
|
||||
import EthQuery from 'ethjs-query';
|
||||
@ -39,11 +39,12 @@ import {
|
||||
hexWEIToDecGWEI,
|
||||
} from '../../../../shared/modules/conversion.utils';
|
||||
import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils';
|
||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
|
||||
import {
|
||||
HARDFORKS,
|
||||
CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP,
|
||||
NETWORK_TYPES,
|
||||
NetworkStatus,
|
||||
} from '../../../../shared/constants/network';
|
||||
import {
|
||||
determineTransactionAssetType,
|
||||
@ -115,7 +116,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.initState - initial transaction list default is an empty array
|
||||
* @param {Function} opts.getNetworkState - Get the current network state.
|
||||
* @param {Function} opts.getNetworkId - Get the current network ID.
|
||||
* @param {Function} opts.getNetworkStatus - Get the current network status.
|
||||
* @param {Function} opts.onNetworkStateChange - Subscribe to network state change events.
|
||||
* @param {object} opts.blockTracker - An instance of eth-blocktracker
|
||||
* @param {object} opts.provider - A network provider.
|
||||
@ -129,7 +131,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
|
||||
export default class TransactionController extends EventEmitter {
|
||||
constructor(opts) {
|
||||
super();
|
||||
this.getNetworkState = opts.getNetworkState;
|
||||
this.getNetworkId = opts.getNetworkId;
|
||||
this.getNetworkStatus = opts.getNetworkStatus;
|
||||
this._getCurrentChainId = opts.getCurrentChainId;
|
||||
this.getProviderConfig = opts.getProviderConfig;
|
||||
this._getCurrentNetworkEIP1559Compatibility =
|
||||
@ -167,7 +170,8 @@ export default class TransactionController extends EventEmitter {
|
||||
this.txStateManager = new TransactionStateManager({
|
||||
initState: opts.initState,
|
||||
txHistoryLimit: opts.txHistoryLimit,
|
||||
getNetworkState: this.getNetworkState,
|
||||
getNetworkId: this.getNetworkId,
|
||||
getNetworkStatus: this.getNetworkStatus,
|
||||
getCurrentChainId: opts.getCurrentChainId,
|
||||
});
|
||||
|
||||
@ -226,10 +230,13 @@ export default class TransactionController extends EventEmitter {
|
||||
* @returns {number} The numerical chainId.
|
||||
*/
|
||||
getChainId() {
|
||||
const networkState = this.getNetworkState();
|
||||
const networkStatus = this.getNetworkStatus();
|
||||
const chainId = this._getCurrentChainId();
|
||||
const integerChainId = parseInt(chainId, 16);
|
||||
if (networkState === 'loading' || Number.isNaN(integerChainId)) {
|
||||
if (
|
||||
networkStatus !== NetworkStatus.Available ||
|
||||
Number.isNaN(integerChainId)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
return integerChainId;
|
||||
@ -272,12 +279,13 @@ export default class TransactionController extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
// For 'rpc' we need to use the same basic configuration as mainnet,
|
||||
// since we only support EVM compatible chains, and then override the
|
||||
// For 'rpc' we need to use the same basic configuration as mainnet, since
|
||||
// we only support EVM compatible chains, and then override the
|
||||
// name, chainId and networkId properties. This is done using the
|
||||
// `forCustomChain` static method on the Common class.
|
||||
const chainId = parseInt(this._getCurrentChainId(), 16);
|
||||
const networkId = this.getNetworkState();
|
||||
const networkStatus = this.getNetworkStatus();
|
||||
const networkId = this.getNetworkId();
|
||||
|
||||
const customChainParams = {
|
||||
name,
|
||||
@ -291,7 +299,8 @@ export default class TransactionController extends EventEmitter {
|
||||
// on a custom network that requires valid network id. I have not ran
|
||||
// into this limitation on any network I have attempted, even when
|
||||
// hardcoding networkId to 'loading'.
|
||||
networkId: networkId === 'loading' ? 0 : parseInt(networkId, 10),
|
||||
networkId:
|
||||
networkStatus === NetworkStatus.Available ? parseInt(networkId, 10) : 0,
|
||||
};
|
||||
|
||||
return Common.forCustomChain(
|
||||
@ -2018,7 +2027,7 @@ export default class TransactionController extends EventEmitter {
|
||||
this._trackMetaMetricsEvent({
|
||||
event: 'Swap Failed',
|
||||
sensitiveProperties: { ...txMeta.swapMetaData },
|
||||
category: EVENT.CATEGORIES.SWAPS,
|
||||
category: MetaMetricsEventCategory.Swaps,
|
||||
});
|
||||
} else {
|
||||
const tokensReceived = getSwapsTokensReceivedFromTxMeta(
|
||||
@ -2053,7 +2062,7 @@ export default class TransactionController extends EventEmitter {
|
||||
|
||||
this._trackMetaMetricsEvent({
|
||||
event: 'Swap Completed',
|
||||
category: EVENT.CATEGORIES.SWAPS,
|
||||
category: MetaMetricsEventCategory.Swaps,
|
||||
sensitiveProperties: {
|
||||
...txMeta.swapMetaData,
|
||||
token_to_amount_received: tokensReceived,
|
||||
@ -2405,7 +2414,7 @@ export default class TransactionController extends EventEmitter {
|
||||
// occur.
|
||||
case TransactionMetaMetricsEvent.added:
|
||||
this.createEventFragment({
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
initialEvent: TransactionMetaMetricsEvent.added,
|
||||
successEvent: TransactionMetaMetricsEvent.approved,
|
||||
failureEvent: TransactionMetaMetricsEvent.rejected,
|
||||
@ -2427,7 +2436,7 @@ export default class TransactionController extends EventEmitter {
|
||||
case TransactionMetaMetricsEvent.approved:
|
||||
case TransactionMetaMetricsEvent.rejected:
|
||||
this.createEventFragment({
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
successEvent: TransactionMetaMetricsEvent.approved,
|
||||
failureEvent: TransactionMetaMetricsEvent.rejected,
|
||||
properties,
|
||||
@ -2449,7 +2458,7 @@ export default class TransactionController extends EventEmitter {
|
||||
// properties to the transaction event.
|
||||
case TransactionMetaMetricsEvent.submitted:
|
||||
this.createEventFragment({
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
initialEvent: TransactionMetaMetricsEvent.submitted,
|
||||
successEvent: TransactionMetaMetricsEvent.finalized,
|
||||
properties,
|
||||
@ -2469,7 +2478,7 @@ export default class TransactionController extends EventEmitter {
|
||||
// fragment does not exist.
|
||||
case TransactionMetaMetricsEvent.finalized:
|
||||
this.createEventFragment({
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
successEvent: TransactionMetaMetricsEvent.finalized,
|
||||
properties,
|
||||
sensitiveProperties,
|
||||
|
@ -10,7 +10,10 @@ import {
|
||||
getTestAccounts,
|
||||
} from '../../../../test/stub/provider';
|
||||
import mockEstimates from '../../../../test/data/mock-estimates.json';
|
||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||
import {
|
||||
MetaMetricsEventCategory,
|
||||
MetaMetricsTransactionEventSource,
|
||||
} from '../../../../shared/constants/metametrics';
|
||||
import {
|
||||
TransactionStatus,
|
||||
TransactionType,
|
||||
@ -27,12 +30,14 @@ import {
|
||||
} from '../../../../shared/constants/gas';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
||||
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
|
||||
import { NetworkStatus } from '../../../../shared/constants/network';
|
||||
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
|
||||
import TransactionController from '.';
|
||||
|
||||
const noop = () => true;
|
||||
const currentNetworkId = '5';
|
||||
const currentChainId = '0x5';
|
||||
const currentNetworkStatus = NetworkStatus.Available;
|
||||
const providerConfig = {
|
||||
type: 'goerli',
|
||||
};
|
||||
@ -46,7 +51,8 @@ describe('Transaction Controller', function () {
|
||||
providerResultStub,
|
||||
fromAccount,
|
||||
fragmentExists,
|
||||
networkStore;
|
||||
networkStatusStore,
|
||||
getCurrentChainId;
|
||||
|
||||
beforeEach(function () {
|
||||
fragmentExists = false;
|
||||
@ -59,22 +65,27 @@ describe('Transaction Controller', function () {
|
||||
provider = createTestProviderTools({
|
||||
scaffold: providerResultStub,
|
||||
networkId: currentNetworkId,
|
||||
chainId: currentNetworkId,
|
||||
chainId: parseInt(currentChainId, 16),
|
||||
}).provider;
|
||||
|
||||
networkStore = new ObservableStore(currentNetworkId);
|
||||
networkStatusStore = new ObservableStore(currentNetworkStatus);
|
||||
|
||||
fromAccount = getTestAccounts()[0];
|
||||
const blockTrackerStub = new EventEmitter();
|
||||
blockTrackerStub.getCurrentBlock = noop;
|
||||
blockTrackerStub.getLatestBlock = noop;
|
||||
|
||||
getCurrentChainId = sinon.stub().callsFake(() => currentChainId);
|
||||
|
||||
txController = new TransactionController({
|
||||
provider,
|
||||
getGasPrice() {
|
||||
return '0xee6b2800';
|
||||
},
|
||||
getNetworkState: () => networkStore.getState(),
|
||||
onNetworkStateChange: (listener) => networkStore.subscribe(listener),
|
||||
getNetworkId: () => currentNetworkId,
|
||||
getNetworkStatus: () => networkStatusStore.getState(),
|
||||
onNetworkStateChange: (listener) =>
|
||||
networkStatusStore.subscribe(listener),
|
||||
getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(false),
|
||||
getCurrentAccountEIP1559Compatibility: () => false,
|
||||
txHistoryLimit: 10,
|
||||
@ -85,7 +96,7 @@ describe('Transaction Controller', function () {
|
||||
}),
|
||||
getProviderConfig: () => providerConfig,
|
||||
getPermittedAccounts: () => undefined,
|
||||
getCurrentChainId: () => currentChainId,
|
||||
getCurrentChainId,
|
||||
getParticipateInMetrics: () => false,
|
||||
trackMetaMetricsEvent: () => undefined,
|
||||
createEventFragment: () => undefined,
|
||||
@ -467,8 +478,8 @@ describe('Transaction Controller', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if netId is loading', async function () {
|
||||
networkStore.putState('loading');
|
||||
it('should fail if the network status is not "available"', async function () {
|
||||
networkStatusStore.putState(NetworkStatus.Unknown);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
txController.addUnapprovedTransaction(undefined, {
|
||||
@ -1079,8 +1090,19 @@ describe('Transaction Controller', function () {
|
||||
});
|
||||
|
||||
describe('#getChainId', function () {
|
||||
it('returns 0 when the chainId is NaN', function () {
|
||||
networkStore.putState('loading');
|
||||
it('returns the chain ID of the network when it is available', function () {
|
||||
networkStatusStore.putState(NetworkStatus.Available);
|
||||
assert.equal(txController.getChainId(), 5);
|
||||
});
|
||||
|
||||
it('returns 0 when the network is not available', function () {
|
||||
networkStatusStore.putState('asdflsfadf');
|
||||
assert.equal(txController.getChainId(), 0);
|
||||
});
|
||||
|
||||
it('returns 0 when the chain ID cannot be parsed as a hex string', function () {
|
||||
networkStatusStore.putState(NetworkStatus.Available);
|
||||
getCurrentChainId.returns('$fdsjfldf');
|
||||
assert.equal(txController.getChainId(), 0);
|
||||
});
|
||||
});
|
||||
@ -1753,7 +1775,7 @@ describe('Transaction Controller', function () {
|
||||
successEvent: 'Transaction Approved',
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
persist: true,
|
||||
properties: {
|
||||
chain_id: '0x5',
|
||||
@ -1762,7 +1784,7 @@ describe('Transaction Controller', function () {
|
||||
gas_edit_type: 'none',
|
||||
network: '5',
|
||||
referrer: ORIGIN_METAMASK,
|
||||
source: EVENT.SOURCE.TRANSACTION.USER,
|
||||
source: MetaMetricsTransactionEventSource.User,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
account_type: 'MetaMask',
|
||||
asset_type: AssetType.native,
|
||||
@ -1840,7 +1862,7 @@ describe('Transaction Controller', function () {
|
||||
initialEvent: 'Transaction Submitted',
|
||||
successEvent: 'Transaction Finalized',
|
||||
uniqueIdentifier: 'transaction-submitted-1',
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
persist: true,
|
||||
properties: {
|
||||
chain_id: '0x5',
|
||||
@ -1849,7 +1871,7 @@ describe('Transaction Controller', function () {
|
||||
gas_edit_type: 'none',
|
||||
network: '5',
|
||||
referrer: ORIGIN_METAMASK,
|
||||
source: EVENT.SOURCE.TRANSACTION.USER,
|
||||
source: MetaMetricsTransactionEventSource.User,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
account_type: 'MetaMask',
|
||||
asset_type: AssetType.native,
|
||||
@ -1939,7 +1961,7 @@ describe('Transaction Controller', function () {
|
||||
successEvent: 'Transaction Approved',
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
persist: true,
|
||||
properties: {
|
||||
chain_id: '0x5',
|
||||
@ -1948,7 +1970,7 @@ describe('Transaction Controller', function () {
|
||||
gas_edit_type: 'none',
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
account_type: 'MetaMask',
|
||||
asset_type: AssetType.native,
|
||||
@ -2028,7 +2050,7 @@ describe('Transaction Controller', function () {
|
||||
initialEvent: 'Transaction Submitted',
|
||||
successEvent: 'Transaction Finalized',
|
||||
uniqueIdentifier: 'transaction-submitted-1',
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
persist: true,
|
||||
properties: {
|
||||
chain_id: '0x5',
|
||||
@ -2037,7 +2059,7 @@ describe('Transaction Controller', function () {
|
||||
gas_edit_type: 'none',
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
account_type: 'MetaMask',
|
||||
asset_type: AssetType.native,
|
||||
@ -2119,7 +2141,7 @@ describe('Transaction Controller', function () {
|
||||
successEvent: 'Transaction Approved',
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
persist: true,
|
||||
properties: {
|
||||
chain_id: '0x5',
|
||||
@ -2128,7 +2150,7 @@ describe('Transaction Controller', function () {
|
||||
gas_edit_type: 'none',
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
account_type: 'MetaMask',
|
||||
asset_type: AssetType.native,
|
||||
@ -2192,11 +2214,11 @@ describe('Transaction Controller', function () {
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
persist: true,
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
properties: {
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
chain_id: '0x5',
|
||||
eip_1559_version: '0',
|
||||
@ -2266,11 +2288,11 @@ describe('Transaction Controller', function () {
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
persist: true,
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
properties: {
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
chain_id: '0x5',
|
||||
eip_1559_version: '0',
|
||||
@ -2340,11 +2362,11 @@ describe('Transaction Controller', function () {
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
persist: true,
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
properties: {
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
chain_id: '0x5',
|
||||
eip_1559_version: '0',
|
||||
@ -2422,7 +2444,7 @@ describe('Transaction Controller', function () {
|
||||
failureEvent: 'Transaction Rejected',
|
||||
uniqueIdentifier: 'transaction-added-1',
|
||||
persist: true,
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
category: MetaMetricsEventCategory.Transactions,
|
||||
properties: {
|
||||
chain_id: '0x5',
|
||||
eip_1559_version: '2',
|
||||
@ -2430,7 +2452,7 @@ describe('Transaction Controller', function () {
|
||||
gas_edit_type: 'none',
|
||||
network: '5',
|
||||
referrer: 'other',
|
||||
source: EVENT.SOURCE.TRANSACTION.DAPP,
|
||||
source: MetaMetricsTransactionEventSource.Dapp,
|
||||
transaction_type: TransactionType.simpleSend,
|
||||
account_type: 'MetaMask',
|
||||
asset_type: AssetType.native,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import EventEmitter from 'safe-event-emitter';
|
||||
import EventEmitter from '@metamask/safe-event-emitter';
|
||||
import log from 'loglevel';
|
||||
import EthQuery from 'ethjs-query';
|
||||
import { TransactionStatus } from '../../../../shared/constants/transaction';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import EventEmitter from 'safe-event-emitter';
|
||||
import EventEmitter from '@metamask/safe-event-emitter';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import log from 'loglevel';
|
||||
import { values, keyBy, mapValues, omitBy, pickBy, sortBy } from 'lodash';
|
||||
@ -7,6 +7,7 @@ import { TransactionStatus } from '../../../../shared/constants/transaction';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
||||
import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils';
|
||||
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
|
||||
import { NetworkStatus } from '../../../../shared/constants/network';
|
||||
import {
|
||||
generateHistoryEntry,
|
||||
replayHistory,
|
||||
@ -54,13 +55,15 @@ export const ERROR_SUBMITTING =
|
||||
* transactions list keyed by id
|
||||
* @param {number} [opts.txHistoryLimit] - limit for how many finished
|
||||
* transactions can hang around in state
|
||||
* @param {Function} opts.getNetworkState - Get the current network state.
|
||||
* @param {Function} opts.getNetworkId - Get the current network Id.
|
||||
* @param {Function} opts.getNetworkStatus - Get the current network status.
|
||||
*/
|
||||
export default class TransactionStateManager extends EventEmitter {
|
||||
constructor({
|
||||
initState,
|
||||
txHistoryLimit,
|
||||
getNetworkState,
|
||||
getNetworkId,
|
||||
getNetworkStatus,
|
||||
getCurrentChainId,
|
||||
}) {
|
||||
super();
|
||||
@ -70,7 +73,8 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
...initState,
|
||||
});
|
||||
this.txHistoryLimit = txHistoryLimit;
|
||||
this.getNetworkState = getNetworkState;
|
||||
this.getNetworkId = getNetworkId;
|
||||
this.getNetworkStatus = getNetworkStatus;
|
||||
this.getCurrentChainId = getCurrentChainId;
|
||||
}
|
||||
|
||||
@ -86,9 +90,10 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
* @returns {TransactionMeta} the default txMeta object
|
||||
*/
|
||||
generateTxMeta(opts = {}) {
|
||||
const netId = this.getNetworkState();
|
||||
const networkId = this.getNetworkId();
|
||||
const networkStatus = this.getNetworkStatus();
|
||||
const chainId = this.getCurrentChainId();
|
||||
if (netId === 'loading') {
|
||||
if (networkStatus !== NetworkStatus.Available) {
|
||||
throw new Error('MetaMask is having trouble connecting to the network');
|
||||
}
|
||||
|
||||
@ -128,7 +133,7 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
id: createId(),
|
||||
time: new Date().getTime(),
|
||||
status: TransactionStatus.unapproved,
|
||||
metamaskNetworkId: netId,
|
||||
metamaskNetworkId: networkId,
|
||||
originalGasEstimate: opts.txParams?.gas,
|
||||
userEditedGasLimit: false,
|
||||
chainId,
|
||||
@ -149,12 +154,12 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
*/
|
||||
getUnapprovedTxList() {
|
||||
const chainId = this.getCurrentChainId();
|
||||
const network = this.getNetworkState();
|
||||
const networkId = this.getNetworkId();
|
||||
return pickBy(
|
||||
this.store.getState().transactions,
|
||||
(transaction) =>
|
||||
transaction.status === TransactionStatus.unapproved &&
|
||||
transactionMatchesNetwork(transaction, chainId, network),
|
||||
transactionMatchesNetwork(transaction, chainId, networkId),
|
||||
);
|
||||
}
|
||||
|
||||
@ -413,7 +418,7 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
limit,
|
||||
} = {}) {
|
||||
const chainId = this.getCurrentChainId();
|
||||
const network = this.getNetworkState();
|
||||
const networkId = this.getNetworkId();
|
||||
// searchCriteria is an object that might have values that aren't predicate
|
||||
// methods. When providing any other value type (string, number, etc), we
|
||||
// consider this shorthand for "check the value at key for strict equality
|
||||
@ -442,7 +447,7 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
// when filterToCurrentNetwork is true.
|
||||
if (
|
||||
filterToCurrentNetwork &&
|
||||
transactionMatchesNetwork(transaction, chainId, network) === false
|
||||
transactionMatchesNetwork(transaction, chainId, networkId) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@ -596,8 +601,7 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all transactions for the given address on the current network,
|
||||
* preferring chainId for comparison over networkId.
|
||||
* Removes all transactions for the given address on the current network.
|
||||
*
|
||||
* @param {string} address - hex string of the from address on the txParams
|
||||
* to remove
|
||||
@ -605,8 +609,8 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
wipeTransactions(address) {
|
||||
// network only tx
|
||||
const { transactions } = this.store.getState();
|
||||
const network = this.getNetworkState();
|
||||
const chainId = this.getCurrentChainId();
|
||||
const networkId = this.getNetworkId();
|
||||
|
||||
// Update state
|
||||
this.store.updateState({
|
||||
@ -614,7 +618,7 @@ export default class TransactionStateManager extends EventEmitter {
|
||||
transactions,
|
||||
(transaction) =>
|
||||
transaction.txParams.from === address &&
|
||||
transactionMatchesNetwork(transaction, chainId, network),
|
||||
transactionMatchesNetwork(transaction, chainId, networkId),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -62,7 +62,7 @@ export default class AccountTracker {
|
||||
accounts: {},
|
||||
currentBlockGasLimit: '',
|
||||
};
|
||||
this.store = new ObservableStore(initState);
|
||||
this.store = new ObservableStore({ ...initState, ...opts.initState });
|
||||
|
||||
this.resetState = () => {
|
||||
this.store.updateState(initState);
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { errorCodes } from 'eth-rpc-errors';
|
||||
import { detectSIWE } from '@metamask/controller-utils';
|
||||
import { isValidAddress } from 'ethereumjs-util';
|
||||
|
||||
import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app';
|
||||
import { TransactionStatus } from '../../../shared/constants/transaction';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import { detectSIWE } from '../../../shared/modules/siwe';
|
||||
|
||||
import {
|
||||
EVENT,
|
||||
EVENT_NAMES,
|
||||
METAMETRIC_KEY_OPTIONS,
|
||||
METAMETRIC_KEY,
|
||||
MetaMetricsEventCategory,
|
||||
MetaMetricsEventName,
|
||||
MetaMetricsEventUiCustomization,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
|
||||
/**
|
||||
@ -41,55 +44,55 @@ const RATE_LIMIT_MAP = {
|
||||
|
||||
/**
|
||||
* For events with user interaction (approve / reject | cancel) this map will
|
||||
* return an object with APPROVED, REJECTED and REQUESTED keys that map to the
|
||||
* return an object with APPROVED, REJECTED, REQUESTED, and FAILED keys that map to the
|
||||
* appropriate event names.
|
||||
*/
|
||||
const EVENT_NAME_MAP = {
|
||||
[MESSAGE_TYPE.ETH_SIGN]: {
|
||||
APPROVED: EVENT_NAMES.SIGNATURE_APPROVED,
|
||||
FAILED: EVENT_NAMES.SIGNATURE_FAILED,
|
||||
REJECTED: EVENT_NAMES.SIGNATURE_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.SignatureApproved,
|
||||
FAILED: MetaMetricsEventName.SignatureFailed,
|
||||
REJECTED: MetaMetricsEventName.SignatureRejected,
|
||||
REQUESTED: MetaMetricsEventName.SignatureRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: {
|
||||
APPROVED: EVENT_NAMES.SIGNATURE_APPROVED,
|
||||
REJECTED: EVENT_NAMES.SIGNATURE_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.SignatureApproved,
|
||||
REJECTED: MetaMetricsEventName.SignatureRejected,
|
||||
REQUESTED: MetaMetricsEventName.SignatureRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: {
|
||||
APPROVED: EVENT_NAMES.SIGNATURE_APPROVED,
|
||||
REJECTED: EVENT_NAMES.SIGNATURE_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.SignatureApproved,
|
||||
REJECTED: MetaMetricsEventName.SignatureRejected,
|
||||
REQUESTED: MetaMetricsEventName.SignatureRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: {
|
||||
APPROVED: EVENT_NAMES.SIGNATURE_APPROVED,
|
||||
REJECTED: EVENT_NAMES.SIGNATURE_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.SignatureApproved,
|
||||
REJECTED: MetaMetricsEventName.SignatureRejected,
|
||||
REQUESTED: MetaMetricsEventName.SignatureRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.PERSONAL_SIGN]: {
|
||||
APPROVED: EVENT_NAMES.SIGNATURE_APPROVED,
|
||||
REJECTED: EVENT_NAMES.SIGNATURE_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.SignatureApproved,
|
||||
REJECTED: MetaMetricsEventName.SignatureRejected,
|
||||
REQUESTED: MetaMetricsEventName.SignatureRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.ETH_DECRYPT]: {
|
||||
APPROVED: EVENT_NAMES.DECRYPTION_APPROVED,
|
||||
REJECTED: EVENT_NAMES.DECRYPTION_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.DECRYPTION_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.DecryptionApproved,
|
||||
REJECTED: MetaMetricsEventName.DecryptionRejected,
|
||||
REQUESTED: MetaMetricsEventName.DecryptionRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: {
|
||||
APPROVED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_APPROVED,
|
||||
REJECTED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.EncryptionPublicKeyApproved,
|
||||
REJECTED: MetaMetricsEventName.EncryptionPublicKeyRejected,
|
||||
REQUESTED: MetaMetricsEventName.EncryptionPublicKeyRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: {
|
||||
APPROVED: EVENT_NAMES.PERMISSIONS_APPROVED,
|
||||
REJECTED: EVENT_NAMES.PERMISSIONS_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.PERMISSIONS_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.PermissionsApproved,
|
||||
REJECTED: MetaMetricsEventName.PermissionsRejected,
|
||||
REQUESTED: MetaMetricsEventName.PermissionsRequested,
|
||||
},
|
||||
[MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: {
|
||||
APPROVED: EVENT_NAMES.PERMISSIONS_APPROVED,
|
||||
REJECTED: EVENT_NAMES.PERMISSIONS_REJECTED,
|
||||
REQUESTED: EVENT_NAMES.PERMISSIONS_REQUESTED,
|
||||
APPROVED: MetaMetricsEventName.PermissionsApproved,
|
||||
REJECTED: MetaMetricsEventName.PermissionsRejected,
|
||||
REQUESTED: MetaMetricsEventName.PermissionsRequested,
|
||||
},
|
||||
};
|
||||
|
||||
@ -142,6 +145,8 @@ export default function createRPCMethodTrackingMiddleware({
|
||||
// keys for the various events in the flow.
|
||||
const eventType = EVENT_NAME_MAP[method];
|
||||
|
||||
const eventProperties = {};
|
||||
|
||||
// Boolean variable that reduces code duplication and increases legibility
|
||||
const shouldTrackEvent =
|
||||
// Don't track if the request came from our own UI or background
|
||||
@ -160,29 +165,32 @@ export default function createRPCMethodTrackingMiddleware({
|
||||
// 'Provider Method Called'.
|
||||
const event = eventType
|
||||
? eventType.REQUESTED
|
||||
: EVENT_NAMES.PROVIDER_METHOD_CALLED;
|
||||
: MetaMetricsEventName.ProviderMethodCalled;
|
||||
|
||||
const properties = {};
|
||||
if (event === MetaMetricsEventName.SignatureRequested) {
|
||||
eventProperties.signature_type = method;
|
||||
|
||||
let msgParams;
|
||||
|
||||
if (event === EVENT_NAMES.SIGNATURE_REQUESTED) {
|
||||
properties.signature_type = method;
|
||||
|
||||
const data = req?.params?.[0];
|
||||
const from = req?.params?.[1];
|
||||
// In personal messages the first param is data while in typed messages second param is data
|
||||
// if condition below is added to ensure that the right params are captured as data and address.
|
||||
let data;
|
||||
let from;
|
||||
if (isValidAddress(req?.params?.[1])) {
|
||||
data = req?.params?.[0];
|
||||
from = req?.params?.[1];
|
||||
} else {
|
||||
data = req?.params?.[1];
|
||||
from = req?.params?.[0];
|
||||
}
|
||||
const paramsExamplePassword = req?.params?.[2];
|
||||
|
||||
msgParams = {
|
||||
...paramsExamplePassword,
|
||||
from,
|
||||
data,
|
||||
origin,
|
||||
};
|
||||
|
||||
const msgData = {
|
||||
msgParams,
|
||||
status: 'unapproved',
|
||||
msgParams: {
|
||||
...paramsExamplePassword,
|
||||
from,
|
||||
data,
|
||||
origin,
|
||||
},
|
||||
status: TransactionStatus.unapproved,
|
||||
type: req.method,
|
||||
};
|
||||
|
||||
@ -193,25 +201,21 @@ export default function createRPCMethodTrackingMiddleware({
|
||||
);
|
||||
|
||||
if (securityProviderResponse?.flagAsDangerous === 1) {
|
||||
properties.ui_customizations = ['flagged_as_malicious'];
|
||||
eventProperties.ui_customizations = [
|
||||
MetaMetricsEventUiCustomization.FlaggedAsMalicious,
|
||||
];
|
||||
} else if (securityProviderResponse?.flagAsDangerous === 2) {
|
||||
properties.ui_customizations = ['flagged_as_safety_unknown'];
|
||||
} else {
|
||||
properties.ui_customizations = null;
|
||||
eventProperties.ui_customizations = [
|
||||
MetaMetricsEventUiCustomization.FlaggedAsSafetyUnknown,
|
||||
];
|
||||
}
|
||||
|
||||
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
|
||||
const { isSIWEMessage } = detectSIWE({ data });
|
||||
if (isSIWEMessage) {
|
||||
properties.ui_customizations === null
|
||||
? (properties.ui_customizations = [
|
||||
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
|
||||
.SIWE,
|
||||
])
|
||||
: properties.ui_customizations.push(
|
||||
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
|
||||
.SIWE,
|
||||
);
|
||||
eventProperties.ui_customizations = (
|
||||
eventProperties.ui_customizations || []
|
||||
).concat(MetaMetricsEventUiCustomization.Siwe);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@ -220,16 +224,16 @@ export default function createRPCMethodTrackingMiddleware({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
properties.method = method;
|
||||
eventProperties.method = method;
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
event,
|
||||
category: EVENT.CATEGORIES.INPAGE_PROVIDER,
|
||||
category: MetaMetricsEventCategory.InpageProvider,
|
||||
referrer: {
|
||||
url: origin,
|
||||
},
|
||||
properties,
|
||||
properties: eventProperties,
|
||||
});
|
||||
|
||||
rateLimitTimeouts[method] = setTimeout(() => {
|
||||
@ -242,8 +246,6 @@ export default function createRPCMethodTrackingMiddleware({
|
||||
return callback();
|
||||
}
|
||||
|
||||
const properties = {};
|
||||
|
||||
// The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings
|
||||
const isDisabledEthSignAdvancedSetting =
|
||||
method === MESSAGE_TYPE.ETH_SIGN &&
|
||||
@ -254,79 +256,20 @@ export default function createRPCMethodTrackingMiddleware({
|
||||
let event;
|
||||
if (isDisabledRPCMethod) {
|
||||
event = eventType.FAILED;
|
||||
properties.error = res.error;
|
||||
} else if (res.error?.code === 4001) {
|
||||
eventProperties.error = res.error;
|
||||
} else if (res.error?.code === errorCodes.provider.userRejectedRequest) {
|
||||
event = eventType.REJECTED;
|
||||
} else {
|
||||
event = eventType.APPROVED;
|
||||
}
|
||||
|
||||
let msgParams;
|
||||
|
||||
if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) {
|
||||
properties.signature_type = method;
|
||||
|
||||
const data = req?.params?.[0];
|
||||
const from = req?.params?.[1];
|
||||
const paramsExamplePassword = req?.params?.[2];
|
||||
|
||||
msgParams = {
|
||||
...paramsExamplePassword,
|
||||
from,
|
||||
data,
|
||||
origin,
|
||||
};
|
||||
|
||||
const msgData = {
|
||||
msgParams,
|
||||
status: 'unapproved',
|
||||
type: req.method,
|
||||
};
|
||||
|
||||
try {
|
||||
const securityProviderResponse = await securityProviderRequest(
|
||||
msgData,
|
||||
req.method,
|
||||
);
|
||||
|
||||
if (securityProviderResponse?.flagAsDangerous === 1) {
|
||||
properties.ui_customizations = ['flagged_as_malicious'];
|
||||
} else if (securityProviderResponse?.flagAsDangerous === 2) {
|
||||
properties.ui_customizations = ['flagged_as_safety_unknown'];
|
||||
} else {
|
||||
properties.ui_customizations = null;
|
||||
}
|
||||
|
||||
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
|
||||
const { isSIWEMessage } = detectSIWE({ data });
|
||||
if (isSIWEMessage) {
|
||||
properties.ui_customizations === null
|
||||
? (properties.ui_customizations = [
|
||||
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
|
||||
.SIWE,
|
||||
])
|
||||
: properties.ui_customizations.push(
|
||||
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
|
||||
.SIWE,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
properties.method = method;
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
event,
|
||||
category: EVENT.CATEGORIES.INPAGE_PROVIDER,
|
||||
category: MetaMetricsEventCategory.InpageProvider,
|
||||
referrer: {
|
||||
url: origin,
|
||||
},
|
||||
properties,
|
||||
properties: eventProperties,
|
||||
});
|
||||
|
||||
return callback();
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { errorCodes } from 'eth-rpc-errors';
|
||||
import { detectSIWE } from '@metamask/controller-utils';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import { EVENT_NAMES } from '../../../shared/constants/metametrics';
|
||||
import {
|
||||
MetaMetricsEventName,
|
||||
MetaMetricsEventUiCustomization,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import createRPCMethodTrackingMiddleware from './createRPCMethodTrackingMiddleware';
|
||||
|
||||
@ -52,6 +56,12 @@ function getNext(timeout = 500) {
|
||||
const waitForSeconds = async (seconds) =>
|
||||
await new Promise((resolve) => setTimeout(resolve, SECOND * seconds));
|
||||
|
||||
jest.mock('@metamask/controller-utils', () => ({
|
||||
detectSIWE: jest.fn().mockImplementation(() => {
|
||||
return { isSIWEMessage: false };
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('createRPCMethodTrackingMiddleware', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
@ -101,7 +111,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
metricsState.participateInMetaMetrics = true;
|
||||
});
|
||||
|
||||
it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => {
|
||||
it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
@ -115,7 +125,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
event: MetaMetricsEventName.SignatureRequested,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
},
|
||||
@ -123,7 +133,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`should track a ${EVENT_NAMES.SIGNATURE_APPROVED} event if the user approves`, async () => {
|
||||
it(`should track a ${MetaMetricsEventName.SignatureApproved} event if the user approves`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4,
|
||||
origin: 'some.dapp',
|
||||
@ -138,7 +148,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
expect(trackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(trackEvent.mock.calls[1][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_APPROVED,
|
||||
event: MetaMetricsEventName.SignatureApproved,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4,
|
||||
},
|
||||
@ -146,14 +156,14 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`should track a ${EVENT_NAMES.SIGNATURE_REJECTED} event if the user approves`, async () => {
|
||||
it(`should track a ${MetaMetricsEventName.SignatureRejected} event if the user approves`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.PERSONAL_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
|
||||
const res = {
|
||||
error: { code: 4001 },
|
||||
error: { code: errorCodes.provider.userRejectedRequest },
|
||||
};
|
||||
const { next, executeMiddlewareStack } = getNext();
|
||||
await handler(req, res, next);
|
||||
@ -161,7 +171,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
expect(trackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(trackEvent.mock.calls[1][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_REJECTED,
|
||||
event: MetaMetricsEventName.SignatureRejected,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.PERSONAL_SIGN,
|
||||
},
|
||||
@ -169,7 +179,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`should track a ${EVENT_NAMES.PERMISSIONS_APPROVED} event if the user approves`, async () => {
|
||||
it(`should track a ${MetaMetricsEventName.PermissionsApproved} event if the user approves`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS,
|
||||
origin: 'some.dapp',
|
||||
@ -182,7 +192,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
expect(trackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(trackEvent.mock.calls[1][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.PERMISSIONS_APPROVED,
|
||||
event: MetaMetricsEventName.PermissionsApproved,
|
||||
properties: { method: MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS },
|
||||
referrer: { url: 'some.dapp' },
|
||||
});
|
||||
@ -230,8 +240,38 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
expect(trackEvent.mock.calls[1][0].properties.method).toBe('eth_chainId');
|
||||
});
|
||||
|
||||
it('should track Sign-in With Ethereum (SIWE) message if detected', async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.PERSONAL_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next, executeMiddlewareStack } = getNext();
|
||||
|
||||
detectSIWE.mockImplementation(() => {
|
||||
return { isSIWEMessage: true };
|
||||
});
|
||||
|
||||
await handler(req, res, next);
|
||||
await executeMiddlewareStack();
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(trackEvent.mock.calls[1][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: MetaMetricsEventName.SignatureApproved,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.PERSONAL_SIGN,
|
||||
ui_customizations: [MetaMetricsEventUiCustomization.Siwe],
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when '${MESSAGE_TYPE.ETH_SIGN}' is disabled in advanced settings`, () => {
|
||||
it(`should track ${EVENT_NAMES.SIGNATURE_FAILED} and include error property`, async () => {
|
||||
it(`should track ${MetaMetricsEventName.SignatureFailed} and include error property`, async () => {
|
||||
const mockError = { code: errorCodes.rpc.methodNotFound };
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
@ -249,7 +289,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
|
||||
expect(trackEvent.mock.calls[1][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_FAILED,
|
||||
event: MetaMetricsEventName.SignatureFailed,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
error: mockError,
|
||||
@ -258,93 +298,150 @@ describe('createRPCMethodTrackingMiddleware', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('participateInMetaMetrics is set to true with a request flagged as safe', () => {
|
||||
beforeEach(() => {
|
||||
metricsState.participateInMetaMetrics = true;
|
||||
});
|
||||
describe('when request is flagged as safe by security provider', () => {
|
||||
it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next } = getNext();
|
||||
|
||||
it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safe`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
await handler(req, res, next);
|
||||
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next } = getNext();
|
||||
await handler(req, res, next);
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
ui_customizations: null,
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: MetaMetricsEventName.SignatureRequested,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('participateInMetaMetrics is set to true with a request flagged as malicious', () => {
|
||||
beforeEach(() => {
|
||||
metricsState.participateInMetaMetrics = true;
|
||||
flagAsDangerous = 1;
|
||||
});
|
||||
describe('when request is flagged as malicious by security provider', () => {
|
||||
beforeEach(() => {
|
||||
flagAsDangerous = 1;
|
||||
});
|
||||
|
||||
it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event which is flagged as malicious`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next } = getNext();
|
||||
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next } = getNext();
|
||||
await handler(req, res, next);
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
ui_customizations: ['flagged_as_malicious'],
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
await handler(req, res, next);
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: MetaMetricsEventName.SignatureRequested,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
ui_customizations: ['flagged_as_malicious'],
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('participateInMetaMetrics is set to true with a request flagged as safety unknown', () => {
|
||||
beforeEach(() => {
|
||||
metricsState.participateInMetaMetrics = true;
|
||||
flagAsDangerous = 2;
|
||||
describe('when request flagged as safety unknown by security provider', () => {
|
||||
beforeEach(() => {
|
||||
flagAsDangerous = 2;
|
||||
});
|
||||
|
||||
it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event which is flagged as safety unknown`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next } = getNext();
|
||||
|
||||
await handler(req, res, next);
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: MetaMetricsEventName.SignatureRequested,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
ui_customizations: ['flagged_as_safety_unknown'],
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => {
|
||||
const req = {
|
||||
method: MESSAGE_TYPE.ETH_SIGN,
|
||||
origin: 'some.dapp',
|
||||
};
|
||||
describe('when signature requests are received', () => {
|
||||
let securityProviderReq, fnHandler;
|
||||
beforeEach(() => {
|
||||
securityProviderReq = jest.fn().mockReturnValue(() =>
|
||||
Promise.resolve({
|
||||
flagAsDangerous: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const res = {
|
||||
error: null,
|
||||
};
|
||||
const { next } = getNext();
|
||||
await handler(req, res, next);
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(trackEvent.mock.calls[0][0]).toMatchObject({
|
||||
category: 'inpage_provider',
|
||||
event: EVENT_NAMES.SIGNATURE_REQUESTED,
|
||||
properties: {
|
||||
signature_type: MESSAGE_TYPE.ETH_SIGN,
|
||||
ui_customizations: ['flagged_as_safety_unknown'],
|
||||
},
|
||||
referrer: { url: 'some.dapp' },
|
||||
fnHandler = createRPCMethodTrackingMiddleware({
|
||||
trackEvent,
|
||||
getMetricsState,
|
||||
rateLimitSeconds: 1,
|
||||
securityProviderRequest: securityProviderReq,
|
||||
});
|
||||
});
|
||||
it(`should pass correct data for personal sign`, async () => {
|
||||
const req = {
|
||||
method: 'personal_sign',
|
||||
params: [
|
||||
'0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765',
|
||||
'0x8eeee1781fd885ff5ddef7789486676961873d12',
|
||||
'Example password',
|
||||
],
|
||||
jsonrpc: '2.0',
|
||||
id: 1142196570,
|
||||
origin: 'https://metamask.github.io',
|
||||
tabId: 1048582817,
|
||||
};
|
||||
const res = { id: 1142196570, jsonrpc: '2.0' };
|
||||
const { next } = getNext();
|
||||
|
||||
await fnHandler(req, res, next);
|
||||
|
||||
expect(securityProviderReq).toHaveBeenCalledTimes(1);
|
||||
const call = securityProviderReq.mock.calls[0][0];
|
||||
expect(call.msgParams.data).toStrictEqual(req.params[0]);
|
||||
});
|
||||
it(`should pass correct data for typed sign`, async () => {
|
||||
const req = {
|
||||
method: 'eth_signTypedData_v4',
|
||||
params: [
|
||||
'0x8eeee1781fd885ff5ddef7789486676961873d12',
|
||||
'{"domain":{"chainId":"5","name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Group":[{"name":"name","type":"string"},{"name":"members","type":"Person[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}',
|
||||
],
|
||||
jsonrpc: '2.0',
|
||||
id: 1142196571,
|
||||
origin: 'https://metamask.github.io',
|
||||
tabId: 1048582817,
|
||||
};
|
||||
const res = { id: 1142196571, jsonrpc: '2.0' };
|
||||
const { next } = getNext();
|
||||
|
||||
await fnHandler(req, res, next);
|
||||
|
||||
expect(securityProviderReq).toHaveBeenCalledTimes(1);
|
||||
const call = securityProviderReq.mock.calls[0][0];
|
||||
expect(call.msgParams.data).toStrictEqual(req.params[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ import { bufferToHex } from 'ethereumjs-util';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import log from 'loglevel';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import { EVENT } from '../../../shared/constants/metametrics';
|
||||
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
|
||||
import createId from '../../../shared/modules/random-id';
|
||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
||||
@ -237,7 +237,7 @@ export default class DecryptMessageManager extends EventEmitter {
|
||||
if (reason) {
|
||||
this.metricsEvent({
|
||||
event: reason,
|
||||
category: EVENT.CATEGORIES.MESSAGES,
|
||||
category: MetaMetricsEventCategory.Messages,
|
||||
properties: {
|
||||
action: 'Decrypt Message Request',
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import { ObservableStore } from '@metamask/obs-store';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import log from 'loglevel';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import { EVENT } from '../../../shared/constants/metametrics';
|
||||
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
|
||||
import createId from '../../../shared/modules/random-id';
|
||||
|
||||
@ -225,7 +225,7 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
|
||||
if (reason) {
|
||||
this.metricsEvent({
|
||||
event: reason,
|
||||
category: EVENT.CATEGORIES.MESSAGES,
|
||||
category: MetaMetricsEventCategory.Messages,
|
||||
properties: {
|
||||
action: 'Encryption public key Request',
|
||||
},
|
||||
|
@ -1,329 +0,0 @@
|
||||
import EventEmitter from 'events';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import { bufferToHex } from 'ethereumjs-util';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
|
||||
import createId from '../../../shared/modules/random-id';
|
||||
import { EVENT } from '../../../shared/constants/metametrics';
|
||||
|
||||
/**
|
||||
* Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for
|
||||
* an eth_sign call is requested.
|
||||
*
|
||||
* @see {@link https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign}
|
||||
* @typedef {object} Message
|
||||
* @property {number} id An id to track and identify the message object
|
||||
* @property {object} msgParams The parameters to pass to the eth_sign method once the signature request is approved.
|
||||
* @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
|
||||
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
|
||||
* @property {number} time The epoch time at which the this message was created
|
||||
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
|
||||
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' with
|
||||
* always have a 'eth_sign' type.
|
||||
*/
|
||||
|
||||
export default class MessageManager extends EventEmitter {
|
||||
/**
|
||||
* Controller in charge of managing - storing, adding, removing, updating - Messages.
|
||||
*
|
||||
* @param {object} opts - Controller options
|
||||
* @param {Function} opts.metricsEvent - A function for emitting a metric event.
|
||||
* @param {Function} opts.securityProviderRequest - A function for verifying a message, whether it is malicious or not.
|
||||
*/
|
||||
constructor({ metricsEvent, securityProviderRequest }) {
|
||||
super();
|
||||
this.memStore = new ObservableStore({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedMsgCount: 0,
|
||||
});
|
||||
|
||||
this.resetState = () => {
|
||||
this.memStore.updateState({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedMsgCount: 0,
|
||||
});
|
||||
};
|
||||
|
||||
this.messages = [];
|
||||
this.metricsEvent = metricsEvent;
|
||||
this.securityProviderRequest = securityProviderRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the number of 'unapproved' Messages in this.messages
|
||||
*
|
||||
* @returns {number} The number of 'unapproved' Messages in this.messages
|
||||
*/
|
||||
get unapprovedMsgCount() {
|
||||
return Object.keys(this.getUnapprovedMsgs()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the 'unapproved' Messages in this.messages
|
||||
*
|
||||
* @returns {object} An index of Message ids to Messages, for all 'unapproved' Messages in this.messages
|
||||
*/
|
||||
getUnapprovedMsgs() {
|
||||
return this.messages
|
||||
.filter((msg) => msg.status === 'unapproved')
|
||||
.reduce((result, msg) => {
|
||||
result[msg.id] = msg;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the
|
||||
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
|
||||
*
|
||||
* @param {object} msgParams - The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {object} [req] - The original request object possibly containing the origin
|
||||
* @returns {promise} after signature has been
|
||||
*/
|
||||
async addUnapprovedMessageAsync(msgParams, req) {
|
||||
const msgId = await this.addUnapprovedMessage(msgParams, req);
|
||||
return await new Promise((resolve, reject) => {
|
||||
// await finished
|
||||
this.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return resolve(data.rawSig);
|
||||
case 'rejected':
|
||||
return reject(
|
||||
ethErrors.provider.userRejectedRequest(
|
||||
'MetaMask Message Signature: User denied message signature.',
|
||||
),
|
||||
);
|
||||
case 'errored':
|
||||
return reject(
|
||||
new Error(`MetaMask Message Signature: ${data.error}`),
|
||||
);
|
||||
default:
|
||||
return reject(
|
||||
new Error(
|
||||
`MetaMask Message Signature: Unknown problem: ${JSON.stringify(
|
||||
msgParams,
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the
|
||||
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
|
||||
*
|
||||
* @param {object} msgParams - The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {object} [req] - The original request object where the origin may be specified
|
||||
* @returns {number} The id of the newly created message.
|
||||
*/
|
||||
async addUnapprovedMessage(msgParams, req) {
|
||||
// add origin from request
|
||||
if (req) {
|
||||
msgParams.origin = req.origin;
|
||||
}
|
||||
msgParams.data = normalizeMsgData(msgParams.data);
|
||||
// create txData obj with parameters and meta data
|
||||
const time = new Date().getTime();
|
||||
const msgId = createId();
|
||||
const msgData = {
|
||||
id: msgId,
|
||||
msgParams,
|
||||
time,
|
||||
status: 'unapproved',
|
||||
type: MESSAGE_TYPE.ETH_SIGN,
|
||||
};
|
||||
this.addMsg(msgData);
|
||||
|
||||
const securityProviderResponse = await this.securityProviderRequest(
|
||||
msgData,
|
||||
msgData.type,
|
||||
);
|
||||
|
||||
msgData.securityProviderResponse = securityProviderResponse;
|
||||
|
||||
// signal update
|
||||
this.emit('update');
|
||||
return msgId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a passed Message to this.messages, and calls this._saveMsgList() to save the unapproved Messages from that
|
||||
* list to this.memStore.
|
||||
*
|
||||
* @param {Message} msg - The Message to add to this.messages
|
||||
*/
|
||||
addMsg(msg) {
|
||||
this.messages.push(msg);
|
||||
this._saveMsgList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specified Message.
|
||||
*
|
||||
* @param {number} msgId - The id of the Message to get
|
||||
* @returns {Message|undefined} The Message with the id that matches the passed msgId, or undefined if no Message has that id.
|
||||
*/
|
||||
getMsg(msgId) {
|
||||
return this.messages.find((msg) => msg.id === msgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approves a Message. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise with
|
||||
* any the message params modified for proper signing.
|
||||
*
|
||||
* @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask.
|
||||
* @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask.
|
||||
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
|
||||
*/
|
||||
approveMessage(msgParams) {
|
||||
this.setMsgStatusApproved(msgParams.metamaskId);
|
||||
return this.prepMsgForSigning(msgParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Message status to 'approved' via a call to this._setMsgStatus.
|
||||
*
|
||||
* @param {number} msgId - The id of the Message to approve.
|
||||
*/
|
||||
setMsgStatusApproved(msgId) {
|
||||
this._setMsgStatus(msgId, 'approved');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Message status to 'signed' via a call to this._setMsgStatus and updates that Message in this.messages by
|
||||
* adding the raw signature data of the signature request to the Message
|
||||
*
|
||||
* @param {number} msgId - The id of the Message to sign.
|
||||
* @param {buffer} rawSig - The raw data of the signature request
|
||||
*/
|
||||
setMsgStatusSigned(msgId, rawSig) {
|
||||
const msg = this.getMsg(msgId);
|
||||
msg.rawSig = rawSig;
|
||||
this._updateMsg(msg);
|
||||
this._setMsgStatus(msgId, 'signed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
|
||||
*
|
||||
* @param {object} msgParams - The msgParams to modify
|
||||
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
|
||||
*/
|
||||
async prepMsgForSigning(msgParams) {
|
||||
delete msgParams.metamaskId;
|
||||
return msgParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Message status to 'rejected' via a call to this._setMsgStatus.
|
||||
*
|
||||
* @param {number} msgId - The id of the Message to reject.
|
||||
* @param reason
|
||||
*/
|
||||
rejectMsg(msgId, reason = undefined) {
|
||||
if (reason) {
|
||||
const msg = this.getMsg(msgId);
|
||||
this.metricsEvent({
|
||||
event: reason,
|
||||
category: EVENT.CATEGORIES.TRANSACTIONS,
|
||||
properties: {
|
||||
action: 'Sign Request',
|
||||
type: msg.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
this._setMsgStatus(msgId, 'rejected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Message status to 'errored' via a call to this._setMsgStatus.
|
||||
*
|
||||
* @param {number} msgId - The id of the Message to error
|
||||
* @param error
|
||||
*/
|
||||
errorMessage(msgId, error) {
|
||||
const msg = this.getMsg(msgId);
|
||||
msg.error = error;
|
||||
this._updateMsg(msg);
|
||||
this._setMsgStatus(msgId, 'errored');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all unapproved messages from memory.
|
||||
*/
|
||||
clearUnapproved() {
|
||||
this.messages = this.messages.filter((msg) => msg.status !== 'unapproved');
|
||||
this._saveMsgList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the status of a Message in this.messages via a call to this._updateMsg
|
||||
*
|
||||
* @private
|
||||
* @param {number} msgId - The id of the Message to update.
|
||||
* @param {string} status - The new status of the Message.
|
||||
* @throws A 'MessageManager - Message not found for id: "${msgId}".' if there is no Message in this.messages with an
|
||||
* id equal to the passed msgId
|
||||
* @fires An event with a name equal to `${msgId}:${status}`. The Message is also fired.
|
||||
* @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along with the message
|
||||
*/
|
||||
_setMsgStatus(msgId, status) {
|
||||
const msg = this.getMsg(msgId);
|
||||
if (!msg) {
|
||||
throw new Error(`MessageManager - Message not found for id: "${msgId}".`);
|
||||
}
|
||||
msg.status = status;
|
||||
this._updateMsg(msg);
|
||||
this.emit(`${msgId}:${status}`, msg);
|
||||
if (status === 'rejected' || status === 'signed') {
|
||||
this.emit(`${msgId}:finished`, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Message in this.messages to the passed Message if the ids are equal. Then saves the unapprovedMsg list to
|
||||
* storage via this._saveMsgList
|
||||
*
|
||||
* @private
|
||||
* @param {Message} msg - A Message that will replace an existing Message (with the same id) in this.messages
|
||||
*/
|
||||
_updateMsg(msg) {
|
||||
const index = this.messages.findIndex((message) => message.id === msg.id);
|
||||
if (index !== -1) {
|
||||
this.messages[index] = msg;
|
||||
}
|
||||
this._saveMsgList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the unapproved messages, and their count, to this.memStore
|
||||
*
|
||||
* @private
|
||||
* @fires 'updateBadge'
|
||||
*/
|
||||
_saveMsgList() {
|
||||
const unapprovedMsgs = this.getUnapprovedMsgs();
|
||||
const unapprovedMsgCount = Object.keys(unapprovedMsgs).length;
|
||||
this.memStore.updateState({ unapprovedMsgs, unapprovedMsgCount });
|
||||
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex.
|
||||
*
|
||||
* @param {any} data - The buffer data to convert to a hex
|
||||
* @returns {string} A hex string conversion of the buffer data
|
||||
*/
|
||||
export function normalizeMsgData(data) {
|
||||
if (data.slice(0, 2) === '0x') {
|
||||
// data is already hex
|
||||
return data;
|
||||
}
|
||||
// data is unicode, convert to hex
|
||||
return bufferToHex(Buffer.from(data, 'utf8'));
|
||||
}
|