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

Merge remote-tracking branch 'origin/develop' into master-sync

This commit is contained in:
PeterYinusa 2023-04-04 11:35:15 +01:00
commit fc620d4de6
619 changed files with 28034 additions and 13919 deletions

View File

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

View File

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

View File

@ -38,6 +38,9 @@ addParameters({
],
},
},
controls: {
expanded: true,
},
});
export const globalTypes = {

View File

@ -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': {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Zeigen"
},
"showAdvancedGasInline": {
"message": "Erweiterte Gaskontrollen"
},
"showAdvancedGasInlineDescription": {
"message": "Wählen Sie dies aus, um den Gaspreis und die Limitkontrollen direkt auf den Senden- und Bestätigen-Bildschirmen anzuzeigen."
},
"showFiatConversionInTestnets": {
"message": "Umwandlung auf Testnets anzeigen"
},
@ -3364,23 +3349,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 ..."
},
@ -3397,7 +3371,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."

View File

@ -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": "Μέγ."
},
@ -2188,9 +2182,6 @@
"message": "Το Nonce είναι υψηλότερο από το προτεινόμενο nonce του $1",
"description": "The next nonce according to MetaMask's internal logic"
},
"nft": {
"message": "NFT"
},
"nftAddFailedMessage": {
"message": "Τα NFT δεν μπορούν να προστεθούν, διότι τα στοιχεία της κυριότητας δεν ταυτίζονται. Σιγουρευτείτε ότι έχετε εισαγάγει τα σωστά στοιχεία."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Εμφάνιση"
},
"showAdvancedGasInline": {
"message": "Προωθημένος έλεγχος gas"
},
"showAdvancedGasInlineDescription": {
"message": "Επιλέξτε αυτό για να εμφανίσετε τις τιμές αερίου και να περιορίσετε τα στοιχεία ελέγχου απευθείας στις οθόνες αποστολής και επιβεβαίωσης."
},
"showFiatConversionInTestnets": {
"message": "Εμφάνιση Μετατροπής σε Δοκιμαστικά Δίκτυα"
},
@ -3364,23 +3349,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": "Φόρτωση πληροφοριών συναλλαγών..."
},
@ -3397,7 +3371,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": "Ορισμένα δίκτυα ενδέχεται να ενέχουν κινδύνους για την ασφάλεια ή/και το απόρρητο. Ενημερωθείτε για τους κινδύνους πριν προσθέσετε και χρησιμοποιήσετε ένα δίκτυο."

View File

@ -103,6 +103,9 @@
"SIWEWarningTitle": {
"message": "Are you sure?"
},
"ShowMore": {
"message": "Show more"
},
"about": {
"message": "About"
},
@ -150,6 +153,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
"activated": {
"message": "Active"
},
"active": {
"message": "Active"
},
@ -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,39 @@
"close": {
"message": "Close"
},
"codefiCompliance": {
"message": "Codefi Compliance"
},
"coingecko": {
"message": "CoinGecko"
},
"complianceBlurb0": {
"message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties."
},
"complianceBlurb1": {
"message": "Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting."
},
"complianceBlurbStep1": {
"message": "Sign up to Codefi Compliance below"
},
"complianceBlurbStep2": {
"message": "Create an organisation"
},
"complianceBlurbStep3": {
"message": "Create a project"
},
"complianceBlurbStep4": {
"message": "Set your compliance settings"
},
"complianceBlurbStep5": {
"message": "Click the \"Enable Compliance in MMI\" button"
},
"complianceBlurpStep0": {
"message": "Steps to enable AML/CFT Compliance:"
},
"complianceSettingsExplanation": {
"message": "Change your settings or view reports by opening up Codefi Compliance or disconnect below."
},
"confirm": {
"message": "Confirm"
},
@ -857,6 +914,12 @@
"curveMediumGasEstimate": {
"message": "Market gas estimate graph"
},
"custodian": {
"message": "Custodian"
},
"custodianAccount": {
"message": "Custodian account"
},
"custom": {
"message": "Advanced"
},
@ -1311,9 +1374,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 +1397,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 +1441,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 +1620,9 @@
"hardware": {
"message": "Hardware"
},
"hardwareWallet": {
"message": "Hardware wallet"
},
"hardwareWalletConnected": {
"message": "Hardware wallet connected"
},
@ -1725,6 +1805,10 @@
"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."
},
"insightsFromSnap": {
"message": "Insights from $1",
"description": "$1 represents the name of the snap"
},
"install": {
"message": "Install"
},
@ -1940,6 +2024,9 @@
"lock": {
"message": "Lock"
},
"lockMetaMask": {
"message": "Lock MetaMask"
},
"lockTimeTooGreat": {
"message": "Lock time is too great"
},
@ -1976,9 +2063,6 @@
"malformedData": {
"message": "Malformed data"
},
"manageSnaps": {
"message": "Manage your installed snaps"
},
"max": {
"message": "Max"
},
@ -2054,6 +2138,9 @@
"missingToken": {
"message": "Don't see your token?"
},
"mmiAddToken": {
"message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional"
},
"mobileSyncWarning": {
"message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile."
},
@ -2105,6 +2192,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 +2305,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 cant be added as the ownership details do not match. Make sure you have entered correct information."
},
@ -2270,7 +2357,7 @@
"message": "No NFTs yet"
},
"noSnaps": {
"message": "No Snaps installed"
"message": "You don't have any snaps installed."
},
"noThanksVariant2": {
"message": "No, thanks."
@ -2655,6 +2742,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."
@ -2764,22 +2854,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"
@ -2788,22 +2898,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''."
@ -2812,22 +2942,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."
@ -2836,6 +2986,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'."
@ -2844,6 +2998,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"
},
@ -2863,6 +3021,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"
@ -3019,6 +3180,9 @@
"replace": {
"message": "replace"
},
"requestFailed": {
"message": "Request failed"
},
"requestFlaggedAsMaliciousFallbackCopyReason": {
"message": "The security provider has not shared additional details"
},
@ -3120,16 +3284,24 @@
"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"
@ -3251,6 +3423,9 @@
"selectHdPath": {
"message": "Select HD path"
},
"selectJWT": {
"message": "Select token"
},
"selectNFTPrivacyPreference": {
"message": "Turn on NFT detection in Settings"
},
@ -3323,12 +3498,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"
},
@ -3418,36 +3587,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"
},
@ -3461,7 +3654,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."
@ -3570,6 +3764,9 @@
"statusNotConnected": {
"message": "Not connected"
},
"statusNotConnectedAccount": {
"message": "No accounts connected"
},
"step1LatticeWallet": {
"message": "Connect your Lattice1"
},
@ -3899,7 +4096,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."
@ -4086,6 +4283,9 @@
"thingsToKeep": {
"message": "Things to keep in mind:"
},
"thisCollection": {
"message": "this collection"
},
"thisIsBasedOn": {
"message": "This is based on information from "
},
@ -4165,6 +4365,12 @@
"tooltipApproveButton": {
"message": "I understand"
},
"tooltipSatusConnected": {
"message": "connected"
},
"tooltipSatusNotConnected": {
"message": "not connected"
},
"total": {
"message": "Total"
},
@ -4378,6 +4584,9 @@
"upArrow": {
"message": "up arrow"
},
"update": {
"message": "Update"
},
"updatedWithDate": {
"message": "Updated $1"
},
@ -4473,6 +4682,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"
},

View File

@ -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."
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Mostrar"
},
"showAdvancedGasInline": {
"message": "Controles avanzados de gas"
},
"showAdvancedGasInlineDescription": {
"message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación."
},
"showFiatConversionInTestnets": {
"message": "Mostrar conversión en redes de prueba"
},
@ -3364,23 +3349,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..."
},
@ -3397,7 +3371,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."

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Afficher"
},
"showAdvancedGasInline": {
"message": "Contrôles de carburant avancés"
},
"showAdvancedGasInlineDescription": {
"message": "Sélectionnez cette option pour afficher le prix du carburant et les contrôles des limites directement sur les écrans denvoi et de confirmation."
},
"showFiatConversionInTestnets": {
"message": "Afficher la conversion sur les testnets"
},
@ -3364,23 +3349,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 dune 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 sest 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 laperçu de transaction…"
},
@ -3397,7 +3371,8 @@
"message": "Un snap ne sexécute que sil est activé"
},
"snapsUIError": {
"message": "Linterface utilisateur (IU) spécifiée par le snap nest pas valide."
"message": "Linterface utilisateur (IU) spécifiée par le snap nest 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 dajouter et dutiliser un réseau."

View File

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

View File

@ -1317,9 +1317,6 @@
"etherscanViewOn": {
"message": "Etherscan पर देखें"
},
"expandExperience": {
"message": "MetaMask स्नैप्स के साथ अपने web3 अनुभव का विस्तार करें"
},
"expandView": {
"message": "दृश्य का विस्तार करें"
},
@ -1952,9 +1949,6 @@
"malformedData": {
"message": "विकृत डेटा"
},
"manageSnaps": {
"message": "अपने इंस्टाल किए गए स्नैप्स मैनेज करें"
},
"max": {
"message": "अधिकतम"
},
@ -2188,9 +2182,6 @@
"message": "नॉन्स $1 के सुझाए गए नॉन्स से अधिक है",
"description": "The next nonce according to MetaMask's internal logic"
},
"nft": {
"message": "एनएफटी"
},
"nftAddFailedMessage": {
"message": "एनएफटी जोड़ा नहीं जा सकता क्योंकि स्वामित्व विवरण मेल नहीं खा रहे हैं। सुनिश्चित करें कि आपने सही जानकारी दर्ज की है।"
},
@ -3259,12 +3250,6 @@
"show": {
"message": "दिखाएं"
},
"showAdvancedGasInline": {
"message": "उन्नत गैस नियंत्रण"
},
"showAdvancedGasInlineDescription": {
"message": "गैस मूल्य और सीमा नियंत्रण को सीधे भेजने और पुष्टि करने की स्क्रीन पर दिखाने के लिए इसका चयन करें।"
},
"showFiatConversionInTestnets": {
"message": "टेस्ट नेटवर्क पर रूपांतरण दिखाएं"
},
@ -3364,23 +3349,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": "ट्रांजैक्शन इनसाइट लोड हो रही है..."
},
@ -3397,7 +3371,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": "कुछ नेटवर्क सुरक्षा और/या गोपनीयता संबंधी जोखिम पैदा कर सकते हैं। नेटवर्क जोड़ने और उपयोग करने से पहले जोखिमों को समझें।"

View File

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

View File

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

View File

@ -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"
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Tampil"
},
"showAdvancedGasInline": {
"message": "Kontrol gas lanjutan"
},
"showAdvancedGasInlineDescription": {
"message": "Pilih ini untuk menampilkan biaya gas dan kontrol batas secara langsung di layar kirim dan konfirmasi."
},
"showFiatConversionInTestnets": {
"message": "Tampilkan konversi di jaringan uji"
},
@ -3364,23 +3349,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..."
},
@ -3397,7 +3371,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."

View File

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

View File

@ -1317,9 +1317,6 @@
"etherscanViewOn": {
"message": "Etherscanで表示"
},
"expandExperience": {
"message": "MetaMask Snaps で web3 エクスペリエンスを拡張"
},
"expandView": {
"message": "ビューを展開"
},
@ -1952,9 +1949,6 @@
"malformedData": {
"message": "不正な形式のデータ"
},
"manageSnaps": {
"message": "インストールされたスナップの管理"
},
"max": {
"message": "最大"
},
@ -2188,9 +2182,6 @@
"message": "ナンスが提案され$1よりも大きいです",
"description": "The next nonce according to MetaMask's internal logic"
},
"nft": {
"message": "NFT"
},
"nftAddFailedMessage": {
"message": "所有者情報が一致していないため、NFT を追加できません。入力された情報が正しいことを確認してください。"
},
@ -3259,12 +3250,6 @@
"show": {
"message": "表示"
},
"showAdvancedGasInline": {
"message": "高度なガスコントロール"
},
"showAdvancedGasInlineDescription": {
"message": "これを選択すると、ガス代と限度額のコントロールが送金画面と確認画面に直接表示されます。"
},
"showFiatConversionInTestnets": {
"message": "テストネット上に変換を表示"
},
@ -3364,23 +3349,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": "トランザクションインサイトを読み込み中..."
},
@ -3397,7 +3371,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": "ネットワークによっては、セキュリティやプライバシーの面でリスクが伴う可能性があります。ネットワークを追加・使用する前にリスクを理解するようにしてください。"

View File

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

View File

@ -1317,9 +1317,6 @@
"etherscanViewOn": {
"message": "Etherscan에서 보기"
},
"expandExperience": {
"message": "MetaMask 스냅으로 web3 경험을 확대하세요"
},
"expandView": {
"message": "보기 확장"
},
@ -1952,9 +1949,6 @@
"malformedData": {
"message": "잘못된 데이터"
},
"manageSnaps": {
"message": "설치된 스냅을 관리하세요"
},
"max": {
"message": "최대"
},
@ -2188,9 +2182,6 @@
"message": "임시값이 권장 임시값인 $1보다 큽니다.",
"description": "The next nonce according to MetaMask's internal logic"
},
"nft": {
"message": "NFT\n"
},
"nftAddFailedMessage": {
"message": "소유권 정보가 일치하지 않아 NFT를 추가할 수 없습니다. 올바른 정보를 입력했는지 확인하세요."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "보기"
},
"showAdvancedGasInline": {
"message": "고급 가스 제어 기능"
},
"showAdvancedGasInlineDescription": {
"message": "이 항목을 선택하면 보내기 및 확인 화면에서 바로 가스 가격과 한도 조절을 확인할 수 있습니다."
},
"showFiatConversionInTestnets": {
"message": "테스트넷에 전환 표시"
},
@ -3364,23 +3349,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": "거래 인사이트를 가져오는 중..."
},
@ -3397,7 +3371,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": "네트워크에 따라 보안이나 개인 정보 유출의 위험이 있을 수 있습니다. 네트워크 추가 및 사용 이전에 위험 요소를 파악하세요."

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Exibir"
},
"showAdvancedGasInline": {
"message": "Controles avançados de gás"
},
"showAdvancedGasInlineDescription": {
"message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação."
},
"showFiatConversionInTestnets": {
"message": "Mostrar conversão nas redes de teste"
},
@ -3364,23 +3349,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..."
},
@ -3397,7 +3371,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."

View File

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

View File

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

View File

@ -1317,9 +1317,6 @@
"etherscanViewOn": {
"message": "Посмотреть на Etherscan"
},
"expandExperience": {
"message": "Расширьте свои возможности web3 с помощью MetaMask Snaps"
},
"expandView": {
"message": "Развернуть представление"
},
@ -1952,9 +1949,6 @@
"malformedData": {
"message": "Искаженные данные"
},
"manageSnaps": {
"message": "Управляйте установленными снапами"
},
"max": {
"message": "Макс."
},
@ -2188,9 +2182,6 @@
"message": "Одноразовый номер больше, чем предложенный одноразовый номер $1",
"description": "The next nonce according to MetaMask's internal logic"
},
"nft": {
"message": "NFT"
},
"nftAddFailedMessage": {
"message": "Невозможно добавить NFT, так как сведения о владельце не совпадают. Убедитесь, что вы ввели правильную информацию."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Показать"
},
"showAdvancedGasInline": {
"message": "Расширенное управление газом"
},
"showAdvancedGasInlineDescription": {
"message": "Выберите это, чтобы отображать цену газа и управление лимитами непосредственно на экранах отправки и подтверждения."
},
"showFiatConversionInTestnets": {
"message": "Показывать конвертацию в тестовых сетях"
},
@ -3364,23 +3349,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": "Загрузка аналитики по транзакции..."
},
@ -3397,7 +3371,8 @@
"message": "Снап будет работать только в том случае, если он включен"
},
"snapsUIError": {
"message": "Пользовательский интерфейс, указанный привязкой, недействителен."
"message": "Пользовательский интерфейс, указанный привязкой, недействителен.",
"description": "This is shown when the insight snap throws an error. $1 is the snap name"
},
"someNetworksMayPoseSecurity": {
"message": "Некоторые сети могут представлять угрозу безопасности и/или конфиденциальности. Прежде чем добавлять и использовать сеть, ознакомьтесь с рисками."

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Ipakita"
},
"showAdvancedGasInline": {
"message": "Mga advanced na kontrol sa gas"
},
"showAdvancedGasInlineDescription": {
"message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma."
},
"showFiatConversionInTestnets": {
"message": "Ipakita ang Conversion sa Testnets"
},
@ -3364,23 +3349,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..."
},
@ -3397,7 +3371,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."

View File

@ -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"
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Göster"
},
"showAdvancedGasInline": {
"message": "Gelişmiş gaz kontrolleri"
},
"showAdvancedGasInlineDescription": {
"message": "Gaz fiyatı ve limit kontrollerini doğrudan gönder ve onayla ekranlarında göstermek için bunu seçin."
},
"showFiatConversionInTestnets": {
"message": "Test ağlarında dönüşümü göster"
},
@ -3364,23 +3349,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..."
},
@ -3397,7 +3371,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."

View File

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

View File

@ -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"
},
@ -2188,9 +2182,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."
},
@ -3259,12 +3250,6 @@
"show": {
"message": "Hiển thị"
},
"showAdvancedGasInline": {
"message": "Quyền kiểm soát gas nâng cao"
},
"showAdvancedGasInlineDescription": {
"message": "Chọn tùy chọn này để hiển thị các quyền kiểm soát giá gas và giới hạn ngay trên màn hình gửi và xác nhận."
},
"showFiatConversionInTestnets": {
"message": "Hiển thị tỷ lệ quy đổi trên các mạng thử nghiệm"
},
@ -3364,23 +3349,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..."
},
@ -3397,7 +3371,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."

View File

@ -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": "最大"
},
@ -2188,9 +2182,6 @@
"message": "Nonce 高于建议的 nouce 值 $1",
"description": "The next nonce according to MetaMask's internal logic"
},
"nft": {
"message": "非同质化代币(NFT)"
},
"nftAddFailedMessage": {
"message": "由于所有权信息不匹配无法添加NFT。请确保所输入的信息正确无误。"
},
@ -3259,12 +3250,6 @@
"show": {
"message": "显示"
},
"showAdvancedGasInline": {
"message": "高级燃料控制"
},
"showAdvancedGasInlineDescription": {
"message": "选择此项可直接在发送和确认界面显示燃料价格和上限控制。"
},
"showFiatConversionInTestnets": {
"message": "在测试网络上显示转换"
},
@ -3364,23 +3349,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": "正在加载交易洞察……"
},
@ -3397,7 +3371,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": "某些网络可能会带来安全和/或隐私风险。在添加和使用网络之前,请先了解风险。"

View File

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

View 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

View File

@ -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.
@ -555,6 +556,10 @@ export function setupController(initState, initLangCode, overrides) {
if (message.name === WORKER_KEEP_ALIVE_MESSAGE) {
// To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI.
remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE });
controller.appStateController.setServiceWorkerLastActiveTime(
Date.now(),
);
}
});
}
@ -656,14 +661,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 +669,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 +705,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 +738,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 +752,7 @@ export function setupController(initState, initLangCode, overrides) {
.forEach((tx) =>
controller.encryptionPublicKeyManager.rejectMsg(
tx.id,
REJECT_NOTFICIATION_CLOSE,
REJECT_NOTIFICATION_CLOSE,
),
);
@ -880,11 +848,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;

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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,
});
});

View File

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

View 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();
});
}

View File

@ -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,
});
}

View File

@ -1,5 +0,0 @@
import { testsForProviderType } from './provider-api-tests/shared-tests';
describe('createInfuraClient', () => {
testsForProviderType('infura');
});

View File

@ -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();
});
}

View File

@ -1,5 +0,0 @@
import { testsForProviderType } from './provider-api-tests/shared-tests';
describe('createJsonRpcClient', () => {
testsForProviderType('custom');
});

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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]]);
});
});
}
}

File diff suppressed because it is too large Load Diff

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

View File

@ -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),
);
});
});
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,609 @@
import {
MessageManager,
PersonalMessageManager,
TypedMessageManager,
} from '@metamask/message-manager';
import {
AbstractMessage,
OriginalRequest,
} from '@metamask/message-manager/dist/AbstractMessageManager';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import { detectSIWE } from '../../../shared/modules/siwe';
import SignController, {
SignControllerMessenger,
SignControllerOptions,
} from './sign';
jest.mock('@metamask/message-manager', () => ({
MessageManager: jest.fn(),
PersonalMessageManager: jest.fn(),
TypedMessageManager: jest.fn(),
}));
jest.mock('../../../shared/modules/siwe', () => ({
detectSIWE: 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 siweMockFound = {
isSIWEMessage: true,
parsedMessage: { domain: 'test.com', test: 'value' },
};
const siweMockNotFound = { isSIWEMessage: false };
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 detectSIWEMock = detectSIWE as jest.MockedFunction<typeof detectSIWE>;
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 },
});
detectSIWEMock.mockReturnValue(siweMockNotFound);
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,
);
});
it('adds message to personal message manager including Ethereum sign in data', async () => {
detectSIWEMock.mockReturnValueOnce(siweMockFound);
await signController.newUnsignedPersonalMessage(
messageParamsMock,
requestMock,
);
expect(
personalMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledTimes(1);
expect(
personalMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledWith(
{
...messageParamsMock,
siwe: siweMockFound,
},
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('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',
);
});
});
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,
});
});
});
});

View File

@ -0,0 +1,658 @@
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 { detectSIWE } from '../../../shared/modules/siwe';
import PreferencesController from './preferences';
const controllerName = 'SignController';
const methodNameSign = 'eth_sign';
const methodNamePersonalSign = 'personal_sign';
const methodNameTypedSign = 'eth_signTypedData';
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> {
const ethereumSignInData = this._getEthereumSignInData(msgParams);
const finalMsgParams = { ...msgParams, siwe: ethereumSignInData };
return this._personalMessageManager.addUnapprovedMessageAsync(
finalMsgParams,
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.
*/
cancelMessage(msgId: string) {
this._cancelAbstractMessage(this._messageManager, msgId);
}
/**
* Used to cancel a personal_sign type message.
*
* @param msgId - The ID of the message to cancel.
*/
cancelPersonalMessage(msgId: string) {
this._cancelAbstractMessage(this._personalMessageManager, msgId);
}
/**
* Used to cancel a eth_signTypedData type message.
*
* @param msgId - The ID of the message to cancel.
*/
cancelTypedMessage(msgId: string) {
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 _getEthereumSignInData(messgeParams: PersonalMessageParams): any {
return detectSIWE(messgeParams);
}
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) {
this.messagingSystem.call('ApprovalController:acceptRequest', messageId);
}
private _rejectApproval(messageId: string) {
this.messagingSystem.call(
'ApprovalController:rejectRequest',
messageId,
'Cancel',
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
),
});
}

View File

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

View File

@ -1,12 +1,12 @@
import { errorCodes } from 'eth-rpc-errors';
import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app';
import { TransactionStatus } from '../../../shared/constants/transaction';
import { SECOND } from '../../../shared/constants/time';
import { detectSIWE } from '../../../shared/modules/siwe';
import {
EVENT,
EVENT_NAMES,
METAMETRIC_KEY_OPTIONS,
METAMETRIC_KEY,
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsEventUiCustomization,
} from '../../../shared/constants/metametrics';
/**
@ -41,55 +41,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 +142,8 @@ export default function createRPCMethodTrackingMiddleware({
// keys for the various events in the flow.
const eventType = EVENT_NAME_MAP[method];
const eventProperties = {};
// Boolean variable that reduces code duplication and increases legibility
const shouldTrackEvent =
// Don't track if the request came from our own UI or background
@ -160,29 +162,23 @@ export default function createRPCMethodTrackingMiddleware({
// 'Provider Method Called'.
const event = eventType
? eventType.REQUESTED
: EVENT_NAMES.PROVIDER_METHOD_CALLED;
: MetaMetricsEventName.ProviderMethodCalled;
const properties = {};
let msgParams;
if (event === EVENT_NAMES.SIGNATURE_REQUESTED) {
properties.signature_type = method;
if (event === MetaMetricsEventName.SignatureRequested) {
eventProperties.signature_type = method;
const data = req?.params?.[0];
const from = req?.params?.[1];
const paramsExamplePassword = req?.params?.[2];
msgParams = {
...paramsExamplePassword,
from,
data,
origin,
};
const msgData = {
msgParams,
status: 'unapproved',
msgParams: {
...paramsExamplePassword,
from,
data,
origin,
},
status: TransactionStatus.unapproved,
type: req.method,
};
@ -193,25 +189,21 @@ export default function createRPCMethodTrackingMiddleware({
);
if (securityProviderResponse?.flagAsDangerous === 1) {
properties.ui_customizations = ['flagged_as_malicious'];
eventProperties.ui_customizations = [
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 +212,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 +234,6 @@ export default function createRPCMethodTrackingMiddleware({
return callback();
}
const properties = {};
// The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings
const isDisabledEthSignAdvancedSetting =
method === MESSAGE_TYPE.ETH_SIGN &&
@ -254,79 +244,20 @@ export default function createRPCMethodTrackingMiddleware({
let event;
if (isDisabledRPCMethod) {
event = eventType.FAILED;
properties.error = res.error;
} else if (res.error?.code === 4001) {
eventProperties.error = res.error;
} else if (res.error?.code === errorCodes.provider.userRejectedRequest) {
event = eventType.REJECTED;
} else {
event = eventType.APPROVED;
}
let msgParams;
if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) {
properties.signature_type = method;
const data = req?.params?.[0];
const from = req?.params?.[1];
const paramsExamplePassword = req?.params?.[2];
msgParams = {
...paramsExamplePassword,
from,
data,
origin,
};
const msgData = {
msgParams,
status: 'unapproved',
type: req.method,
};
try {
const securityProviderResponse = await securityProviderRequest(
msgData,
req.method,
);
if (securityProviderResponse?.flagAsDangerous === 1) {
properties.ui_customizations = ['flagged_as_malicious'];
} else if (securityProviderResponse?.flagAsDangerous === 2) {
properties.ui_customizations = ['flagged_as_safety_unknown'];
} else {
properties.ui_customizations = null;
}
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
const { isSIWEMessage } = detectSIWE({ data });
if (isSIWEMessage) {
properties.ui_customizations === null
? (properties.ui_customizations = [
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
.SIWE,
])
: properties.ui_customizations.push(
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
.SIWE,
);
}
}
} catch (e) {
console.warn(
`createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`,
);
}
} else {
properties.method = method;
}
trackEvent({
event,
category: EVENT.CATEGORIES.INPAGE_PROVIDER,
category: MetaMetricsEventCategory.InpageProvider,
referrer: {
url: origin,
},
properties,
properties: eventProperties,
});
return callback();

View File

@ -1,7 +1,11 @@
import { errorCodes } from 'eth-rpc-errors';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { EVENT_NAMES } from '../../../shared/constants/metametrics';
import {
MetaMetricsEventName,
MetaMetricsEventUiCustomization,
} from '../../../shared/constants/metametrics';
import { SECOND } from '../../../shared/constants/time';
import { detectSIWE } from '../../../shared/modules/siwe';
import createRPCMethodTrackingMiddleware from './createRPCMethodTrackingMiddleware';
const trackEvent = jest.fn();
@ -52,6 +56,12 @@ function getNext(timeout = 500) {
const waitForSeconds = async (seconds) =>
await new Promise((resolve) => setTimeout(resolve, SECOND * seconds));
jest.mock('../../../shared/modules/siwe', () => ({
detectSIWE: jest.fn().mockImplementation(() => {
return { isSIWEMessage: false };
}),
}));
describe('createRPCMethodTrackingMiddleware', () => {
afterEach(() => {
jest.resetAllMocks();
@ -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,89 @@ describe('createRPCMethodTrackingMiddleware', () => {
});
});
});
});
describe('participateInMetaMetrics is set to true with a request flagged as safe', () => {
beforeEach(() => {
metricsState.participateInMetaMetrics = true;
});
describe('when request is flagged as safe by security provider', () => {
it(`should immediately track a ${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 ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => {
const req = {
method: MESSAGE_TYPE.ETH_SIGN,
origin: 'some.dapp',
};
it(`should immediately track a ${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();
const res = {
error: null,
};
const { next } = getNext();
await handler(req, res, next);
expect(trackEvent).toHaveBeenCalledTimes(1);
expect(trackEvent.mock.calls[0][0]).toMatchObject({
category: 'inpage_provider',
event: EVENT_NAMES.SIGNATURE_REQUESTED,
properties: {
signature_type: MESSAGE_TYPE.ETH_SIGN,
ui_customizations: ['flagged_as_safety_unknown'],
},
referrer: { url: 'some.dapp' },
await handler(req, res, next);
expect(trackEvent).toHaveBeenCalledTimes(1);
expect(trackEvent.mock.calls[0][0]).toMatchObject({
category: 'inpage_provider',
event: MetaMetricsEventName.SignatureRequested,
properties: {
signature_type: MESSAGE_TYPE.ETH_SIGN,
ui_customizations: ['flagged_as_safety_unknown'],
},
referrer: { url: 'some.dapp' },
});
});
});
});

View File

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

View File

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

View File

@ -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'));
}

View File

@ -1,128 +0,0 @@
import { TransactionStatus } from '../../../shared/constants/transaction';
import MessageManager from './message-manager';
describe('Message Manager', () => {
let messageManager;
beforeEach(() => {
messageManager = new MessageManager({
metricsEvent: jest.fn(),
});
});
describe('#getMsgList', () => {
it('when new should return empty array', () => {
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(0);
});
});
describe('#addMsg', () => {
it('adds a Msg returned in getMsgList', () => {
const Msg = {
id: 1,
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
};
messageManager.addMsg(Msg);
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].id).toStrictEqual(1);
});
});
describe('#setMsgStatusApproved', () => {
it('sets the Msg status to approved', () => {
const Msg = {
id: 1,
status: 'unapproved',
metamaskNetworkId: 'unit test',
};
messageManager.addMsg(Msg);
messageManager.setMsgStatusApproved(1);
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TransactionStatus.approved);
});
});
describe('#rejectMsg', () => {
it('sets the Msg status to rejected', () => {
const Msg = {
id: 1,
status: 'unapproved',
metamaskNetworkId: 'unit test',
};
messageManager.addMsg(Msg);
messageManager.rejectMsg(1);
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TransactionStatus.rejected);
});
});
describe('#_updateMsg', () => {
it('replaces the Msg with the same id', () => {
messageManager.addMsg({
id: '1',
status: 'unapproved',
metamaskNetworkId: 'unit test',
});
messageManager.addMsg({
id: '2',
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
});
messageManager._updateMsg({
id: '1',
status: 'blah',
hash: 'foo',
metamaskNetworkId: 'unit test',
});
const result = messageManager.getMsg('1');
expect(result.hash).toStrictEqual('foo');
});
});
describe('#getUnapprovedMsgs', () => {
it('returns unapproved Msgs in a hash', () => {
messageManager.addMsg({
id: '1',
status: 'unapproved',
metamaskNetworkId: 'unit test',
});
messageManager.addMsg({
id: '2',
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
});
const result = messageManager.getUnapprovedMsgs();
expect(typeof result).toStrictEqual('object');
expect(result['1'].status).toStrictEqual('unapproved');
expect(result['2']).toBeUndefined();
});
});
describe('#getMsg', () => {
it('returns a Msg with the requested id', () => {
messageManager.addMsg({
id: '1',
status: 'unapproved',
metamaskNetworkId: 'unit test',
});
messageManager.addMsg({
id: '2',
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
});
expect(messageManager.getMsg('1').status).toStrictEqual('unapproved');
expect(messageManager.getMsg('2').status).toStrictEqual(
TransactionStatus.approved,
);
});
});
});

View File

@ -1,5 +1,5 @@
import { EthereumRpcError } from 'eth-rpc-errors';
import SafeEventEmitter from 'safe-event-emitter';
import SafeEventEmitter from '@metamask/safe-event-emitter';
import createRandomId from '../../../shared/modules/random-id';
import { TEN_SECONDS_IN_MILLISECONDS } from '../../../shared/lib/transactions-controller-utils';

View File

@ -1,4 +1,4 @@
import EventEmitter from 'safe-event-emitter';
import EventEmitter from '@metamask/safe-event-emitter';
import ExtensionPlatform from '../platforms/extension';
const NOTIFICATION_HEIGHT = 620;

View File

@ -1,370 +0,0 @@
import EventEmitter from 'events';
import { ObservableStore } from '@metamask/obs-store';
import { bufferToHex } from 'ethereumjs-util';
import { ethErrors } from 'eth-rpc-errors';
import log from 'loglevel';
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';
import { detectSIWE } from '../../../shared/modules/siwe';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
import { addHexPrefix } from './util';
const hexRe = /^[0-9A-Fa-f]+$/gu;
/**
* Represents, and contains data about, an 'personal_sign' type signature request. These are created when a
* signature for an personal_sign call is requested.
*
* @see {@link https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html#sign}
* @typedef {object} PersonalMessage
* @property {number} id An id to track and identify the message object
* @property {object} msgParams The parameters to pass to the personal_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' will
* always have a 'personal_sign' type.
*/
export default class PersonalMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - PersonalMessage.
*
* @param options
* @param options.metricsEvent
* @param options.securityProviderRequest
*/
constructor({ metricsEvent, securityProviderRequest }) {
super();
this.memStore = new ObservableStore({
unapprovedPersonalMsgs: {},
unapprovedPersonalMsgCount: 0,
});
this.resetState = () => {
this.memStore.updateState({
unapprovedPersonalMsgs: {},
unapprovedPersonalMsgCount: 0,
});
};
this.messages = [];
this.metricsEvent = metricsEvent;
this.securityProviderRequest = securityProviderRequest;
}
/**
* A getter for the number of 'unapproved' PersonalMessages in this.messages
*
* @returns {number} The number of 'unapproved' PersonalMessages in this.messages
*/
get unapprovedPersonalMsgCount() {
return Object.keys(this.getUnapprovedMsgs()).length;
}
/**
* A getter for the 'unapproved' PersonalMessages in this.messages
*
* @returns {object} An index of PersonalMessage ids to PersonalMessages, for all 'unapproved' PersonalMessages in
* this.messages
*/
getUnapprovedMsgs() {
return this.messages
.filter((msg) => msg.status === 'unapproved')
.reduce((result, msg) => {
result[msg.id] = msg;
return result;
}, {});
}
/**
* Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages 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} When the message has been signed or rejected
*/
addUnapprovedMessageAsync(msgParams, req) {
return new Promise((resolve, reject) => {
if (!msgParams.from) {
reject(
new Error('MetaMask Message Signature: from field is required.'),
);
return;
}
this.addUnapprovedMessage(msgParams, req).then((msgId) => {
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
resolve(data.rawSig);
return;
case 'rejected':
reject(
ethErrors.provider.userRejectedRequest(
'MetaMask Message Signature: User denied message signature.',
),
);
return;
case 'errored':
reject(new Error(`MetaMask Message Signature: ${data.error}`));
return;
default:
reject(
new Error(
`MetaMask Message Signature: Unknown problem: ${JSON.stringify(
msgParams,
)}`,
),
);
}
});
});
});
}
/**
* Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages 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 {number} The id of the newly created PersonalMessage.
*/
async addUnapprovedMessage(msgParams, req) {
log.debug(
`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(
msgParams,
)}`,
);
// add origin from request
if (req) {
msgParams.origin = req.origin;
}
msgParams.data = this.normalizeMsgData(msgParams.data);
// check for SIWE message
const siwe = detectSIWE(msgParams);
msgParams.siwe = siwe;
// 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.PERSONAL_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 PersonalMessage to this.messages, and calls this._saveMsgList() to save the unapproved PersonalMessages from that
* list to this.memStore.
*
* @param {Message} msg - The PersonalMessage to add to this.messages
*/
addMsg(msg) {
this.messages.push(msg);
this._saveMsgList();
}
/**
* Returns a specified PersonalMessage.
*
* @param {number} msgId - The id of the PersonalMessage to get
* @returns {PersonalMessage|undefined} The PersonalMessage with the id that matches the passed msgId, or undefined
* if no PersonalMessage has that id.
*/
getMsg(msgId) {
return this.messages.find((msg) => msg.id === msgId);
}
/**
* Approves a PersonalMessage. 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 PersonalMessage status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the PersonalMessage to approve.
*/
setMsgStatusApproved(msgId) {
this._setMsgStatus(msgId, 'approved');
}
/**
* Sets a PersonalMessage status to 'signed' via a call to this._setMsgStatus and updates that PersonalMessage in
* this.messages by adding the raw signature data of the signature request to the PersonalMessage
*
* @param {number} msgId - The id of the PersonalMessage 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 PersonalMessage status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the PersonalMessage 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 PersonalMessage in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId - The id of the PersonalMessage to update.
* @param {string} status - The new status of the PersonalMessage.
* @throws A 'PersonalMessageManager - PersonalMessage not found for id: "${msgId}".' if there is no PersonalMessage
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The PersonalMessage is also fired.
* @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along
* with the PersonalMessage
*/
_setMsgStatus(msgId, status) {
const msg = this.getMsg(msgId);
if (!msg) {
throw new Error(
`PersonalMessageManager - 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 PersonalMessage in this.messages to the passed PersonalMessage if the ids are equal. Then saves the
* unapprovedPersonalMsgs index to storage via this._saveMsgList
*
* @private
* @param {PersonalMessage} msg - A PersonalMessage that will replace an existing PersonalMessage (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 PersonalMessages, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*/
_saveMsgList() {
const unapprovedPersonalMsgs = this.getUnapprovedMsgs();
const unapprovedPersonalMsgCount = Object.keys(
unapprovedPersonalMsgs,
).length;
this.memStore.updateState({
unapprovedPersonalMsgs,
unapprovedPersonalMsgCount,
});
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
*/
normalizeMsgData(data) {
try {
const stripped = stripHexPrefix(data);
if (stripped.match(hexRe)) {
return addHexPrefix(stripped);
}
} catch (e) {
log.debug(`Message was not hex encoded, interpreting as utf8.`);
}
return bufferToHex(Buffer.from(data, 'utf8'));
}
}

View File

@ -1,182 +0,0 @@
import { TransactionStatus } from '../../../shared/constants/transaction';
import PersonalMessageManager from './personal-message-manager';
describe('Personal Message Manager', () => {
let messageManager;
beforeEach(() => {
messageManager = new PersonalMessageManager({
metricsEvent: jest.fn(),
securityProviderRequest: jest.fn(),
});
});
describe('#getMsgList', () => {
it('when new should return empty array', () => {
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(0);
});
});
describe('#addMsg', () => {
it('adds a Msg returned in getMsgList', () => {
const Msg = {
id: 1,
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
};
messageManager.addMsg(Msg);
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].id).toStrictEqual(1);
});
});
describe('#setMsgStatusApproved', () => {
it('sets the Msg status to approved', () => {
const Msg = {
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: 'unit test',
};
messageManager.addMsg(Msg);
messageManager.setMsgStatusApproved(1);
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TransactionStatus.approved);
});
});
describe('#rejectMsg', () => {
it('sets the Msg status to rejected', () => {
const Msg = {
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: 'unit test',
};
messageManager.addMsg(Msg);
messageManager.rejectMsg(1);
const result = messageManager.messages;
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TransactionStatus.rejected);
});
});
describe('#_updateMsg', () => {
it('replaces the Msg with the same id', () => {
messageManager.addMsg({
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: 'unit test',
});
messageManager.addMsg({
id: '2',
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
});
messageManager._updateMsg({
id: '1',
status: 'blah',
hash: 'foo',
metamaskNetworkId: 'unit test',
});
const result = messageManager.getMsg('1');
expect(result.hash).toStrictEqual('foo');
});
});
describe('#getUnapprovedMsgs', () => {
it('returns unapproved Msgs in a hash', () => {
messageManager.addMsg({
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: 'unit test',
});
messageManager.addMsg({
id: '2',
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
});
const result = messageManager.getUnapprovedMsgs();
expect(typeof result).toStrictEqual('object');
expect(result['1'].status).toStrictEqual(TransactionStatus.unapproved);
expect(result['2']).toBeUndefined();
});
});
describe('#getMsg', () => {
it('returns a Msg with the requested id', () => {
messageManager.addMsg({
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: 'unit test',
});
messageManager.addMsg({
id: '2',
status: TransactionStatus.approved,
metamaskNetworkId: 'unit test',
});
expect(messageManager.getMsg('1').status).toStrictEqual(
TransactionStatus.unapproved,
);
expect(messageManager.getMsg('2').status).toStrictEqual(
TransactionStatus.approved,
);
});
});
describe('#normalizeMsgData', () => {
it('converts text to a utf8 hex string', () => {
const input = 'hello';
const output = messageManager.normalizeMsgData(input);
expect(output).toStrictEqual('0x68656c6c6f');
});
it('tolerates a hex prefix', () => {
const input = '0x12';
const output = messageManager.normalizeMsgData(input);
expect(output).toStrictEqual('0x12');
});
it('tolerates normal hex', () => {
const input = '12';
const output = messageManager.normalizeMsgData(input);
expect(output).toStrictEqual('0x12');
});
});
describe('#addUnapprovedMessage', () => {
const origin = 'http://localhost:8080';
const from = '0xFb2C15004343904e5f4082578c4e8e11105cF7e3';
const msgParams = {
from,
data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e',
};
it('should detect SIWE messages', async () => {
const request = { origin };
const nonSiweMsgParams = {
from,
data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0',
};
// siwe message
const msgId = await messageManager.addUnapprovedMessage(
msgParams,
request,
);
const result = messageManager.getMsg(msgId);
expect(result.msgParams.siwe.isSIWEMessage).toStrictEqual(true);
// non-siwe message
const msgId2 = await messageManager.addUnapprovedMessage(
nonSiweMsgParams,
request,
);
const result2 = messageManager.getMsg(msgId2);
expect(result2.msgParams.siwe.isSIWEMessage).toStrictEqual(false);
});
});
});

View File

@ -9,7 +9,7 @@ import {
isPrefixedFormattedHexString,
isSafeChainId,
} from '../../../../../shared/modules/network.utils';
import { EVENT } from '../../../../../shared/constants/metametrics';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
const addEthereumChain = {
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
@ -262,7 +262,7 @@ async function addEthereumChainHandler(
rpcUrl: firstValidRPCUrl,
ticker,
},
{ source: EVENT.SOURCE.NETWORK.DAPP, referrer: origin },
{ source: MetaMetricsNetworkEventSource.Dapp, referrer: origin },
);
// Once the network has been added, the requested is considered successful

View File

@ -1,5 +1,5 @@
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
import { EVENT } from '../../../../../shared/constants/metametrics';
import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics';
/**
* This RPC method is called by the inpage provider whenever it detects the
@ -49,7 +49,7 @@ function logWeb3ShimUsageHandler(
sendMetrics(
{
event: `Website Accessed window.web3 Shim`,
category: EVENT.CATEGORIES.INPAGE_PROVIDER,
category: MetaMetricsEventCategory.InpageProvider,
referrer: {
url: origin,
},

View File

@ -1,7 +1,7 @@
import { KeyringController } from '@metamask/eth-keyring-controller';
import log from 'loglevel';
import { HardwareKeyringTypes } from '../../../shared/constants/hardware-wallets';
import { KeyringType } from '../../../shared/constants/keyring';
const seedPhraseVerifier = {
/**
@ -23,7 +23,7 @@ const seedPhraseVerifier = {
const keyringController = new KeyringController({});
const keyringBuilder = keyringController.getKeyringBuilderForType(
HardwareKeyringTypes.hdKeyTree,
KeyringType.hdKeyTree,
);
const keyring = keyringBuilder();
const opts = {

View File

@ -6,13 +6,13 @@ import { cloneDeep } from 'lodash';
import { KeyringController } from '@metamask/eth-keyring-controller';
import firstTimeState from '../first-time-state';
import mockEncryptor from '../../../test/lib/mock-encryptor';
import { HardwareKeyringTypes } from '../../../shared/constants/hardware-wallets';
import { KeyringType } from '../../../shared/constants/keyring';
import seedPhraseVerifier from './seed-phrase-verifier';
describe('SeedPhraseVerifier', () => {
describe('verifyAccounts', () => {
const password = 'passw0rd1';
const { hdKeyTree } = HardwareKeyringTypes;
const { hdKeyTree } = KeyringType;
let keyringController;
let primaryKeyring;

View File

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

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