1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

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

This commit is contained in:
Matthias Kretschmann 2023-04-27 13:37:53 +02:00
commit ea91836181
Signed by: m
GPG Key ID: 606EEEF3C479A91F
394 changed files with 9333 additions and 5267 deletions

View File

@ -143,11 +143,6 @@ workflows:
requires:
- prep-deps
- prep-build
- test-mozilla-lint-beta:
<<: *rc_branch_only
requires:
- prep-deps
- trigger-beta-build
- test-mozilla-lint-desktop:
filters:
branches:
@ -176,7 +171,6 @@ workflows:
- validate-source-maps-desktop
- validate-source-maps-flask
- test-mozilla-lint
- test-mozilla-lint-beta
- test-mozilla-lint-desktop
- test-mozilla-lint-flask
- test-e2e-chrome
@ -1144,17 +1138,6 @@ jobs:
name: test:mozilla-lint
command: NODE_OPTIONS=--max_old_space_size=3072 yarn mozilla-lint
test-mozilla-lint-beta:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Lint beta for firefox
command: |
.circleci/scripts/mozilla-lint-beta.sh
test-mozilla-lint-desktop:
executor: node-browsers
steps:

View File

@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
current_commit_msg=$(git show -s --format='%s' HEAD)
if [[ $current_commit_msg =~ Version[[:space:]](v[[:digit:]]+.[[:digit:]]+.[[:digit:]]+[-]beta.[[:digit:]]) ]]
then
# filter the commit message like Version v10.24.1-beta.1
printf '%s\n' "Linting beta builds for firefox"
# Move beta build to dist
mv ./dist-beta ./dist
# Move beta zips to builds
mv ./builds-beta ./builds
# test:mozilla-lint
export NODE_OPTIONS='--max_old_space_size=3072'
yarn mozilla-lint
else
printf '%s\n' 'Commit message does not match commit message for beta pattern; skipping linting for firefox'
mkdir dist
mkdir builds
exit 0
fi
exit 0

View File

@ -12,8 +12,9 @@ if [[ $current_commit_msg =~ Version[[:space:]](v[[:digit:]]+.[[:digit:]]+.[[:di
then
# filter the commit message like Version v10.24.1-beta.1
printf '%s\n' "Create a build for $version with beta version $current_commit_msg"
yarn build --build-type beta dist
yarn build --build-type beta prod
export ENABLE_MV3=true
yarn build --build-type beta --platform='chrome' dist
yarn build --build-type beta --platform='chrome' prod
else
printf '%s\n' 'Commit message does not match commit message for beta pattern; skipping beta automation build'
mkdir dist

View File

@ -17,12 +17,13 @@ jobs:
with:
node-version: '16'
- name: Install dependencies
run: yarn
- name: Run fitness functions
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
# git fetch origin $HEAD_REF
# git fetch origin $BASE_REF
# git diff origin/$BASE_REF origin/$HEAD_REF -- . ':(exclude)development/fitness-functions/*' > diff
# npm run fitness-functions -- "ci" "./diff"
git fetch origin $BASE_REF
git diff origin/$BASE_REF HEAD -- . > diff
npm run fitness-functions -- "ci" "./diff"

View File

@ -1,11 +1,15 @@
; Extra environment variables
PASSWORD=METAMASK PASSWORD
; Defaults are set in builds.yml
; This variable is required
INFURA_PROJECT_ID=00000000000
SEGMENT_WRITE_KEY=
SWAPS_USE_DEV_APIS=
PORTFOLIO_URL=
TRANSACTION_SECURITY_PROVIDER=
MULTICHAIN=
;PASSWORD=METAMASK PASSWORD
,SEGMENT_WRITE_KEY=
;SWAPS_USE_DEV_APIS=
;PORTFOLIO_URL=
;TRANSACTION_SECURITY_PROVIDER=
;MULTICHAIN=
; Set this to test changes to the phishing warning page.
PHISHING_WARNING_PAGE_URL=
;PHISHING_WARNING_PAGE_URL=

View File

@ -695,10 +695,6 @@
"message": "$1 ist mit keiner Site verbunden.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1-Snap ist mit diesen Sites verbunden. Sie haben Zugriff auf die oben aufgeführten Berechtigungen.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Verbinden..."
},
@ -1355,18 +1351,6 @@
"message": "Dateiimport fehlgeschlagen? Bitte hier klicken!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Details ansehen",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Hinzugefügt am",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "von",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "Sie sollten diese Erweiterung deinstallieren",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Diese Gasgebühr wurde von $1 vorgeschlagen. Dies kann ein Problem mit Ihrer Transaktion verursachen. Bei Fragen wenden Sie sich bitte an $1.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Gasgebühr"
},
"gasLimit": {
"message": "Gaslimit"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Keine passenden Ergebnisse gefunden."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Zeigen"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Intelligente Transaktionen"
},
"snapAccess": {
"message": "$1-Snap hat Zugriff auf:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Am $1 von $2 hinzugefügt",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Diese Inhalte stammen von $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Verwalten Sie Ihre Snaps"
},
"snapsStatus": {
"message": "Snap-Status ist von der Aktivität abhängig."
},
"snapsToggle": {
"message": "Ein Snap wird nur ausgeführt, wenn er aktiviert ist"
},

View File

@ -695,10 +695,6 @@
"message": "$1 δεν είναι συνδεδεμένο με καμία τοποθεσία.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "Το snap $1 συνδέεται με αυτούς τους ιστότοπους. Έχουν πρόσβαση στις παραπάνω άδειες.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Σύνδεση..."
},
@ -1355,18 +1351,6 @@
"message": "Η εισαγωγή αρχείων δεν λειτουργεί; Κάντε κλικ εδώ!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Προβολή λεπτομερειών",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Προστέθηκε στις",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "από",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "θα πρέπει να καταργήσετε την εγκατάσταση αυτής της επέκτασης",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Αυτό το τέλος συναλλαγής έχει προταθεί από το $1. Η παράκαμψη μπορεί να προκαλέσει πρόβλημα με τη συναλλαγή σας. Εάν έχετε απορίες, επικοινωνήστε με $1.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Τέλη Συναλλαγής"
},
"gasLimit": {
"message": "Όριο τέλους συναλλαγής"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Δε βρέθηκαν αποτελέσματα που να ταιριάζουν."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Εμφάνιση"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Έξυπνη Συναλλαγή"
},
"snapAccess": {
"message": "Το snap $1 έχει πρόσβαση σε:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Προστέθηκε στις $1 από $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Αυτό το περιεχόμενο προέρχεται από το $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Διαχειριστείτε τα Snaps σας"
},
"snapsStatus": {
"message": "Η κατάσταση του Snap εξαρτάται από τη δραστηριότητα."
},
"snapsToggle": {
"message": "Ένα snap θα εκτελεστεί μόνο εάν είναι ενεργοποιημένο"
},

View File

@ -333,6 +333,12 @@
"alerts": {
"message": "Alerts"
},
"allCustodianAccountsConnectedSubtitle": {
"message": "You have either already connected all your custodian accounts or dont have any account to connect to MetaMask Institutional."
},
"allCustodianAccountsConnectedTitle": {
"message": "No accounts available to connect"
},
"allOfYour": {
"message": "All of your $1",
"description": "$1 is the symbol or name of the token that the user is approving spending"
@ -466,6 +472,9 @@
"average": {
"message": "Average"
},
"awaitingApproval": {
"message": "Awaiting approval..."
},
"back": {
"message": "Back"
},
@ -721,6 +730,12 @@
"connectAccountOrCreate": {
"message": "Connect account or create new"
},
"connectCustodialAccountMsg": {
"message": "Please choose the custodian you want to connect in order to add or refresh a token."
},
"connectCustodialAccountTitle": {
"message": "Custodial Accounts"
},
"connectHardwareWallet": {
"message": "Connect hardware wallet"
},
@ -771,10 +786,6 @@
"message": "$1 is not connected to any sites.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1 snap is connected to these sites. They have access to the permissions listed above.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Connecting..."
},
@ -936,6 +947,27 @@
"custodianAccount": {
"message": "Custodian account"
},
"custodianReplaceRefreshTokenChangedFailed": {
"message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again."
},
"custodianReplaceRefreshTokenChangedSubtitle": {
"message": "You can now use your custodian accounts in MetaMask Institutional."
},
"custodianReplaceRefreshTokenChangedTitle": {
"message": "Your custodian token has been refreshed"
},
"custodianReplaceRefreshTokenSubtitle": {
"message": "This is will replace the custodian token for the following address:"
},
"custodianReplaceRefreshTokenTitle": {
"message": "Replace custodian token"
},
"custodyApiUrl": {
"message": "$1 API URL"
},
"custodyDeeplinkDescription": {
"message": "Approve the transaction in the $1 app. Once all required custody approvals have been performed the transaction will complete. Check your $1 app for status."
},
"custodyRefreshTokenModalDescription": {
"message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again."
},
@ -1064,6 +1096,10 @@
"description": {
"message": "Description"
},
"descriptionFromSnap": {
"message": "Description from $1",
"description": "$1 represents the name of the snap"
},
"desktopConnectionCriticalErrorDescription": {
"message": "This error could be intermittent, so try restarting the extension or disable MetaMask Desktop."
},
@ -1328,6 +1364,9 @@
"enableSmartTransactions": {
"message": "Enable smart transactions"
},
"enableSnap": {
"message": "Enable"
},
"enableToken": {
"message": "enable $1",
"description": "$1 is a token symbol, e.g. ETH"
@ -1364,6 +1403,9 @@
"enterANumber": {
"message": "Enter a number"
},
"enterCustodianToken": {
"message": "Enter your $1 token or add a new token"
},
"enterMaxSpendLimit": {
"message": "Enter max spend limit"
},
@ -1478,18 +1520,6 @@
"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"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Added on",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "from",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "you should uninstall this extension",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1555,9 +1585,6 @@
"message": "This gas fee has been suggested by $1. Overriding this may cause a problem with your transaction. Please reach out to $1 if you have questions.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Gas fee"
},
"gasLimit": {
"message": "Gas limit"
},
@ -1855,6 +1882,13 @@
"install": {
"message": "Install"
},
"installOrigin": {
"message": "Install origin"
},
"installedOn": {
"message": "Installed on $1",
"description": "$1 is the date when the snap has been installed"
},
"institutionalFeatures": {
"message": "Institutional Features"
},
@ -1969,6 +2003,9 @@
"lastSold": {
"message": "Last sold"
},
"layer1Fees": {
"message": "Layer 1 fees"
},
"learnCancelSpeeedup": {
"message": "Learn how to $1",
"description": "$1 is link to cancel or speed up transactions"
@ -1980,6 +2017,9 @@
"message": "Want to $1 about gas?",
"description": "$1 will be replaced by the learnMore translation key"
},
"learnMoreKeystone": {
"message": "Learn More"
},
"learnMoreUpperCase": {
"message": "Learn more"
},
@ -2152,6 +2192,9 @@
"metrics": {
"message": "Metrics"
},
"mismatchAccount": {
"message": "Your selected account ($1) is different than the account trying to sign ($2)"
},
"mismatchedChainLinkText": {
"message": "verify the network details",
"description": "Serves as link text for the 'mismatchedChain' key. This text will be embedded inside the translation for that key."
@ -2190,6 +2233,9 @@
"mmiAuthenticate": {
"message": "The page at $1 would like to authorise the following projects compliance settings in MetaMask Institutional"
},
"more": {
"message": "more"
},
"moreComingSoon": {
"message": "More coming soon..."
},
@ -2881,6 +2927,9 @@
"passwordsDontMatch": {
"message": "Passwords don't match"
},
"pasteJWTToken": {
"message": "Paste or drop your token here:"
},
"pastePrivateKey": {
"message": "Enter your private key string here:",
"description": "For importing an account from a private key"
@ -3491,18 +3540,27 @@
"seedPhraseWriteDownHeader": {
"message": "Write down your Secret Recovery Phrase"
},
"select": {
"message": "Select"
},
"selectAccounts": {
"message": "Select the account(s) to use on this site"
},
"selectAll": {
"message": "Select all"
},
"selectAllAccounts": {
"message": "Select all accounts"
},
"selectAnAccount": {
"message": "Select an account"
},
"selectAnAccountAlreadyConnected": {
"message": "This account has already been connected to MetaMask"
},
"selectAnAccountHelp": {
"message": "Select the custodian accounts to use in MetaMask Institutional."
},
"selectHdPath": {
"message": "Select HD path"
},
@ -3574,9 +3632,9 @@
"settingsSearchMatchingNotFound": {
"message": "No matching results found."
},
"shorthandVersion": {
"shortVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
"description": "$1 is the version number to show"
},
"show": {
"message": "Show"
@ -3654,14 +3712,6 @@
"smartTransaction": {
"message": "Smart transaction"
},
"snapAccess": {
"message": "$1 snap has access to:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Added on $1 from $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "This content is coming from $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3733,9 +3783,6 @@
"snapsSettingsDescription": {
"message": "Manage your Snaps"
},
"snapsStatus": {
"message": "Snap status is dependent on activity."
},
"snapsToggle": {
"message": "A snap will only run if it is enabled"
},
@ -4523,7 +4570,6 @@
"transactionFailed": {
"message": "Transaction Failed"
},
"transactionFee": {
"message": "Transaction fee"
},
@ -4750,6 +4796,9 @@
"message": "Verify this token on $1 and make sure this is the token you want to trade.",
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
},
"version": {
"message": "Version"
},
"view": {
"message": "View"
},

View File

@ -695,10 +695,6 @@
"message": "$1 no está conectado a ningún sitio.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "El complemento de $1 está conectado a estos sitios. Tienen acceso a los permisos enumerados anteriormente.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Estableciendo conexión…"
},
@ -1355,18 +1351,6 @@
"message": "¿No funciona la importación del archivo? Haga clic aquí.",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Ver detalles",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Añadido el",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "de",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "le recomendamos que desinstale esta extensión",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Esta tarifa de gas ha sido sugerida por $1. Anularla puede causar un problema con su transacción. Comuníquese con $1 si tiene preguntas.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Cuota de gas"
},
"gasLimit": {
"message": "Límite de gas"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "No se encontraron resultados coincidentes."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Mostrar"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Transacción inteligente"
},
"snapAccess": {
"message": "El complemento de $1 tiene acceso a:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Se agregó en $1 de $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Este contenido proviene de $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Administre sus complementos"
},
"snapsStatus": {
"message": "El estado del complemento depende de la actividad."
},
"snapsToggle": {
"message": "Un complemento solo se ejecutará si está habilitado"
},

View File

@ -882,18 +882,6 @@
"message": "¿No funciona la importación del archivo? ¡Haga clic aquí!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Ver detalles",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Añadido el",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "de",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "le recomendamos que desinstale esta extensión",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."

View File

@ -695,10 +695,6 @@
"message": "$1 nest connecté à aucun site.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "Le snap $1 est connecté à ces sites. Ils ont accès aux autorisations énumérées ci-dessus.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Connexion…"
},
@ -1355,18 +1351,6 @@
"message": "Limportation de fichier ne fonctionne pas ? Cliquez ici !",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Voir les détails",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Ajouté le",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "de",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "vous devriez désinstaller cette extension",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Ce prix de carburant a été suggéré par $1. Si vous nen tenez pas compte, vous risquez de rencontrer des difficultés lors de votre transaction. Veuillez contacter $1 pour toute question.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Frais de transaction"
},
"gasLimit": {
"message": "Montant maximal des frais de transaction"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Aucun résultat correspondant trouvé."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Afficher"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Transaction intelligente"
},
"snapAccess": {
"message": "Le snap $1 peut accéder à :",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Ajouté le $1 à partir de $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Ce contenu provient de $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Gérez vos Snaps"
},
"snapsStatus": {
"message": "Létat du Snap dépend de lactivité."
},
"snapsToggle": {
"message": "Un snap ne sexécute que sil est activé"
},

View File

@ -695,10 +695,6 @@
"message": "$1 किसी भी साइट से कनेक्ट नहीं है।",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1 स्नैप इन साइटों से जुड़ा है। वे ऊपर सूचीबद्ध अनुमतियों को एक्सेस कर सकती हैं।",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "कनेक्ट किया जा रहा है..."
},
@ -1355,18 +1351,6 @@
"message": "फाइल आयात काम नहीं कर रहा है? यहां क्लिक करें!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "विवरण देखें",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "जोड़ा गया",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "से",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "आपको इस एक्सटेन्शन को अनइंस्टाल करना चाहिए",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "यह गैस शुल्क $1 द्वारा सुझाया गया है। इसे ओवरराइड करने से आपके लेन-देन में समस्या हो सकती है। यदि आपके पास कोई सवाल हैं तो कृपया $1 तक पहुंचें।",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "गैस शुल्क"
},
"gasLimit": {
"message": "गैस की सीमा"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "कोई मेल खाने वाला परिणाम नहीं मिला।"
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "दिखाएं"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "स्मार्ट लेनदेन"
},
"snapAccess": {
"message": "$1 स्नैप को एक्सेस है:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "$2 से $1 जोड़ा गया",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "यह सामग्री $1 से आ रही है",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "अपने स्नैप्स प्रबंधित करें"
},
"snapsStatus": {
"message": "स्नैप स्टेटस एक्टिविटी पर निर्भर करता है।"
},
"snapsToggle": {
"message": "कोई स्नैप तभी चलेगा जब उसे सक्षम किया गया हो"
},

View File

@ -695,10 +695,6 @@
"message": "$1 tidak terhubung ke situs mana pun.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "Snap $1 terhubung ke situs-situs ini. Token ini memiliki akses ke izin yang tercantum di atas. \t",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Menghubungkan..."
},
@ -1355,18 +1351,6 @@
"message": "Impor file tidak bekerja? Klik di sini!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Lihat detailnya",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Ditambahkan di",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "dari",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "Anda harus menghapus ekstensi ini",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Biaya gas ini telah disarankan oleh $1. Pengabaian dapat menyebabkan masalah pada transaksi Anda. Hubungi $1 jika ada pertanyaan.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Biaya gas"
},
"gasLimit": {
"message": "Batas gas"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Tidak menemukan hasil yang cocok."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Tampil"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Transaksi pintar"
},
"snapAccess": {
"message": "Snap $1 memiliki akses ke:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Ditambahkan pada $1 dari $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Konten ini berasal dari $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Kelola Snap Anda"
},
"snapsStatus": {
"message": "Status snap tergantung pada aktivitas."
},
"snapsToggle": {
"message": "Snap hanya akan beroperasi jika diaktifkan"
},

View File

@ -538,10 +538,6 @@
"message": "$1 non è connesso ad alcun sito.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1 snap è collegato a questi siti. Hanno accesso alle autorizzazioni sopra elencate.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Connessione..."
},

View File

@ -695,10 +695,6 @@
"message": "$1はどのサイトとも接続されていません。",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1 スナップはこれらのサイトに接続されており、上記のパーミッションにアクセスできます。",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "接続中..."
},
@ -1355,18 +1351,6 @@
"message": "ファイルのインポートが機能していない場合ここをクリックしてください!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "詳細を表示",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "追加日",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "元",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "この拡張機能はアンインストールしてください",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "このガス代は$1により提案されています。これを上書きすると、トランザクションに問題が発生する可能性があります。ご質問がございましたら、$1までお問い合わせください。",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "ガス代"
},
"gasLimit": {
"message": "ガスリミット"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "一致する結果が見つかりませんでした。"
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "表示"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "スマートトランザクション"
},
"snapAccess": {
"message": "$1 スナップは次にアクセス可能です:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "$1 に $2 から追加",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "このコンテンツは $1 からのものです",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "スナップの管理"
},
"snapsStatus": {
"message": "スナップのステータスはアクティビティによります。"
},
"snapsToggle": {
"message": "スナップは有効になっている場合にのみ実行されます"
},

View File

@ -695,10 +695,6 @@
"message": "$1 계정은 어떤 사이트에도 연결되어 있지 않습니다.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1 스냅이 이 사이트에 연결되어 있습니다. 이 사이트는 위에 나열된 접근 권한이 있습니다.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "연결 중..."
},
@ -918,10 +914,10 @@
"message": "소수점 이하 자릿수는 0 이상, 36 이하여야 합니다."
},
"decrypt": {
"message": "암호 해독"
"message": "복호화"
},
"decryptCopy": {
"message": "암호 해독된 메시지 복사"
"message": "복호화된 메시지 복사"
},
"decryptInlineError": {
"message": "다음 오류 때문에 이 메시지를 해독할 수 없습니다: $1",
@ -1355,18 +1351,6 @@
"message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요.",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "세부 정보 보기",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "추가하기",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "발신",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "이 확장 프로그램을 삭제해야 합니다",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "$1에서 이 가스 요금을 제안했습니다. 이를 무시하면 거래에 문제가 발생할 수 있습니다. 질문이 있는 경우 $1에 문의하세요.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "가스 수수료"
},
"gasLimit": {
"message": "가스 한도"
},
@ -2176,7 +2157,7 @@
"message": "다음"
},
"nextNonceWarning": {
"message": "임시값이 권장 임시값인 $1보다 큽니다.",
"message": "논스값이 권장 논스값인 $1보다 큽니다.",
"description": "The next nonce according to MetaMask's internal logic"
},
"nftAddFailedMessage": {
@ -2243,13 +2224,13 @@
"message": "웹캠을 찾을 수 없음"
},
"nonce": {
"message": "임시값"
"message": "논스"
},
"nonceField": {
"message": "거래 임시값 맞춤화"
"message": "거래 논스 맞춤화"
},
"nonceFieldDescription": {
"message": "이 기능을 켜면 확인 화면에서 임시값(거래 번호)을 변경할 수 있습니다. 이는 고급 기능으로, 주의해서 사용해야 합니다."
"message": "이 기능을 켜면 확인 화면에서 논스(거래 번호)를 변경할 수 있습니다. 이는 고급 기능으로, 주의해서 사용해야 합니다."
},
"nonceFieldHeading": {
"message": "커스텀 논스"
@ -2349,7 +2330,7 @@
"description": "The 'call to action' on the button, or link, of the 'Swap on Binance Smart Chain!' notification. Upon clicking, users will be taken to a page where then can swap tokens on Binance Smart Chain."
},
"notifications4Description": {
"message": "토큰 스왑 최고가를 지갑에서 바로 이용하세요. MetaMask는 이제 바이낸스 스마트 체인의 여러 분산형 교환 애그리게이터 및 투자전문기관과 연결됩니다.",
"message": "토큰 스왑 최고가를 지갑에서 바로 이용하세요. MetaMask는 이제 바이낸스 스마트 체인의 여러 탈중앙화 거래소 애그리게이터 및 투자전문기관과 연결됩니다.",
"description": "Description of a notification in the 'See What's New' popup."
},
"notifications4Title": {
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "검색 결과가 없습니다."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "보기"
},
@ -3287,7 +3264,7 @@
"message": "본 메시지에 서명하는 행위는 위험의 가능성을 내포하고 있습니다. 본 서명을 통해 메시지 발신 당사자에게 귀하의 계정 및 모든 자산에 대해 완전한 권한을 부여할 수 있기 때문입니다. 이를 통해 계정의 모든 잔액을 인출할 수 있기도 하다는 뜻입니다. 주의하여 진행하세요. $1"
},
"signed": {
"message": "서명완료"
"message": "서명 완료"
},
"signin": {
"message": "로그인"
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "스마트 트랜잭션"
},
"snapAccess": {
"message": "$1 스냅이 접근할 수 있는 대상:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "$1에 $2에서 추가됨",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "콘텐츠 출처: $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "스냅 관리"
},
"snapsStatus": {
"message": "스냅 상태는 활동에 따라 달라집니다."
},
"snapsToggle": {
"message": "스냅은 활성화된 상태에서만 작동합니다."
},
@ -3609,7 +3575,7 @@
"message": "보장 금액"
},
"swapAmountReceivedInfo": {
"message": "수신하는 최소 금액입니다. 슬리지에 따라 추가 금액을 받을 수도 있습니다."
"message": "수신하는 최소 금액입니다. 슬리지에 따라 추가 금액을 받을 수도 있습니다."
},
"swapApproval": {
"message": "스왑을 위해 $1 승인",
@ -3640,7 +3606,7 @@
"message": "맞춤형"
},
"swapDecentralizedExchange": {
"message": "분산형 교환"
"message": "탈중앙화 거래소"
},
"swapDirectContract": {
"message": "직접 계약"
@ -3703,17 +3669,17 @@
"description": "$1 is the selected network, e.g. Ethereum or BSC"
},
"swapHighSlippageWarning": {
"message": "슬리지 금액이 아주 큽니다."
"message": "슬리지 금액이 아주 큽니다."
},
"swapIncludesMMFee": {
"message": "$1%의 MetaMask 요금이 포함됩니다.",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
},
"swapLowSlippageError": {
"message": "거래가 실패할 수도 있습니다. 최대 슬리지가 너무 낮습니다."
"message": "거래가 실패할 수도 있습니다. 최대 슬리지가 너무 낮습니다."
},
"swapMaxSlippage": {
"message": "최대 슬리지"
"message": "최대 슬리지"
},
"swapMetaMaskFee": {
"message": "MetaMask 수수료"
@ -3749,7 +3715,7 @@
"message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수가 만족스러운지 확인하시기 바랍니다."
},
"swapPriceUnavailableTitle": {
"message": "진행하기 전에 율 확인"
"message": "진행하기 전에 율 확인"
},
"swapProcessing": {
"message": "처리 중"
@ -3761,25 +3727,25 @@
"message": "견적 소스"
},
"swapQuotesExpiredErrorDescription": {
"message": "새 견적을 요청해 최신 율을 확인하세요."
"message": "새 견적을 요청해 최신 율을 확인하세요."
},
"swapQuotesExpiredErrorTitle": {
"message": "견적 시간 초과"
},
"swapQuotesNotAvailableErrorDescription": {
"message": "금액 또는 슬리지 설정을 조정한 후 다시 시도해 보세요."
"message": "금액 또는 슬리지 설정을 조정한 후 다시 시도해 보세요."
},
"swapQuotesNotAvailableErrorTitle": {
"message": "사용 가능한 견적 없음"
},
"swapRate": {
"message": "율"
"message": "율"
},
"swapReceiving": {
"message": "수신 중"
},
"swapReceivingInfoTooltip": {
"message": "이것은 예상치입니다. 정확한 금액은 슬리지에 따라 달라집니다."
"message": "이것은 예상치입니다. 정확한 금액은 슬리지에 따라 달라집니다."
},
"swapRequestForQuotation": {
"message": "견적 요청"
@ -3803,20 +3769,20 @@
"message": "다음은 여러 유동성 소스에서 수집한 전체 견적입니다."
},
"swapSlippageNegative": {
"message": "슬리지는 0보다 크거나 같아야 합니다."
"message": "슬리지는 0보다 크거나 같아야 합니다."
},
"swapSlippagePercent": {
"message": "$1%",
"description": "$1 is the amount of % for slippage"
},
"swapSlippageTooltip": {
"message": "주문 시점과 확인 시점 사이에 가격이 변동되는 현상을 \"슬리패지\"라고 합니다. 슬리패지가 \"최대 슬리패지\" 설정을 초과하면 스왑이 자동으로 취소됩니다."
"message": "주문 시점과 확인 시점 사이에 가격이 변동되는 현상을 \"슬리피지\"라고 합니다. 슬리피지가 \"최대 슬리피지\" 설정을 초과하면 스왑이 자동으로 취소됩니다."
},
"swapSource": {
"message": "유동성 소스"
},
"swapSourceInfo": {
"message": "저희는 여러 유동성 소스(교환, 애그리게이터, 투자전문기관)를 검색하여 최상의 율과 최저 네트워크 수수료를 찾아드립니다."
"message": "저희는 여러 유동성 소스(교환, 애그리게이터, 투자전문기관)를 검색하여 최상의 율과 최저 네트워크 수수료를 찾아드립니다."
},
"swapSuggested": {
"message": "제안 스왑"
@ -3884,13 +3850,13 @@
"description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol"
},
"swapZeroSlippage": {
"message": "0% 슬리지"
"message": "0% 슬리지"
},
"swapsAdvancedOptions": {
"message": "고급 옵션"
},
"swapsExcessiveSlippageWarning": {
"message": "슬리패지 금액이 너무 커서 전환율이 좋지 않습니다. 슬리패지 허용치를 15% 값 이하로 줄이세요."
"message": "슬리피지 금액이 너무 커서 전환율이 좋지 않습니다. 슬리피지 허용치를 15% 값 이하로 줄이세요."
},
"swapsMaxSlippage": {
"message": "슬리피지 허용치"
@ -4297,7 +4263,7 @@
"message": "연락처 정보 인증"
},
"verifyThisTokenDecimalOn": {
"message": "토큰 십진수는 $1에서 찾을 수 있습니다.",
"message": "토큰 소수점은 $1에서 찾을 수 있습니다.",
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
},
"verifyThisTokenOn": {

View File

@ -695,10 +695,6 @@
"message": "$1 não está conectada a nenhum site.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "O snap $1 está conectado a estes sites. Eles têm acesso às permissões listadas acima.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Conectando..."
},
@ -1355,18 +1351,6 @@
"message": "A importação de ficheiro não está a funcionar? Carregue aqui!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Ver detalhes",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Adicionado em",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "de",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "você deve desinstalar essa extensão",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Essa taxa de gás foi sugerida por $1. Sua substituição pode causar um problema com a sua transação. Entre em contato com $1 se tiver perguntas.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Taxa de gás"
},
"gasLimit": {
"message": "Limite de gás"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Nenhum resultado correspondente encontrado."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Exibir"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Transação inteligente"
},
"snapAccess": {
"message": "Snap $1 tem acesso a:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Adicionado em $1 a partir de $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Esse conteúdo vem de $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Gerencie seus snaps"
},
"snapsStatus": {
"message": "O status do snap depende da atividade."
},
"snapsToggle": {
"message": "O snap só será executado se estiver ativado"
},

View File

@ -882,18 +882,6 @@
"message": "A importação de arquivo não está funcionando? Clique aqui!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Ver detalhes",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Adicionado em",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "de",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "você deve desinstalar essa extensão",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."

View File

@ -695,10 +695,6 @@
"message": "$1 не подключен ни к каким сайтам.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "Снап $1 подключен к этим сайтам. У них есть доступ к перечисленным выше разрешениям.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Подключение..."
},
@ -1355,18 +1351,6 @@
"message": "Импорт файлов не работает? Нажмите здесь!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "См. подробности",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Добавлена",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "от",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "вам нужно должны удалить это расширение",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Эта плата за газ была предложена $1. Ее переопредление может вызвать проблемы с вашей транзакцией. При наличии вопросов обратитесь к $1.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Плата за газ"
},
"gasLimit": {
"message": "Лимит газа"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Совпадений не найдено."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Показать"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Смарт-транзакция"
},
"snapAccess": {
"message": "У снапа $1 есть доступ к:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Добавлено на $1 из $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Этот контент поступает от $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Управление вашим снапами"
},
"snapsStatus": {
"message": "Статус снапа зависит от активности."
},
"snapsToggle": {
"message": "Снап будет работать только в том случае, если он включен"
},

View File

@ -695,10 +695,6 @@
"message": "Ang $1 ay hindi nakakonekta sa anumang site.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "Ang $1 snap ay konektado sa mga site na ito. May access sila sa mga pahintulot na nakalista sa itaas.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Kumokonekta..."
},
@ -1355,18 +1351,6 @@
"message": "Hindi gumagana ang pag-import ng file? Mag-click dito!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Tingnan ang mga detalye",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Dinagdag sa",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "mula sa",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "dapat mong i-uninstall ang extension na ito",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Ang gas fee na ito ay iminungkahi ng $1. Ang pag-override dito ay maaaring magdulot ng problema sa iyong transaksyon. Mangyaring makipag-ugnayan sa $1 kung mayroon kang mga tanong.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Bayarin sa Gas"
},
"gasLimit": {
"message": "Limitasyon sa Gas"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Walang nakitang katugmang resulta."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Ipakita"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Smart Transaction"
},
"snapAccess": {
"message": "Ang $1 snap ay may access sa:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Idinagdag noong $1 mula sa $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Ang nilalamang ito ay nagmumula sa $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Pamahalaan ang iyong mga Snap"
},
"snapsStatus": {
"message": "Ang lagay ng Snap ay nakadepende sa aktibidad."
},
"snapsToggle": {
"message": "Tatakbo lamang ang snap kapag pinagana ito"
},

View File

@ -695,10 +695,6 @@
"message": "$1 herhangi bir siteye bağlanmamış.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1 snap bu sitelere bağlı. Yukarıda listelenen izinlere erişimleri vardır.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Bağlanıyor..."
},
@ -1355,18 +1351,6 @@
"message": "Dosya içe aktarma çalışmıyor mu? Buraya tıklayın!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Ayrıntıları gör",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Şu tarihte eklendi:",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "şurada:",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "bu uzantıyı kaldırmalısın",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Bu gaz ücreti $1 tarafından önerilmiştir. Bu değerin başka bir değerle değiştirilmesi işleminizle ilgili bir soruna neden olabilir. Sorularınız olursa lütfen $1 ile iletişime geçin.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Gaz ücreti"
},
"gasLimit": {
"message": "Gaz limiti"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Eşleşen sonuç bulunamadı."
},
"shorthandVersion": {
"message": "s$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Göster"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Akıllı işlem"
},
"snapAccess": {
"message": "$1 snap'in şunlara erişimi vardır:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "$1 tarihinde $2 alanından eklendi",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Bu içerik $1 kaynaklıdır",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Snap'lerini yönet"
},
"snapsStatus": {
"message": "Snap durumu etkinliğe bağlıdır."
},
"snapsToggle": {
"message": "Bir snap yalnızca etkinleştirilmişse çalışır"
},

View File

@ -695,10 +695,6 @@
"message": "$1 chưa được kết nối với bất kỳ trang web nào.",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "Snap $1 được kết nối với các trang web này. Các trang web này được phép sử dụng những quyền được liệt kê ở trên.",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "Đang kết nối..."
},
@ -1355,18 +1351,6 @@
"message": "Tính năng nhập tập tin không hoạt động? Nhấn vào đây!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "Xem chi tiết",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Đã thêm vào",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "từ",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "bạn nên gỡ cài đặt tiện ích mở rộng này",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "Phí gas này đã được gợi ý bởi $1. Việc sửa đổi có thể khiến giao dịch của bạn gặp sự cố. Vui lòng liên hệ với $1 nếu bạn có câu hỏi.",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "Phí gas"
},
"gasLimit": {
"message": "Giới hạn gas"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "Không tìm thấy kết quả trùng khớp."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "Hiển thị"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "Giao dịch thông minh"
},
"snapAccess": {
"message": "Snap $1 có quyền truy cập vào:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Đã thêm vào $1 từ $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "Nội dung này đến từ $1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "Quản lý Snap"
},
"snapsStatus": {
"message": "Trạng thái Snap tùy thuộc vào hoạt động."
},
"snapsToggle": {
"message": "Snap chỉ hoạt động khi đã bật"
},

View File

@ -695,10 +695,6 @@
"message": "$1 还没连接到任何网站。",
"description": "$1 is the account name"
},
"connectedSnapSites": {
"message": "$1的snap已连接到这些站点。它们有上述的访问权限。",
"description": "$1 represents the name of the snap"
},
"connecting": {
"message": "连接中……"
},
@ -1355,18 +1351,6 @@
"message": "文件导入失败?点击这里!",
"description": "Helps user import their account from a JSON file"
},
"flaskSnapSettingsCardButtonCta": {
"message": "查看详细信息",
"description": "Call to action a user can take to see more information about the snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "添加于",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "自",
"description": "Part of the sentence describing when and where snap was added"
},
"flaskWelcomeUninstall": {
"message": "您应该卸载此扩展程序",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@ -1432,9 +1416,6 @@
"message": "这笔燃料费是由 $1 建议的。忽略它可能会导致您的交易出现问题。如果您有疑问,请联系 $1。",
"description": "$1 represents the Dapp's origin"
},
"gasFee": {
"message": "燃料费"
},
"gasLimit": {
"message": "燃料上限"
},
@ -3230,10 +3211,6 @@
"settingsSearchMatchingNotFound": {
"message": "没有找到匹配的结果."
},
"shorthandVersion": {
"message": "v$1",
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
},
"show": {
"message": "显示"
},
@ -3307,14 +3284,6 @@
"smartTransaction": {
"message": "智能交易"
},
"snapAccess": {
"message": "$1的snap可以访问",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "从 $2 添加到 $1",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapContent": {
"message": "此内容来自$1",
"description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap."
@ -3351,9 +3320,6 @@
"snapsSettingsDescription": {
"message": "管理您的Snap"
},
"snapsStatus": {
"message": "Snap状态取决于活动。"
},
"snapsToggle": {
"message": "Snap仅在启用后才会运行"
},

View File

@ -18,7 +18,7 @@ import {
ENVIRONMENT_TYPE_FULLSCREEN,
EXTENSION_MESSAGES,
PLATFORM_FIREFOX,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
MESSAGE_TYPE,
///: END:ONLY_INCLUDE_IN
} from '../../shared/constants/app';
@ -55,7 +55,7 @@ import { deferredPromise, getPlatform } from './lib/util';
/* eslint-enable import/first */
/* eslint-disable import/order */
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
import {
CONNECTION_TYPE_EXTERNAL,
CONNECTION_TYPE_INTERNAL,
@ -105,7 +105,7 @@ const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
const OVERRIDE_ORIGIN = {
EXTENSION: 'EXTENSION',
DESKTOP: 'DESKTOP_APP',
@ -263,7 +263,7 @@ async function initialize() {
const initState = await loadStateFromPersistence();
const initLangCode = await getFirstPreferredLangCode();
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
await DesktopManager.init(platform.getVersion());
///: END:ONLY_INCLUDE_IN
@ -525,7 +525,7 @@ export function setupController(
* @param {Port} remotePort - The port provided by a new context.
*/
connectRemote = async (remotePort) => {
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
if (
DesktopManager.isDesktopEnabled() &&
OVERRIDE_ORIGIN.DESKTOP !== overrides?.getOrigin?.()
@ -651,7 +651,7 @@ export function setupController(
// communication with page or other extension
connectExternal = (remotePort) => {
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
if (
DesktopManager.isDesktopEnabled() &&
OVERRIDE_ORIGIN.DESKTOP !== overrides?.getOrigin?.()
@ -682,7 +682,7 @@ export function setupController(
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
updateBadge,
);
controller.decryptMessageManager.on(
controller.decryptMessageController.hub.on(
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
updateBadge,
);
@ -725,14 +725,11 @@ export function setupController(
}
function getUnapprovedTransactionCount() {
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager;
const pendingApprovalCount =
controller.approvalController.getTotalApprovalCount();
const waitingForUnlockCount =
controller.appStateController.waitingForUnlock.length;
return (
unapprovedDecryptMsgCount + pendingApprovalCount + waitingForUnlockCount
);
return pendingApprovalCount + waitingForUnlockCount;
}
notificationManager.on(
@ -753,14 +750,9 @@ export function setupController(
controller.txController.txStateManager.setTxStatusRejected(txId),
);
controller.signController.rejectUnapproved(REJECT_NOTIFICATION_CLOSE_SIG);
controller.decryptMessageManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.decryptMessageManager.rejectMsg(
tx.id,
REJECT_NOTIFICATION_CLOSE,
),
);
controller.decryptMessageController.rejectUnapproved(
REJECT_NOTIFICATION_CLOSE,
);
controller.encryptionPublicKeyController.rejectUnapproved(
REJECT_NOTIFICATION_CLOSE,
);
@ -769,7 +761,7 @@ export function setupController(
Object.values(controller.approvalController.state.pendingApprovals).forEach(
({ id, type }) => {
switch (type) {
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
case MESSAGE_TYPE.SNAP_DIALOG_ALERT:
case MESSAGE_TYPE.SNAP_DIALOG_PROMPT:
controller.approvalController.accept(id, null);
@ -791,7 +783,7 @@ export function setupController(
updateBadge();
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
if (OVERRIDE_ORIGIN.DESKTOP !== overrides?.getOrigin?.()) {
controller.store.subscribe((state) => {
DesktopManager.setState(state);

View File

@ -0,0 +1,222 @@
import { DecryptMessageManager } from '@metamask/message-manager';
import { AbstractMessage } from '@metamask/message-manager/dist/AbstractMessageManager';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import DecryptMessageController, {
DecryptMessageControllerMessenger,
DecryptMessageControllerOptions,
getDefaultState,
} from './decrypt-message';
const messageIdMock = '12345';
const messageMock = {
metamaskId: messageIdMock,
time: 123,
status: 'unapproved',
type: 'testType',
rawSig: undefined,
} as any as AbstractMessage;
const mockExtState = {};
jest.mock('@metamask/message-manager', () => ({
DecryptMessageManager: jest.fn(),
}));
const createKeyringControllerMock = () => ({
decryptMessage: jest.fn(),
});
const createMessengerMock = () =>
({
registerActionHandler: jest.fn(),
publish: jest.fn(),
call: jest.fn(),
} as any as jest.Mocked<DecryptMessageControllerMessenger>);
const createDecryptMessageManagerMock = <T>() =>
({
getUnapprovedMessages: jest.fn(),
getUnapprovedMessagesCount: jest.fn(),
getMessage: jest.fn(),
addUnapprovedMessageAsync: jest.fn(),
approveMessage: jest.fn(),
setMessageStatusAndResult: jest.fn(),
rejectMessage: jest.fn(),
update: jest.fn(),
subscribe: jest.fn(),
updateMessage: jest.fn(),
updateMessageErrorInline: jest.fn(),
setResult: jest.fn(),
hub: {
on: jest.fn(),
},
} as any as jest.Mocked<T>);
describe('EncryptionPublicKeyController', () => {
let decryptMessageController: DecryptMessageController;
const decryptMessageManagerConstructorMock =
DecryptMessageManager as jest.MockedClass<typeof DecryptMessageManager>;
const getStateMock = jest.fn();
const keyringControllerMock = createKeyringControllerMock();
const messengerMock = createMessengerMock();
const metricsEventMock = jest.fn();
const decryptMessageManagerMock =
createDecryptMessageManagerMock<DecryptMessageManager>();
beforeEach(() => {
jest.resetAllMocks();
decryptMessageManagerConstructorMock.mockReturnValue(
decryptMessageManagerMock,
);
decryptMessageController = new DecryptMessageController({
getState: getStateMock as any,
keyringController: keyringControllerMock as any,
messenger: messengerMock as any,
metricsEvent: metricsEventMock as any,
} as DecryptMessageControllerOptions);
});
it('should return unapprovedMsgCount', () => {
decryptMessageManagerMock.getUnapprovedMessagesCount.mockReturnValue(5);
expect(decryptMessageController.unapprovedDecryptMsgCount).toBe(5);
});
it('should reset state', () => {
decryptMessageController.update(() => ({
unapprovedDecryptMsgs: {
[messageIdMock]: messageMock,
} as any,
unapprovedDecryptMsgCount: 1,
}));
decryptMessageController.resetState();
expect(decryptMessageController.state).toStrictEqual(getDefaultState());
});
it('should clear unapproved messages', () => {
decryptMessageController.clearUnapproved();
expect(decryptMessageController.state).toStrictEqual(getDefaultState());
expect(decryptMessageManagerMock.update).toBeCalledTimes(1);
});
it('should add unapproved messages', async () => {
await decryptMessageController.newRequestDecryptMessage(messageMock);
expect(decryptMessageManagerMock.addUnapprovedMessageAsync).toBeCalledTimes(
1,
);
expect(decryptMessageManagerMock.addUnapprovedMessageAsync).toBeCalledWith(
messageMock,
undefined,
);
});
it('should decrypt message', async () => {
const messageToDecrypt = {
...messageMock,
data: '0x7b22666f6f223a22626172227d',
};
decryptMessageManagerMock.approveMessage.mockResolvedValue(
messageToDecrypt,
);
keyringControllerMock.decryptMessage.mockResolvedValue('decryptedMessage');
getStateMock.mockReturnValue(mockExtState);
const result = await decryptMessageController.decryptMessage(
messageToDecrypt,
);
expect(decryptMessageManagerMock.approveMessage).toBeCalledTimes(1);
expect(decryptMessageManagerMock.approveMessage).toBeCalledWith(
messageToDecrypt,
);
expect(keyringControllerMock.decryptMessage).toBeCalledTimes(1);
expect(keyringControllerMock.decryptMessage).toBeCalledWith(
messageToDecrypt,
);
expect(decryptMessageManagerMock.setMessageStatusAndResult).toBeCalledTimes(
1,
);
expect(decryptMessageManagerMock.setMessageStatusAndResult).toBeCalledWith(
messageIdMock,
'decryptedMessage',
'decrypted',
);
expect(result).toBe(mockExtState);
});
it('should cancel decrypt request', async () => {
const messageToDecrypt = {
...messageMock,
data: '0x7b22666f6f223a22626172227d',
};
decryptMessageManagerMock.approveMessage.mockResolvedValue(
messageToDecrypt,
);
keyringControllerMock.decryptMessage.mockRejectedValue(new Error('error'));
getStateMock.mockReturnValue(mockExtState);
return expect(
decryptMessageController.decryptMessage(messageToDecrypt),
).rejects.toThrow('error');
});
it('should decrypt message inline', async () => {
const messageToDecrypt = {
...messageMock,
data: '0x7b22666f6f223a22626172227d',
};
decryptMessageManagerMock.getMessage.mockReturnValue(messageToDecrypt);
keyringControllerMock.decryptMessage.mockResolvedValue('decryptedMessage');
getStateMock.mockReturnValue(mockExtState);
const result = await decryptMessageController.decryptMessageInline(
messageToDecrypt,
);
expect(decryptMessageManagerMock.setResult).toBeCalledTimes(1);
expect(decryptMessageManagerMock.setResult).toBeCalledWith(
messageMock.metamaskId,
'decryptedMessage',
);
expect(result).toBe(mockExtState);
});
it('should be able to cancel decrypt message', async () => {
decryptMessageManagerMock.rejectMessage.mockResolvedValue(messageMock);
getStateMock.mockReturnValue(mockExtState);
const result = await decryptMessageController.cancelDecryptMessage(
messageIdMock,
);
expect(decryptMessageManagerMock.rejectMessage).toBeCalledTimes(1);
expect(decryptMessageManagerMock.rejectMessage).toBeCalledWith(
messageIdMock,
);
expect(result).toBe(mockExtState);
});
it('should be able to reject all unapproved messages', async () => {
decryptMessageManagerMock.getUnapprovedMessages.mockReturnValue({
[messageIdMock]: messageMock,
});
await decryptMessageController.rejectUnapproved('reason to cancel');
expect(decryptMessageManagerMock.rejectMessage).toBeCalledTimes(1);
expect(decryptMessageManagerMock.rejectMessage).toBeCalledWith(
messageIdMock,
);
expect(metricsEventMock).toBeCalledTimes(1);
expect(metricsEventMock).toBeCalledWith({
event: 'reason to cancel',
category: MetaMetricsEventCategory.Messages,
properties: {
action: 'Decrypt Message Request',
},
});
});
});

View File

@ -0,0 +1,394 @@
import EventEmitter from 'events';
import log from 'loglevel';
import {
DecryptMessageManager,
DecryptMessageParams,
DecryptMessageParamsMetamask,
} from '@metamask/message-manager';
import { KeyringController } from '@metamask/eth-keyring-controller';
import {
AbstractMessage,
AbstractMessageManager,
AbstractMessageParams,
AbstractMessageParamsMetamask,
MessageManagerState,
OriginalRequest,
} from '@metamask/message-manager/dist/AbstractMessageManager';
import {
BaseControllerV2,
RestrictedControllerMessenger,
} from '@metamask/base-controller';
import {
AcceptRequest,
AddApprovalRequest,
RejectRequest,
} from '@metamask/approval-controller';
import { ApprovalType, ORIGIN_METAMASK } from '@metamask/controller-utils';
import { Patch } from 'immer';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
const controllerName = 'DecryptMessageController';
const stateMetadata = {
unapprovedDecryptMsgs: { persist: false, anonymous: false },
unapprovedDecryptMsgCount: { persist: false, anonymous: false },
};
export const getDefaultState = () => ({
unapprovedDecryptMsgs: {},
unapprovedDecryptMsgCount: 0,
});
export type CoreMessage = AbstractMessage & {
messageParams: AbstractMessageParams;
};
export type StateMessage = Required<
Omit<AbstractMessage, 'securityProviderResponse'>
>;
export type DecryptMessageControllerState = {
unapprovedDecryptMsgs: Record<string, StateMessage>;
unapprovedDecryptMsgCount: number;
};
export type GetDecryptMessageState = {
type: `${typeof controllerName}:getState`;
handler: () => DecryptMessageControllerState;
};
export type DecryptMessageStateChange = {
type: `${typeof controllerName}:stateChange`;
payload: [DecryptMessageControllerState, Patch[]];
};
export type DecryptMessageControllerActions = GetDecryptMessageState;
export type DecryptMessageControllerEvents = DecryptMessageStateChange;
type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest;
export type DecryptMessageControllerMessenger = RestrictedControllerMessenger<
typeof controllerName,
DecryptMessageControllerActions | AllowedActions,
DecryptMessageControllerEvents,
AllowedActions['type'],
never
>;
export type DecryptMessageControllerOptions = {
getState: () => any;
keyringController: KeyringController;
messenger: DecryptMessageControllerMessenger;
metricsEvent: (payload: any, options?: any) => void;
};
/**
* Controller for decrypt signing requests requiring user approval.
*/
export default class DecryptMessageController extends BaseControllerV2<
typeof controllerName,
DecryptMessageControllerState,
DecryptMessageControllerMessenger
> {
hub: EventEmitter;
private _getState: () => any;
private _keyringController: KeyringController;
private _metricsEvent: (payload: any, options?: any) => void;
private _decryptMessageManager: DecryptMessageManager;
/**
* Construct a DecryptMessage controller.
*
* @param options - The controller options.
* @param options.getState - Callback to retrieve all user state.
* @param options.keyringController - An instance of a keyring controller used to decrypt message
* @param options.messenger - A reference to the messaging system.
* @param options.metricsEvent - A function for emitting a metric event.
*/
constructor({
getState,
keyringController,
metricsEvent,
messenger,
}: DecryptMessageControllerOptions) {
super({
metadata: stateMetadata,
messenger,
name: controllerName,
state: getDefaultState(),
});
this._getState = getState;
this._keyringController = keyringController;
this._metricsEvent = metricsEvent;
this.hub = new EventEmitter();
this._decryptMessageManager = new DecryptMessageManager(
undefined,
undefined,
undefined,
['decrypted'],
);
this._decryptMessageManager.hub.on('updateBadge', () => {
this.hub.emit('updateBadge');
});
this._decryptMessageManager.hub.on(
'unapprovedMessage',
(messageParams: AbstractMessageParamsMetamask) => {
this._requestApproval(messageParams);
},
);
this._subscribeToMessageState(
this._decryptMessageManager,
(state, newMessages, messageCount) => {
state.unapprovedDecryptMsgs = newMessages;
state.unapprovedDecryptMsgCount = messageCount;
},
);
}
/**
* A getter for the number of 'unapproved' Messages in the DecryptMessageManager.
*
* @returns The number of 'unapproved' Messages in the DecryptMessageManager.
*/
get unapprovedDecryptMsgCount(): number {
return this._decryptMessageManager.getUnapprovedMessagesCount();
}
/**
* Reset the controller state to the initial state.
*/
resetState() {
this.update(() => getDefaultState());
}
/**
* Clears all unapproved messages from memory.
*/
clearUnapproved() {
this._decryptMessageManager.update({
unapprovedMessages: {},
unapprovedMessagesCount: 0,
});
}
/**
* Called when a dapp uses the eth_decrypt method
*
* @param messageParams - The params passed to eth_decrypt.
* @param req - The original request, containing the origin.
* @returns Promise resolving to the raw data of the signature request.
*/
async newRequestDecryptMessage(
messageParams: DecryptMessageParams,
req: OriginalRequest,
): Promise<string> {
return this._decryptMessageManager.addUnapprovedMessageAsync(
messageParams,
req,
);
}
/**
* Signifies a user's approval to decrypt a message in queue.
* Triggers decrypt, and the callback function from newUnsignedDecryptMessage.
*
* @param messageParams - The params of the message to decrypt & return to the Dapp.
* @returns A full state update.
*/
async decryptMessage(messageParams: DecryptMessageParamsMetamask) {
const messageId = messageParams.metamaskId as string;
try {
const cleanMessageParams =
await this._decryptMessageManager.approveMessage(messageParams);
cleanMessageParams.data = this._parseMessageData(cleanMessageParams.data);
const rawMessage = await this._keyringController.decryptMessage(
cleanMessageParams,
);
this._decryptMessageManager.setMessageStatusAndResult(
messageId,
rawMessage,
'decrypted',
);
this._acceptApproval(messageId);
} catch (error) {
log.info('MetaMaskController - eth_decrypt failed.', error);
this._cancelAbstractMessage(this._decryptMessageManager, messageId);
throw error;
}
return this._getState();
}
/**
* Only decrypt message and don't touch transaction state
*
* @param messageParams - The params of the message to decrypt.
* @returns A full state update.
*/
async decryptMessageInline(messageParams: DecryptMessageParamsMetamask) {
const messageId = messageParams.metamaskId as string;
messageParams.data = this._parseMessageData(messageParams.data);
const rawMessage = await this._keyringController.decryptMessage(
messageParams,
);
this._decryptMessageManager.setResult(messageId, rawMessage);
return this._getState();
}
/**
* Used to cancel a eth_decrypt type message.
*
* @param messageId - The ID of the message to cancel.
* @returns A full state update.
*/
cancelDecryptMessage(messageId: string) {
this._decryptMessageManager.rejectMessage(messageId);
this._rejectApproval(messageId);
return this._getState();
}
/**
* Reject all unapproved messages of any type.
*
* @param reason - A message to indicate why.
*/
rejectUnapproved(reason?: string) {
Object.keys(this._decryptMessageManager.getUnapprovedMessages()).forEach(
(messageId) => {
this._cancelAbstractMessage(
this._decryptMessageManager,
messageId,
reason,
);
},
);
}
private _acceptApproval(messageId: string) {
this.messagingSystem.call('ApprovalController:acceptRequest', messageId);
}
private _cancelAbstractMessage(
messageManager: AbstractMessageManager<
AbstractMessage,
AbstractMessageParams,
AbstractMessageParamsMetamask
>,
messageId: string,
reason?: string,
) {
if (reason) {
this._metricsEvent({
event: reason,
category: MetaMetricsEventCategory.Messages,
properties: {
action: 'Decrypt Message Request',
},
});
}
messageManager.rejectMessage(messageId);
this._rejectApproval(messageId);
return this._getState();
}
private _subscribeToMessageState(
messageManager: AbstractMessageManager<
AbstractMessage,
AbstractMessageParams,
AbstractMessageParamsMetamask
>,
updateState: (
state: DecryptMessageControllerState,
newMessages: Record<string, StateMessage>,
messageCount: number,
) => void,
) {
messageManager.subscribe((state: MessageManagerState<AbstractMessage>) => {
const newMessages = this._migrateMessages(
state.unapprovedMessages as any,
);
this.update((draftState) => {
updateState(draftState, newMessages, state.unapprovedMessagesCount);
});
});
}
private _migrateMessages(
coreMessages: Record<string, CoreMessage>,
): Record<string, StateMessage> {
const stateMessages: Record<string, StateMessage> = {};
for (const messageId of Object.keys(coreMessages)) {
const coreMessage = coreMessages[messageId];
const stateMessage = this._migrateMessage(coreMessage);
stateMessages[messageId] = stateMessage;
}
return stateMessages;
}
private _migrateMessage(coreMessage: CoreMessage): StateMessage {
const { messageParams, ...coreMessageData } = coreMessage;
const stateMessage = {
...coreMessageData,
rawSig: coreMessage.rawSig as string,
msgParams: messageParams,
origin: messageParams.origin,
};
return stateMessage;
}
private _requestApproval(messageParams: AbstractMessageParamsMetamask) {
const id = messageParams.metamaskId as string;
const origin = messageParams.origin || ORIGIN_METAMASK;
try {
this.messagingSystem.call(
'ApprovalController:addRequest',
{
id,
origin,
type: ApprovalType.EthDecrypt,
},
true,
);
} catch (error) {
log.info('Error adding request to approval controller', error);
}
}
private _parseMessageData(data: string) {
const stripped = stripHexPrefix(data);
const buff = Buffer.from(stripped, 'hex');
return JSON.parse(buff.toString('utf8'));
}
private _rejectApproval(messageId: string) {
try {
this.messagingSystem.call(
'ApprovalController:rejectRequest',
messageId,
'Cancel',
);
} catch (error) {
log.info('Error rejecting request to approval controller', error);
}
}
}

View File

@ -352,6 +352,15 @@ describe('EncryptionPublicKeyController', () => {
'Cancel',
);
});
it('returns current state', async () => {
getStateMock.mockReturnValueOnce(stateMock);
expect(
await encryptionPublicKeyController.cancelEncryptionPublicKey(
messageIdMock,
),
).toEqual(stateMock);
});
});
describe('message manager events', () => {

View File

@ -275,7 +275,7 @@ export default class EncryptionPublicKeyController extends BaseControllerV2<
* @param msgId - The id of the message to cancel.
*/
cancelEncryptionPublicKey(msgId: string) {
this._cancelAbstractMessage(this._encryptionPublicKeyManager, msgId);
return this._cancelAbstractMessage(this._encryptionPublicKeyManager, msgId);
}
/**
@ -342,36 +342,31 @@ export default class EncryptionPublicKeyController extends BaseControllerV2<
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);
});
},
);
messageManager.subscribe((state: MessageManagerState<AbstractMessage>) => {
const newMessages = this._migrateMessages(
state.unapprovedMessages as any,
);
this.update((draftState) => {
updateState(draftState, newMessages, state.unapprovedMessagesCount);
});
});
}
private async _migrateMessages(
private _migrateMessages(
coreMessages: Record<string, CoreMessage>,
): Promise<Record<string, StateMessage>> {
): 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);
const stateMessage = this._migrateMessage(coreMessage);
stateMessages[messageId] = stateMessage;
}
return stateMessages;
}
private async _migrateMessage(
coreMessage: CoreMessage,
): Promise<StateMessage> {
private _migrateMessage(coreMessage: CoreMessage): StateMessage {
const { messageParams, ...coreMessageData } = coreMessage;
// Core message managers use messageParams but frontend uses msgParams with lots of references

View File

@ -724,7 +724,7 @@ export default class MetaMetricsController {
[MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default',
[MetaMetricsUserTrait.TokenDetectionEnabled]:
metamaskState.useTokenDetection,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
[MetaMetricsUserTrait.DesktopEnabled]:
metamaskState.desktopEnabled || false,
///: END:ONLY_INCLUDE_IN

View File

@ -327,7 +327,7 @@ function buildDefaultProviderConfigState(): ProviderConfiguration {
};
} else if (
process.env.METAMASK_DEBUG ||
process.env.METAMASK_ENV === 'test'
process.env.METAMASK_ENVIRONMENT === 'test'
) {
return {
type: NETWORK_TYPES.GOERLI,

View File

@ -4,6 +4,6 @@ export * from './enums';
export * from './permission-log';
export * from './specifications';
export * from './selectors';
///: BEGIN:ONLY_INCLUDE_IN(flask)
export * from './flask/snap-permissions';
///: BEGIN:ONLY_INCLUDE_IN(snaps)
export * from './snaps/snap-permissions';
///: END:ONLY_INCLUDE_IN

View File

@ -2,7 +2,7 @@ import {
constructPermission,
PermissionType,
} from '@metamask/permission-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { endowmentCaveatSpecifications as snapsEndowmentCaveatSpecifications } from '@metamask/snaps-controllers';
import { caveatSpecifications as snapsCaveatsSpecifications } from '@metamask/rpc-methods';
///: END:ONLY_INCLUDE_IN
@ -71,7 +71,7 @@ export const getCaveatSpecifications = ({ getIdentities }) => {
validateCaveatAccounts(caveat.value, getIdentities),
},
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
...snapsCaveatsSpecifications,
...snapsEndowmentCaveatSpecifications,
///: END:ONLY_INCLUDE_IN

View File

@ -1,351 +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 { 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';
import { addHexPrefix } from './util';
const hexRe = /^[0-9A-Fa-f]+$/gu;
/**
* Represents, and contains data about, an 'eth_decrypt' type decryption request. These are created when a
* decryption for an eth_decrypt call is requested.
*
* @typedef {object} DecryptMessage
* @property {number} id An id to track and identify the message object
* @property {object} msgParams The parameters to pass to the decryptMessage method once the decryption 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 decryption request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the decryption request is 'unapproved', 'approved', 'decrypted' or 'rejected'
* @property {string} type The json-prc decryption method for which a decryption request has been made. A 'Message' will
* always have a 'eth_decrypt' type.
*/
export default class DecryptMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - DecryptMessage.
*
* @param {object} opts - Controller options
* @param {Function} opts.metricEvent - A function for emitting a metric event.
*/
constructor(opts) {
super();
this.memStore = new ObservableStore({
unapprovedDecryptMsgs: {},
unapprovedDecryptMsgCount: 0,
});
this.resetState = () => {
this.memStore.updateState({
unapprovedDecryptMsgs: {},
unapprovedDecryptMsgCount: 0,
});
};
this.messages = [];
this.metricsEvent = opts.metricsEvent;
}
/**
* A getter for the number of 'unapproved' DecryptMessages in this.messages
*
* @returns {number} The number of 'unapproved' DecryptMessages in this.messages
*/
get unapprovedDecryptMsgCount() {
return Object.keys(this.getUnapprovedMsgs()).length;
}
/**
* A getter for the 'unapproved' DecryptMessages in this.messages
*
* @returns {object} An index of DecryptMessage ids to DecryptMessages, for all 'unapproved' DecryptMessages in
* this.messages
*/
getUnapprovedMsgs() {
return this.messages
.filter((msg) => msg.status === 'unapproved')
.reduce((result, msg) => {
result[msg.id] = msg;
return result;
}, {});
}
/**
* Creates a new DecryptMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new DecryptMessage to this.messages, and to save the unapproved DecryptMessages from that list to
* this.memStore.
*
* @param {object} msgParams - The params for the eth_decrypt call to be made after the message is approved.
* @param {object} [req] - The original request object possibly containing the origin
* @returns {Promise<Buffer>} The raw decrypted message contents
*/
addUnapprovedMessageAsync(msgParams, req) {
return new Promise((resolve, reject) => {
if (!msgParams.from) {
reject(new Error('MetaMask Decryption: from field is required.'));
return;
}
const msgId = this.addUnapprovedMessage(msgParams, req);
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'decrypted':
resolve(data.rawData);
return;
case 'rejected':
reject(
ethErrors.provider.userRejectedRequest(
'MetaMask Decryption: User denied message decryption.',
),
);
return;
case 'errored':
reject(new Error('This message cannot be decrypted'));
return;
default:
reject(
new Error(
`MetaMask Decryption: Unknown problem: ${JSON.stringify(
msgParams,
)}`,
),
);
}
});
});
}
/**
* Creates a new DecryptMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new DecryptMessage to this.messages, and to save the unapproved DecryptMessages from that list to
* this.memStore.
*
* @param {object} msgParams - The params for the eth_decryptMsg 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 DecryptMessage.
*/
addUnapprovedMessage(msgParams, req) {
log.debug(
`DecryptMessageManager addUnapprovedMessage: ${JSON.stringify(
msgParams,
)}`,
);
// add origin from request
if (req) {
msgParams.origin = req.origin;
}
msgParams.data = this.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_DECRYPT,
};
this.addMsg(msgData);
// signal update
this.emit('update');
return msgId;
}
/**
* Adds a passed DecryptMessage to this.messages, and calls this._saveMsgList() to save the unapproved DecryptMessages from that
* list to this.memStore.
*
* @param {Message} msg - The DecryptMessage to add to this.messages
*/
addMsg(msg) {
this.messages.push(msg);
this._saveMsgList();
}
/**
* Returns a specified DecryptMessage.
*
* @param {number} msgId - The id of the DecryptMessage to get
* @returns {DecryptMessage|undefined} The DecryptMessage with the id that matches the passed msgId, or undefined
* if no DecryptMessage has that id.
*/
getMsg(msgId) {
return this.messages.find((msg) => msg.id === msgId);
}
/**
* Approves a DecryptMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise
* with the message params modified for proper decryption.
*
* @param {object} msgParams - The msgParams to be used when eth_decryptMsg 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.prepMsgForDecryption(msgParams);
}
/**
* Sets a DecryptMessage status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the DecryptMessage to approve.
*/
setMsgStatusApproved(msgId) {
this._setMsgStatus(msgId, 'approved');
}
/**
* Sets a DecryptMessage status to 'decrypted' via a call to this._setMsgStatus and updates that DecryptMessage in
* this.messages by adding the raw decryption data of the decryption request to the DecryptMessage
*
* @param {number} msgId - The id of the DecryptMessage to decrypt.
* @param {buffer} rawData - The raw data of the message request
*/
setMsgStatusDecrypted(msgId, rawData) {
const msg = this.getMsg(msgId);
msg.rawData = rawData;
this._updateMsg(msg);
this._setMsgStatus(msgId, 'decrypted');
}
/**
* 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 prepMsgForDecryption(msgParams) {
delete msgParams.metamaskId;
return msgParams;
}
/**
* Sets a DecryptMessage status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the DecryptMessage to reject.
* @param reason
*/
rejectMsg(msgId, reason = undefined) {
if (reason) {
this.metricsEvent({
event: reason,
category: MetaMetricsEventCategory.Messages,
properties: {
action: 'Decrypt Message Request',
},
});
}
this._setMsgStatus(msgId, 'rejected');
}
/**
* Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the TypedMessage 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 DecryptMessage in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId - The id of the DecryptMessage to update.
* @param {string} status - The new status of the DecryptMessage.
* @throws A 'DecryptMessageManager - DecryptMessage not found for id: "${msgId}".' if there is no DecryptMessage
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The DecryptMessage is also fired.
* @fires If status is 'rejected' or 'decrypted', an event with a name equal to `${msgId}:finished` is fired along
* with the DecryptMessage
*/
_setMsgStatus(msgId, status) {
const msg = this.getMsg(msgId);
if (!msg) {
throw new Error(
`DecryptMessageManager - Message not found for id: "${msgId}".`,
);
}
msg.status = status;
this._updateMsg(msg);
this.emit(`${msgId}:${status}`, msg);
if (
status === 'rejected' ||
status === 'decrypted' ||
status === 'errored'
) {
this.emit(`${msgId}:finished`, msg);
}
}
/**
* Sets a DecryptMessage in this.messages to the passed DecryptMessage if the ids are equal. Then saves the
* unapprovedDecryptMsgs index to storage via this._saveMsgList
*
* @private
* @param {DecryptMessage} msg - A DecryptMessage that will replace an existing DecryptMessage (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 DecryptMessages, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*/
_saveMsgList() {
const unapprovedDecryptMsgs = this.getUnapprovedMsgs();
const unapprovedDecryptMsgCount = Object.keys(unapprovedDecryptMsgs).length;
this.memStore.updateState({
unapprovedDecryptMsgs,
unapprovedDecryptMsgCount,
});
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,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { handlers as permittedSnapMethods } from '@metamask/rpc-methods/dist/permitted';
///: END:ONLY_INCLUDE_IN
import { permissionRpcMethods } from '@metamask/permission-controller';
@ -72,7 +72,7 @@ export function createMethodMiddleware(hooks) {
};
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
const snapHandlerMap = permittedSnapMethods.reduce((map, handler) => {
for (const methodName of handler.methodNames) {
map.set(methodName, handler);

View File

@ -1,7 +1,6 @@
import * as Sentry from '@sentry/browser';
import { Dedupe, ExtraErrorData } from '@sentry/integrations';
import { BuildType } from '../../../shared/constants/app';
import { FilterEvents } from './sentry-filter-events';
import extractEthjsErrorMessage from './extractEthjsErrorMessage';
@ -90,7 +89,7 @@ export default function setupSentry({ release, getState }) {
}
const environment =
METAMASK_BUILD_TYPE === BuildType.main
METAMASK_BUILD_TYPE === 'main'
? METAMASK_ENVIRONMENT
: `${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}`;

View File

@ -48,12 +48,12 @@ import {
SubjectMetadataController,
SubjectType,
} from '@metamask/subject-metadata-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { RateLimitController } from '@metamask/rate-limit-controller';
import { NotificationController } from '@metamask/notification-controller';
///: END:ONLY_INCLUDE_IN
import SmartTransactionsController from '@metamask/smart-transactions-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import {
CronjobController,
JsonSnapsRegistry,
@ -83,19 +83,18 @@ import { KeyringType } from '../../shared/constants/keyring';
import {
CaveatTypes,
RestrictedMethods,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
EndowmentPermissions,
ExcludedSnapPermissions,
ExcludedSnapEndowments,
///: END:ONLY_INCLUDE_IN
} from '../../shared/constants/permissions';
import { UI_NOTIFICATIONS } from '../../shared/notifications';
import { stripHexPrefix } from '../../shared/modules/hexstring-utils';
import { MILLISECOND, SECOND } from '../../shared/constants/time';
import {
ORIGIN_METAMASK,
MESSAGE_TYPE,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
SNAP_DIALOG_TYPES,
///: END:ONLY_INCLUDE_IN
POLLING_TOKEN_ENVIRONMENT_TYPES,
@ -115,8 +114,7 @@ import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens';
import { getTokenValueParam } from '../../shared/lib/metamask-controller-utils';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { hexToDecimal } from '../../shared/modules/conversion.utils';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { isMain, isFlask } from '../../shared/constants/environment';
///: BEGIN:ONLY_INCLUDE_IN(desktop)
// eslint-disable-next-line import/order
import { DesktopController } from '@metamask/desktop/dist/controllers/desktop';
///: END:ONLY_INCLUDE_IN
@ -131,7 +129,7 @@ import createDupeReqFilterMiddleware from './lib/createDupeReqFilterMiddleware';
import createLoggerMiddleware from './lib/createLoggerMiddleware';
import {
createMethodMiddleware,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
createSnapMethodMiddleware,
///: END:ONLY_INCLUDE_IN
} from './lib/rpc-method-middleware';
@ -151,7 +149,7 @@ import AlertController from './controllers/alert';
import OnboardingController from './controllers/onboarding';
import BackupController from './controllers/backup';
import IncomingTransactionsController from './controllers/incoming-transactions';
import DecryptMessageManager from './lib/decrypt-message-manager';
import DecryptMessageController from './controllers/decrypt-message';
import TransactionController from './controllers/transactions';
import DetectTokensController from './controllers/detect-tokens';
import SwapsController from './controllers/swaps';
@ -175,7 +173,7 @@ import {
NOTIFICATION_NAMES,
PermissionLogController,
unrestrictedMethods,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
buildSnapEndowmentSpecifications,
buildSnapRestrictedMethodSpecifications,
///: END:ONLY_INCLUDE_IN
@ -786,7 +784,7 @@ export default class MetamaskController extends EventEmitter {
);
},
}),
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
...this.getSnapPermissionSpecifications(),
///: END:ONLY_INCLUDE_IN
},
@ -807,7 +805,7 @@ export default class MetamaskController extends EventEmitter {
subjectCacheLimit: 100,
});
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
const snapExecutionServiceArgs = {
iframeUrl: new URL('https://execution.metamask.io/0.15.1/index.html'),
messenger: this.controllerMessenger.getRestricted({
@ -852,6 +850,9 @@ export default class MetamaskController extends EventEmitter {
],
});
const allowLocalSnaps = process.env.ALLOW_LOCAL_SNAPS;
const requireAllowlist = process.env.REQUIRE_SNAPS_ALLOWLIST;
this.snapController = new SnapController({
environmentEndowmentPermissions: Object.values(EndowmentPermissions),
excludedPermissions: {
@ -863,8 +864,8 @@ export default class MetamaskController extends EventEmitter {
messenger: snapControllerMessenger,
featureFlags: {
dappsCanUpdateSnaps: true,
allowLocalSnaps: isFlask,
requireAllowlist: isMain,
allowLocalSnaps,
requireAllowlist,
},
});
@ -933,8 +934,8 @@ export default class MetamaskController extends EventEmitter {
this.snapsRegistry = new JsonSnapsRegistry({
state: initState.SnapsRegistry,
messenger: snapsRegistryMessenger,
refetchOnAllowlistMiss: isMain,
failOnUnavailableRegistry: isMain,
refetchOnAllowlistMiss: requireAllowlist,
failOnUnavailableRegistry: requireAllowlist,
url: {
registry: 'https://acl.execution.metamask.io/latest/registry.json',
signature: 'https://acl.execution.metamask.io/latest/signature.json',
@ -943,6 +944,9 @@ export default class MetamaskController extends EventEmitter {
'0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6',
});
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(desktop)
this.desktopController = new DesktopController({
initState: initState.DesktopController,
});
@ -1151,7 +1155,17 @@ export default class MetamaskController extends EventEmitter {
);
this.networkController.lookupNetwork();
this.decryptMessageManager = new DecryptMessageManager({
this.decryptMessageController = new DecryptMessageController({
getState: this.getState.bind(this),
keyringController: this.keyringController,
messenger: this.controllerMessenger.getRestricted({
name: 'DecryptMessageController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
}),
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
@ -1251,7 +1265,7 @@ export default class MetamaskController extends EventEmitter {
() => {
this.txController.txStateManager.clearUnapprovedTxs();
this.encryptionPublicKeyController.clearUnapproved();
this.decryptMessageManager.clearUnapproved();
this.decryptMessageController.clearUnapproved();
this.signController.clearUnapproved();
},
);
@ -1316,11 +1330,14 @@ export default class MetamaskController extends EventEmitter {
this.signController.newUnsignedPersonalMessage.bind(
this.signController,
),
processDecryptMessage: this.newRequestDecryptMessage.bind(this),
processEncryptionPublicKey:
this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
processDecryptMessage:
this.decryptMessageController.newRequestDecryptMessage.bind(
this.decryptMessageController,
),
getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) =>
this.txController.getTransactions({
@ -1342,7 +1359,7 @@ export default class MetamaskController extends EventEmitter {
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
TokenRatesController: this.tokenRatesController,
DecryptMessageManager: this.decryptMessageManager.memStore,
DecryptMessageController: this.decryptMessageController,
EncryptionPublicKeyController: this.encryptionPublicKeyController,
SignController: this.signController,
SwapsController: this.swapsController.store,
@ -1374,11 +1391,13 @@ export default class MetamaskController extends EventEmitter {
SmartTransactionsController: this.smartTransactionsController,
NftController: this.nftController,
PhishingController: this.phishingController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
SnapController: this.snapController,
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(desktop)
DesktopController: this.desktopController.store,
///: END:ONLY_INCLUDE_IN
...resetOnRestartStore,
@ -1408,11 +1427,13 @@ export default class MetamaskController extends EventEmitter {
TokensController: this.tokensController,
SmartTransactionsController: this.smartTransactionsController,
NftController: this.nftController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
SnapController: this.snapController,
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(desktop)
DesktopController: this.desktopController.store,
///: END:ONLY_INCLUDE_IN
...resetOnRestartStore,
@ -1424,7 +1445,9 @@ export default class MetamaskController extends EventEmitter {
const resetMethods = [
this.accountTracker.resetState,
this.txController.resetState,
this.decryptMessageManager.resetState,
this.decryptMessageController.resetState.bind(
this.decryptMessageController,
),
this.encryptionPublicKeyController.resetState.bind(
this.encryptionPublicKeyController,
),
@ -1504,7 +1527,7 @@ export default class MetamaskController extends EventEmitter {
}
canUseHardwareWallets() {
return !isManifestV3 || process.env.CONF?.HARDWARE_WALLETS_MV3;
return !isManifestV3 || process.env.HARDWARE_WALLETS_MV3;
}
resetStates(resetMethods) {
@ -1517,7 +1540,7 @@ export default class MetamaskController extends EventEmitter {
});
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
/**
* Constructor helper for getting Snap permission specifications.
*/
@ -1659,7 +1682,7 @@ export default class MetamaskController extends EventEmitter {
getPermittedAccountsByOrigin,
);
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
// Record Snap metadata whenever a Snap is added to state.
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapAdded`,
@ -1960,6 +1983,10 @@ export default class MetamaskController extends EventEmitter {
this.networkController.upsertNetworkConfiguration.bind(
this.networkController,
),
getCurrentNetworkEIP1559Compatibility:
this.networkController.getEIP1559Compatibility.bind(
this.networkController,
),
// PreferencesController
setSelectedAddress: preferencesController.setSelectedAddress.bind(
preferencesController,
@ -2128,10 +2155,18 @@ export default class MetamaskController extends EventEmitter {
this.signController,
),
// decryptMessageManager
decryptMessage: this.decryptMessage.bind(this),
decryptMessageInline: this.decryptMessageInline.bind(this),
cancelDecryptMessage: this.cancelDecryptMessage.bind(this),
// decryptMessageController
decryptMessage: this.decryptMessageController.decryptMessage.bind(
this.decryptMessageController,
),
decryptMessageInline:
this.decryptMessageController.decryptMessageInline.bind(
this.decryptMessageController,
),
cancelDecryptMessage:
this.decryptMessageController.cancelDecryptMessage.bind(
this.decryptMessageController,
),
// EncryptionPublicKeyController
encryptionPublicKey:
@ -2165,7 +2200,7 @@ export default class MetamaskController extends EventEmitter {
rejectPermissionsRequest: this.rejectPermissionsRequest,
...getPermissionBackgroundApiMethods(permissionController),
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
// snaps
removeSnapError: this.controllerMessenger.call.bind(
this.controllerMessenger,
@ -2189,6 +2224,8 @@ export default class MetamaskController extends EventEmitter {
),
dismissNotifications: this.dismissNotifications.bind(this),
markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(desktop)
// Desktop
getDesktopEnabled: this.desktopController.getDesktopEnabled.bind(
this.desktopController,
@ -2488,7 +2525,7 @@ export default class MetamaskController extends EventEmitter {
// clear permissions
this.permissionController.clearState();
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
// Clear snap state
this.snapController.clearState();
// Clear notification state
@ -2614,7 +2651,7 @@ export default class MetamaskController extends EventEmitter {
async _loginUser() {
try {
// Automatic login via config password
const password = process.env.CONF?.PASSWORD;
const password = process.env.PASSWORD;
if (password && !process.env.IN_TEST) {
await this.submitPassword(password);
}
@ -3147,95 +3184,6 @@ export default class MetamaskController extends EventEmitter {
return await this.txController.newUnapprovedTransaction(txParams, req);
}
// eth_decrypt methods
/**
* Called when a dapp uses the eth_decrypt method.
*
* @param {object} msgParams - The params of the message to sign & return to the Dapp.
* @param {object} req - (optional) the original request, containing the origin
* Passed back to the requesting Dapp.
*/
async newRequestDecryptMessage(msgParams, req) {
const promise = this.decryptMessageManager.addUnapprovedMessageAsync(
msgParams,
req,
);
this.sendUpdate();
this.opts.showUserConfirmation();
return promise;
}
/**
* Only decrypt message and don't touch transaction state
*
* @param {object} msgParams - The params of the message to decrypt.
* @returns {Promise<object>} A full state update.
*/
async decryptMessageInline(msgParams) {
log.info('MetaMaskController - decryptMessageInline');
// decrypt the message inline
const msgId = msgParams.metamaskId;
const msg = this.decryptMessageManager.getMsg(msgId);
try {
const stripped = stripHexPrefix(msgParams.data);
const buff = Buffer.from(stripped, 'hex');
msgParams.data = JSON.parse(buff.toString('utf8'));
msg.rawData = await this.keyringController.decryptMessage(msgParams);
} catch (e) {
msg.error = e.message;
}
this.decryptMessageManager._updateMsg(msg);
return this.getState();
}
/**
* Signifies a user's approval to decrypt a message in queue.
* Triggers decrypt, and the callback function from newUnsignedDecryptMessage.
*
* @param {object} msgParams - The params of the message to decrypt & return to the Dapp.
* @returns {Promise<object>} A full state update.
*/
async decryptMessage(msgParams) {
log.info('MetaMaskController - decryptMessage');
const msgId = msgParams.metamaskId;
// sets the status op the message to 'approved'
// and removes the metamaskId for decryption
try {
const cleanMsgParams = await this.decryptMessageManager.approveMessage(
msgParams,
);
const stripped = stripHexPrefix(cleanMsgParams.data);
const buff = Buffer.from(stripped, 'hex');
cleanMsgParams.data = JSON.parse(buff.toString('utf8'));
// decrypt the message
const rawMess = await this.keyringController.decryptMessage(
cleanMsgParams,
);
// tells the listener that the message has been decrypted and can be returned to the dapp
this.decryptMessageManager.setMsgStatusDecrypted(msgId, rawMess);
} catch (error) {
log.info('MetaMaskController - eth_decrypt failed.', error);
this.decryptMessageManager.errorMessage(msgId, error);
}
return this.getState();
}
/**
* Used to cancel a eth_decrypt type message.
*
* @param {string} msgId - The ID of the message to cancel.
*/
cancelDecryptMessage(msgId) {
const messageManager = this.decryptMessageManager;
messageManager.rejectMsg(msgId);
return this.getState();
}
/**
* @returns {boolean} true if the keyring type supports EIP-1559
*/
@ -3543,7 +3491,7 @@ export default class MetamaskController extends EventEmitter {
if (subjectType === SubjectType.Internal) {
origin = ORIGIN_METAMASK;
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
else if (subjectType === SubjectType.Snap) {
origin = sender.snapId;
}
@ -3591,7 +3539,7 @@ export default class MetamaskController extends EventEmitter {
});
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
/**
* For snaps running in workers.
*
@ -3749,7 +3697,7 @@ export default class MetamaskController extends EventEmitter {
}),
);
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
engine.push(
createSnapMethodMiddleware(subjectType === SubjectType.Snap, {
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
@ -4247,7 +4195,7 @@ export default class MetamaskController extends EventEmitter {
}
};
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
updateCaveat = (origin, target, caveatType, caveatValue) => {
try {
this.controllerMessenger.call(

View File

@ -194,7 +194,7 @@ export default class ExtensionPlatform {
let message = `Transaction ${nonce} failed! ${
errorMessage || txMeta.err.message
}`;
///: BEGIN:ONLY_INCLUDE_IN(mmi)
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
if (isNaN(nonce)) {
message = `Transaction failed! ${errorMessage || txMeta.err.message}`;
}

View File

@ -22,7 +22,7 @@ import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.ut
import { SUPPORT_LINK } from '../../shared/lib/ui-utils';
import {
getErrorHtml,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
registerDesktopErrorActions,
///: END:ONLY_INCLUDE_IN
} from '../../shared/lib/error-utils';
@ -264,7 +264,7 @@ async function start() {
(
err,
store,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
) => {
@ -274,7 +274,7 @@ async function start() {
'troubleStarting',
err,
store,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
);
@ -302,7 +302,7 @@ async function start() {
displayCriticalError(
'troubleStarting',
err,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
undefined,
backgroundConnection,
///: END:ONLY_INCLUDE_IN
@ -345,7 +345,7 @@ function initializeUi(activeTab, connectionStream, cb) {
cb(
err,
null,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
);
@ -367,7 +367,7 @@ async function displayCriticalError(
errorKey,
err,
metamaskState,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
backgroundConnection,
///: END:ONLY_INCLUDE_IN
) {
@ -375,14 +375,14 @@ async function displayCriticalError(
errorKey,
SUPPORT_LINK,
metamaskState,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
err,
///: END:ONLY_INCLUDE_IN
);
container.innerHTML = html;
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
registerDesktopErrorActions(backgroundConnection, browser);
///: END:ONLY_INCLUDE_IN

214
builds.yml Normal file
View File

@ -0,0 +1,214 @@
# TODO(ritave): Add support for environments (<root>/development/build/constants.js:@ENVIRONMENT)
# TODO(ritave): Add support for build targets (<root>/development/build/constants.js:@BUILD_TARGETS)
# TODO(ritave): Warn if not all of declared variables have been defined / used
# The priority order of variable definitions (most important to least important):
# <hardcoded build code>; <environmental variables>; .metamaskprodrc; .metamaskrc; builds.yml:.buildTypes.<type>.env; builds.yml:.features.<feature>.env; builds.yml:.env
# The build type to use when no build type provided in the cli
default: &default main
# Declaration of build types
# Each build type is composed of features, env variables and assets.
# Also known as productFlavors in Android lingo
buildTypes:
main:
features:
- build-main
# Additional env variables that are specific to this build
env:
- INFURA_PROD_PROJECT_ID
- SEGMENT_PROD_WRITE_KEY
- INFURA_ENV_KEY_REF: INFURA_PROD_PROJECT_ID
- SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY
beta:
features:
- build-beta
env:
- INFURA_BETA_PROJECT_ID
- SEGMENT_BETA_WRITE_KEY
- INFURA_ENV_KEY_REF: INFURA_BETA_PROJECT_ID
- SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY
# Modifies how the version is displayed.
# eg. instead of 10.25.0 -> 10.25.0-beta.2
isPrerelease: true
# Folder which contains overrides to browser manifests
manifestOverrides: ./app/build-types/mmi/manifest/
flask:
# Code surrounded using code fences for that feature
# will not be removed
features:
- snaps
- desktop
- build-flask
env:
- INFURA_FLASK_PROJECT_ID
- SEGMENT_FLASK_WRITE_KEY
- ALLOW_LOCAL_SNAPS: true
- REQUIRE_SNAPS_ALLOWLIST: false
- SUPPORT_LINK: https://metamask-flask.zendesk.com/hc
- SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new
- INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID
- SEGMENT_WRITE_KEY_REF: SEGMENT_FLASK_WRITE_KEY
isPrerelease: true
desktop:
features:
- snaps
- desktop
- build-flask
env:
- INFURA_FLASK_PROJECT_ID
- SEGMENT_FLASK_WRITE_KEY
- ALLOW_LOCAL_SNAPS: true
- REQUIRE_SNAPS_ALLOWLIST: false
- SUPPORT_LINK: https://metamask-flask.zendesk.com/hc
- SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new
- INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID
- SEGMENT_WRITE_KEY_REF: SEGMENT_FLASK_WRITE_KEY
isPrerelease: true
mmi:
features:
- build-mmi
env:
- INFURA_MMI_PROJECT_ID
- SEGMENT_MMI_WRITE_KEY
- INFURA_ENV_KEY_REF: INFURA_MMI_PROJECT_ID
- SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY
- SUPPORT_LINK: https://mmi-support.zendesk.com/hc/en-us
- SUPPORT_REQUEST_LINK: https://mmi-support.zendesk.com/hc/en-us/requests/new
# For some reason, MMI uses this type of versioning
# Leaving it on for backwards compatibility
isPrerelease: true
# Build types are composed of a set of features.
# Each feature can have code fences that add new code
# as well declaring, defining and overriding env variables
features:
snaps:
# Each feature might have variables that only exist when built with that feature active
env:
# Whether to allow snaps from localhost - local://localhost:8080/snap.manifest.json
# Enabled in Flask, will be disabled in Main
- ALLOW_LOCAL_SNAPS
# Whether to verify that a snap can be installed using an allow list
- REQUIRE_SNAPS_ALLOWLIST
assets:
- ./{app,shared,ui}/**/snaps/**
desktop:
env:
- COMPATIBILITY_VERSION_EXTENSION: 1
- DISABLE_WEB_SOCKET_ENCRYPTION: false
- SKIP_OTP_PAIRING_FLOW: false
- WEB_SOCKET_PORT: null
###
# Build Type code extensions. Things like different support links, warning pages, banners
###
build-main:
build-beta:
assets:
# Assets that will be copied
- src: ./app/build-types/beta/images/
dest: images
# Assets that are exclusively included in this feature and ignored in others
# Supports globs
- ./{app,shared,ui}/**/beta/**
build-mmi:
assets:
- src: ./app/build-types/mmi/images/
dest: images
- ./{app,shared,ui}/**/mmi/**
build-flask:
assets:
- src: ./app/build-types/flask/images/
dest: images
- ./{app,shared,ui}/**/flask/**
# Env variables that are required for all types of builds
#
# env object supports both declarations (- FOO), and definitions (- FOO: BAR).
# Variables that were declared have to be defined somewhere in the load chain before usage
env:
- SWAPS_USE_DEV_APIS: false
- PORTFOLIO_URL: https://portfolio.metamask.io
- TOKEN_ALLOWANCE_IMPROVEMENTS: false
- TRANSACTION_SECURITY_PROVIDER: false
# The unlock password
- PASSWORD: null
# Also see METAMASK_DEBUG and NODE_DEBUG
- DEBUG: null
- SUPPORT_LINK: https://support.metamask.io
- SUPPORT_REQUEST_LINK: https://metamask.zendesk.com/hc/en-us
- SKIP_BACKGROUND_INITIALIZATION: false
- MULTICHAIN: false
# TODO(ritave): Move ManifestV3 into a feature?
- ENABLE_MV3: false
- HARDWARE_WALLETS_MV3: false
# These are exclusively used for MV3
- APPLY_LAVAMOAT
- FILE_NAMES
###
# API keys to 3rd party services
###
- PUBNUB_PUB_KEY: null
- PUBNUB_SUB_KEY: null
- SEGMENT_HOST: null
- SENTRY_DSN: null
- SENTRY_DSN_DEV: null
- OPENSEA_KEY: null
- ETHERSCAN_KEY: null
# also INFURA_PROJECT_ID below
###
# Build system backwards compatibility
###
- INFURA_ENV_KEY_REF
- SEGMENT_WRITE_KEY_REF
###
# Variables that are modified with hardcoded code
###
# Used for debugging changes to the phishing warning page.
# Modified in <root>/development/build/scripts.js:@getPhishingWarningPageUrl
- PHISHING_WARNING_PAGE_URL: null
# Modified in <root>/development/build/scripts.js:@getInfuraProjectId
- INFURA_PROJECT_ID
# Modified in <root>/development/build/scripts.js:@getSegmentWriteKey
- SEGMENT_WRITE_KEY: ''
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
# Also see DEBUG and NODE_DEBUG
- METAMASK_DEBUG: false
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
- ICON_NAMES
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
- IN_TEST
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
- METAMASK_ENVIRONMENT
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
- METAMASK_VERSION
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
- METAMASK_BUILD_TYPE
# Modified in <root>/development/build/scripts.js:@setEnvironmentVariables
- NODE_ENV
# Defined by node itself
# For the purposes of the build system we define it as empty below
# if it's not inside process.env
# Also see DEBUG and METAMASK_DEBUG
- NODE_DEBUG: ''
###
# Meta variables
###
# Uses yaml anchors to DRY - https://juju.is/docs/sdk/yaml-anchors-and-aliases
- METAMASK_BUILD_TYPE_DEFAULT: *default

View File

@ -6,10 +6,10 @@
// subset of files to check against these targets.
module.exports = {
global: {
lines: 69.3,
branches: 57,
statements: 68.5,
functions: 61.5,
lines: 69.92,
branches: 57.63,
statements: 69.24,
functions: 62.51,
},
transforms: {
branches: 100,

View File

@ -1,26 +1,13 @@
const path = require('path');
const { readFile } = require('fs/promises');
const assert = require('assert');
const { AssertionError } = require('assert');
const ini = require('ini');
const { BuildType } = require('../lib/build-type');
const { loadBuildTypesConfig } = require('../lib/build-type');
const { Variables } = require('../lib/variables');
const { ENVIRONMENT } = require('./constants');
const configurationPropertyNames = [
'MULTICHAIN',
'INFURA_PROJECT_ID',
'PHISHING_WARNING_PAGE_URL',
'PORTFOLIO_URL',
'SEGMENT_HOST',
'SEGMENT_WRITE_KEY',
'SENTRY_DSN_DEV',
'SWAPS_USE_DEV_APIS',
// Desktop
'COMPATIBILITY_VERSION_EXTENSION',
'DISABLE_WEB_SOCKET_ENCRYPTION',
'METAMASK_DEBUG',
'SKIP_OTP_PAIRING_FLOW',
'ENABLE_MV3',
];
const productionConfigurationPropertyNames = [
const VARIABLES_REQUIRED_IN_PRODUCTION = [
'INFURA_BETA_PROJECT_ID',
'INFURA_FLASK_PROJECT_ID',
'INFURA_PROD_PROJECT_ID',
@ -30,96 +17,145 @@ const productionConfigurationPropertyNames = [
'SENTRY_DSN',
];
/**
* Get configuration for non-production builds.
*
* @returns {object} The production configuration.
*/
async function getConfig() {
const configPath = path.resolve(__dirname, '..', '..', '.metamaskrc');
async function fromIniFile(filepath) {
let configContents = '';
try {
configContents = await readFile(configPath, {
configContents = await readFile(filepath, {
encoding: 'utf8',
});
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
return undefined;
}
const environmentVariables = {};
for (const propertyName of configurationPropertyNames) {
if (process.env[propertyName]) {
environmentVariables[propertyName] = process.env[propertyName];
const variables = ini.parse(configContents);
assert(
!Object.values(variables).some((variable) => typeof variable === 'object'),
`When loading ${filepath} - INI categories are not supported`,
);
const entries = Object.entries(variables);
const declarations = new Set(
entries.filter(([, value]) => value === '').map(([key]) => key),
);
const definitions = new Map(
entries
.filter(([, value]) => value !== '')
.map(([key, value]) => [key, value]),
);
return { declarations, definitions };
}
function fromEnv(declarations) {
const definitions = new Map(
[...declarations]
.filter((declaration) => declaration in process.env)
.map((declaration) => [declaration, process.env[declaration]]),
);
return { definitions, declarations: new Set() };
}
function fromBuildsYML(buildType, config) {
const extractDeclarations = (envArray) =>
envArray === undefined
? []
: envArray.map((env) => (typeof env === 'string' ? env : env.key));
const extractDefinitions = (envArray) =>
envArray === undefined
? []
: envArray.filter((env) => typeof env !== 'string');
// eslint-disable-next-line no-param-reassign
buildType = buildType ?? config.default;
const activeBuild = config.buildTypes[buildType];
const activeFeatures = activeBuild.features ?? [];
let declarations = [...extractDeclarations(config.env)];
activeFeatures
.map((feature) => config.features[feature])
.filter((feature) => feature !== null)
.forEach(({ env }) => declarations.push(...extractDeclarations(env)));
declarations.push(...extractDeclarations(activeBuild.env));
declarations = new Set(declarations);
const definitions = new Map();
// 1. root env
extractDefinitions(config.env).forEach(({ key, value }) =>
definitions.set(key, value),
);
// 2. features env
activeFeatures
.filter((key) => config.features[key] !== null)
.map((key) => config.features[key].env)
.map(extractDefinitions)
.flat()
.forEach(({ key, value }) => definitions.set(key, value));
// 3. build type env
extractDefinitions(activeBuild.env).forEach(({ key, value }) =>
definitions.set(key, value),
);
return { declarations, definitions, activeFeatures, activeBuild };
}
/**
*
* @param {string?} buildType - The chosen build type to build
* @param environment
* @returns Parsed configuration of the build pipeline
*/
async function getConfig(buildType, environment) {
const config = loadBuildTypesConfig();
const {
declarations: ymlDeclarations,
definitions: ymlDefinitions,
activeBuild,
activeFeatures,
} = await fromBuildsYML(buildType, config);
const variables = new Variables(ymlDeclarations);
// notice that maps have inverted value and key pair in forEach
ymlDefinitions.forEach((value, key) => variables.set(key, value));
(
await fromIniFile(path.resolve(__dirname, '..', '..', '.metamaskrc'))
)?.definitions.forEach((value, key) => variables.set(key, value));
(
await fromIniFile(path.resolve(__dirname, '..', '..', '.metamaskprodrc'))
)?.definitions.forEach((value, key) => variables.set(key, value));
fromEnv(ymlDeclarations).definitions.forEach((value, key) =>
variables.set(key, value),
);
// TODO(ritave): Move build targets and environments to builds.yml
if (environment === ENVIRONMENT.PRODUCTION) {
const undefinedVariables = VARIABLES_REQUIRED_IN_PRODUCTION.filter(
(variable) => !variables.isDefined(variable),
);
if (undefinedVariables.length !== 0) {
const message = `Some variables required to build production target are not defined.
- ${undefinedVariables.join('\n - ')}
`;
throw new AssertionError({ message });
}
}
return {
...ini.parse(configContents),
...environmentVariables,
variables,
activeBuild,
activeFeatures,
buildsYml: config,
};
}
/**
* Get configuration for production builds and perform validation.
*
* This function validates that all required variables are present, and that
* the production configuration file doesn't include any extraneous entries.
*
* @param {BuildType} buildType - The current build type (e.g. "main", "flask",
* etc.).
* @returns {object} The production configuration.
*/
async function getProductionConfig(buildType) {
const prodConfigPath = path.resolve(__dirname, '..', '..', '.metamaskprodrc');
let prodConfigContents = '';
try {
prodConfigContents = await readFile(prodConfigPath, {
encoding: 'utf8',
});
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
const environmentVariables = {};
for (const propertyName of productionConfigurationPropertyNames) {
if (process.env[propertyName]) {
environmentVariables[propertyName] = process.env[propertyName];
}
}
const prodConfig = {
...ini.parse(prodConfigContents),
...environmentVariables,
};
const requiredEnvironmentVariables = {
all: ['SENTRY_DSN'],
[BuildType.beta]: ['INFURA_BETA_PROJECT_ID', 'SEGMENT_BETA_WRITE_KEY'],
[BuildType.flask]: ['INFURA_FLASK_PROJECT_ID', 'SEGMENT_FLASK_WRITE_KEY'],
[BuildType.main]: ['INFURA_PROD_PROJECT_ID', 'SEGMENT_PROD_WRITE_KEY'],
[BuildType.mmi]: ['INFURA_MMI_PROJECT_ID', 'SEGMENT_MMI_WRITE_KEY'],
};
for (const required of [
...requiredEnvironmentVariables.all,
...requiredEnvironmentVariables[buildType],
]) {
if (!prodConfig[required]) {
throw new Error(`Missing '${required}' environment variable`);
}
}
const allValid = Object.values(requiredEnvironmentVariables).flat();
for (const environmentVariable of Object.keys(prodConfig)) {
if (!allValid.includes(environmentVariable)) {
throw new Error(`Invalid environment variable: '${environmentVariable}'`);
}
}
return prodConfig;
}
module.exports = { getConfig, getProductionConfig };
module.exports = {
getConfig,
};

View File

@ -6,7 +6,7 @@ const del = require('del');
const pify = require('pify');
const pump = pify(require('pump'));
const { BuildType } = require('../lib/build-type');
const { loadBuildTypesConfig } = require('../lib/build-type');
const { TASKS } = require('./constants');
const { createTask, composeParallel } = require('./task');
@ -42,7 +42,7 @@ function createEtcTasks({ browserPlatforms, buildType, livereload, version }) {
function createZipTask(platform, buildType, version) {
return async () => {
const path =
buildType === BuildType.main
buildType === loadBuildTypesConfig().default
? `metamask-${platform}-${version}`
: `metamask-${buildType}-${platform}-${version}`;
await pump(

View File

@ -10,8 +10,10 @@ const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { sync: globby } = require('globby');
const lavapack = require('@lavamoat/lavapack');
const difference = require('lodash/difference');
const { intersection } = require('lodash');
const { getVersion } = require('../lib/get-version');
const { BuildType, BuildTypeInheritance } = require('../lib/build-type');
const { loadBuildTypesConfig } = require('../lib/build-type');
const { TASKS, ENVIRONMENT } = require('./constants');
const {
createTask,
@ -71,65 +73,70 @@ async function defineAndRunBuildTasks() {
shouldLintFenceFiles,
skipStats,
version,
platform,
} = await parseArgv();
// scuttle on production/tests environment only
const shouldScuttle = ['dist', 'prod', 'test'].includes(entryTask);
const isRootTask = ['dist', 'prod', 'test', 'dev'].includes(entryTask);
console.log(
`Building lavamoat runtime file`,
`(scuttling is ${shouldScuttle ? 'on' : 'off'})`,
);
if (isRootTask) {
// scuttle on production/tests environment only
const shouldScuttle = ['dist', 'prod', 'test'].includes(entryTask);
// build lavamoat runtime file
await lavapack.buildRuntime({
scuttleGlobalThis: applyLavaMoat && shouldScuttle,
scuttleGlobalThisExceptions: [
// globals used by different mm deps outside of lm compartment
'toString',
'getComputedStyle',
'addEventListener',
'removeEventListener',
'ShadowRoot',
'HTMLElement',
'Element',
'pageXOffset',
'pageYOffset',
'visualViewport',
'Reflect',
'Set',
'Object',
'navigator',
'harden',
'console',
'Image', // Used by browser to generate notifications
// globals chrome driver needs to function (test env)
/cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu,
'performance',
'parseFloat',
'innerWidth',
'innerHeight',
'Symbol',
'Math',
'DOMRect',
'Number',
'Array',
'crypto',
'Function',
'Uint8Array',
'String',
'Promise',
// globals sentry needs to function
'__SENTRY__',
'appState',
'extra',
'stateHooks',
'sentryHooks',
'sentry',
],
});
console.log(
`Building lavamoat runtime file`,
`(scuttling is ${shouldScuttle ? 'on' : 'off'})`,
);
const browserPlatforms = ['firefox', 'chrome'];
// build lavamoat runtime file
await lavapack.buildRuntime({
scuttleGlobalThis: applyLavaMoat && shouldScuttle,
scuttleGlobalThisExceptions: [
// globals used by different mm deps outside of lm compartment
'toString',
'getComputedStyle',
'addEventListener',
'removeEventListener',
'ShadowRoot',
'HTMLElement',
'Element',
'pageXOffset',
'pageYOffset',
'visualViewport',
'Reflect',
'Set',
'Object',
'navigator',
'harden',
'console',
'Image', // Used by browser to generate notifications
// globals chrome driver needs to function (test env)
/cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu,
'performance',
'parseFloat',
'innerWidth',
'innerHeight',
'Symbol',
'Math',
'DOMRect',
'Number',
'Array',
'crypto',
'Function',
'Uint8Array',
'String',
'Promise',
// globals sentry needs to function
'__SENTRY__',
'appState',
'extra',
'stateHooks',
'sentryHooks',
'sentry',
],
});
}
const browserPlatforms = platform ? [platform] : ['firefox', 'chrome'];
const browserVersionMap = getBrowserVersionMap(browserPlatforms, version);
@ -271,9 +278,9 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`,
type: 'boolean',
})
.option('build-type', {
default: BuildType.main,
default: loadBuildTypesConfig().default,
description: 'The type of build to create.',
choices: Object.keys(BuildType),
choices: Object.keys(loadBuildTypesConfig().buildTypes),
})
.option('build-version', {
default: 0,
@ -311,6 +318,13 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`,
hidden: true,
type: 'boolean',
})
.option('platform', {
default: '',
description:
'Specify a single browser platform to build for. Either `chrome` or `firefox`',
hidden: true,
type: 'string',
})
.check((args) => {
if (!Number.isInteger(args.buildVersion)) {
throw new Error(
@ -335,6 +349,7 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`,
policyOnly,
skipStats,
task,
platform,
} = argv;
// Manually default this to `false` for dev builds only.
@ -365,6 +380,7 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`,
shouldLintFenceFiles,
skipStats,
version,
platform,
};
}
@ -376,29 +392,36 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`,
* build, or `null` if no files are to be ignored.
*/
function getIgnoredFiles(currentBuildType) {
const inheritedBuildTypes = BuildTypeInheritance[currentBuildType] || [];
const excludedFiles = Object.values(BuildType)
// This filter removes "main" and the current build type. The files of any
// build types that remain in the array will be excluded. "main" is the
// default build type, and has no files that are excluded from other builds.
.filter(
(buildType) =>
buildType !== BuildType.main &&
buildType !== currentBuildType &&
!inheritedBuildTypes.includes(buildType),
)
// Compute globs targeting files for exclusion for each excluded build
// type.
.reduce((excludedGlobs, excludedBuildType) => {
return excludedGlobs.concat([
`../../app/**/${excludedBuildType}/**`,
`../../shared/**/${excludedBuildType}/**`,
`../../ui/**/${excludedBuildType}/**`,
]);
}, [])
// This creates absolute paths of the form:
// PATH_TO_REPOSITORY_ROOT/app/**/${excludedBuildType}/**
.map((pathGlob) => path.resolve(__dirname, pathGlob));
const buildConfig = loadBuildTypesConfig();
const cwd = process.cwd();
return globby(excludedFiles);
const exclusiveAssetsForFeatures = (features) =>
globby(
features
.flatMap(
(feature) =>
buildConfig.features[feature].assets
?.filter((asset) => 'exclusiveInclude' in asset)
.map((asset) => asset.exclusiveInclude) ?? [],
)
.map((pathGlob) => path.resolve(cwd, pathGlob)),
);
const allFeatures = Object.keys(buildConfig.features);
const activeFeatures =
buildConfig.buildTypes[currentBuildType].features ?? [];
const inactiveFeatures = difference(allFeatures, activeFeatures);
const ignoredPaths = exclusiveAssetsForFeatures(inactiveFeatures);
// We do a sanity check to verify that any inactive feature haven't excluded files
// that active features are trying to include
const activePaths = exclusiveAssetsForFeatures(activeFeatures);
const conflicts = intersection(activePaths, ignoredPaths);
if (conflicts.length !== 0) {
throw new Error(`Below paths are required exclusively by both active and inactive features resulting in a conflict:
\t-> ${conflicts.join('\n\t-> ')}
Please fix builds.yml`);
}
return ignoredPaths;
}

View File

@ -6,7 +6,7 @@ const { mergeWith, cloneDeep, capitalize } = require('lodash');
const baseManifest = process.env.ENABLE_MV3
? require('../../app/manifest/v3/_base.json')
: require('../../app/manifest/v2/_base.json');
const { BuildType } = require('../lib/build-type');
const { loadBuildTypesConfig } = require('../lib/build-type');
const { TASKS, ENVIRONMENT } = require('./constants');
const { createTask, composeSeries } = require('./task');
@ -165,25 +165,24 @@ async function writeJson(obj, file) {
* Get manifest modifications for the given build type, including modifications specific to the
* given platform.
*
* @param {BuildType} buildType - The build type.
* @param {string} buildType - The build type.
* @param {string} platform - The platform (i.e. the browser).
* @returns {object} The build modificantions for the given build type and platform.
* @returns {object} The build modifications for the given build type and platform.
*/
async function getBuildModifications(buildType, platform) {
if (!Object.values(BuildType).includes(buildType)) {
const buildConfig = loadBuildTypesConfig();
if (!(buildType in buildConfig.buildTypes)) {
throw new Error(`Invalid build type: ${buildType}`);
} else if (buildType === BuildType.main) {
}
const overridesPath = buildConfig.buildTypes[buildType].manifestOverrides;
if (overridesPath === undefined) {
return {};
}
const builtTypeManifestDirectoryPath = path.resolve(
__dirname,
'..',
'..',
'app',
'build-types',
buildType,
'manifest',
process.cwd(),
overridesPath,
);
const baseBuildTypeModificationsPath = path.join(

View File

@ -1,7 +1,10 @@
// TODO(ritave): Remove switches on hardcoded build types
const { callbackify } = require('util');
const path = require('path');
const { writeFileSync, readFileSync } = require('fs');
const EventEmitter = require('events');
const assert = require('assert');
const gulp = require('gulp');
const watch = require('gulp-watch');
const Vinyl = require('vinyl');
@ -29,10 +32,9 @@ const bifyModuleGroups = require('bify-module-groups');
const phishingWarningManifest = require('@metamask/phishing-warning/package.json');
const { streamFlatMap } = require('../stream-flat-map');
const { BuildType } = require('../lib/build-type');
const { generateIconNames } = require('../generate-icon-names');
const { BUILD_TARGETS, ENVIRONMENT } = require('./constants');
const { getConfig, getProductionConfig } = require('./config');
const { getConfig } = require('./config');
const {
isDevBuild,
isTestBuild,
@ -101,67 +103,87 @@ const standardScuttlingConfig = {
* Get the appropriate Infura project ID.
*
* @param {object} options - The Infura project ID options.
* @param {BuildType} options.buildType - The current build type.
* @param {object} options.config - The environment variable configuration.
* @param {string} options.buildType - The current build type.
* @param {ENVIRONMENT[keyof ENVIRONMENT]} options.environment - The build environment.
* @param {boolean} options.testing - Whether this is a test build or not.
* @param options.variables
* @returns {string} The Infura project ID.
*/
function getInfuraProjectId({ buildType, config, environment, testing }) {
function getInfuraProjectId({ buildType, variables, environment, testing }) {
const EMPTY_PROJECT_ID = '00000000000000000000000000000000';
if (testing) {
return '00000000000000000000000000000000';
return EMPTY_PROJECT_ID;
} else if (environment !== ENVIRONMENT.PRODUCTION) {
// Skip validation because this is unset on PRs from forks.
return config.INFURA_PROJECT_ID;
} else if (buildType === BuildType.main) {
return config.INFURA_PROD_PROJECT_ID;
} else if (buildType === BuildType.beta) {
return config.INFURA_BETA_PROJECT_ID;
} else if (buildType === BuildType.flask) {
return config.INFURA_FLASK_PROJECT_ID;
} else if (buildType === BuildType.mmi) {
return config.INFURA_MMI_PROJECT_ID;
// For forks, return empty project ID if we don't have one.
if (
!variables.isDefined('INFURA_PROJECT_ID') &&
environment === ENVIRONMENT.PULL_REQUEST
) {
return EMPTY_PROJECT_ID;
}
return variables.get('INFURA_PROJECT_ID');
}
throw new Error(`Invalid build type: '${buildType}'`);
/** @type {string|undefined} */
const infuraKeyReference = process.env.INFURA_ENV_KEY_REF;
assert(
typeof infuraKeyReference === 'string' && infuraKeyReference.length > 0,
`Build type "${buildType}" has improperly set INFURA_ENV_KEY_REF in builds.yml. Current value: "${infuraKeyReference}"`,
);
/** @type {string|undefined} */
const infuraProjectId = variables.get(infuraKeyReference);
assert(
typeof infuraProjectId === 'string' && infuraProjectId.length > 0,
`Infura Project ID environmental variable "${infuraKeyReference}" is set improperly.`,
);
return infuraProjectId;
}
/**
* Get the appropriate Segment write key.
*
* @param {object} options - The Segment write key options.
* @param {BuildType} options.buildType - The current build type.
* @param {object} options.config - The environment variable configuration.
* @param {string} options.buildType - The current build type.
* @param {keyof ENVIRONMENT} options.environment - The current build environment.
* @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline
* @returns {string} The Segment write key.
*/
function getSegmentWriteKey({ buildType, config, environment }) {
function getSegmentWriteKey({ buildType, variables, environment }) {
if (environment !== ENVIRONMENT.PRODUCTION) {
// Skip validation because this is unset on PRs from forks, and isn't necessary for development builds.
return config.SEGMENT_WRITE_KEY;
} else if (buildType === BuildType.main) {
return config.SEGMENT_PROD_WRITE_KEY;
} else if (buildType === BuildType.beta) {
return config.SEGMENT_BETA_WRITE_KEY;
} else if (buildType === BuildType.flask) {
return config.SEGMENT_FLASK_WRITE_KEY;
} else if (buildType === BuildType.mmi) {
return config.SEGMENT_MMI_WRITE_KEY;
return variables.get('SEGMENT_WRITE_KEY');
}
throw new Error(`Invalid build type: '${buildType}'`);
const segmentKeyReference = process.env.SEGMENT_WRITE_KEY_REF;
assert(
typeof segmentKeyReference === 'string' && segmentKeyReference.length > 0,
`Build type "${buildType}" has improperly set SEGMENT_WRITE_KEY_REF in builds.yml. Current value: "${segmentKeyReference}"`,
);
const segmentWriteKey = variables.get(segmentKeyReference);
assert(
typeof segmentWriteKey === 'string' && segmentWriteKey.length > 0,
`Segment Write Key environmental variable "${segmentKeyReference}" is set improperly.`,
);
return segmentWriteKey;
}
/**
* Get the URL for the phishing warning page, if it has been set.
*
* @param {object} options - The phishing warning page options.
* @param {object} options.config - The environment variable configuration.
* @param {boolean} options.testing - Whether this is a test build or not.
* @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline
* @returns {string} The URL for the phishing warning page, or `undefined` if no URL is set.
*/
function getPhishingWarningPageUrl({ config, testing }) {
let phishingWarningPageUrl = config.PHISHING_WARNING_PAGE_URL;
function getPhishingWarningPageUrl({ variables, testing }) {
let phishingWarningPageUrl = variables.get('PHISHING_WARNING_PAGE_URL');
if (!phishingWarningPageUrl) {
assert(
phishingWarningPageUrl === null ||
typeof phishingWarningPageUrl === 'string',
);
if (phishingWarningPageUrl === null) {
phishingWarningPageUrl = testing
? 'http://localhost:9999/'
: `https://metamask.github.io/phishing-warning/v${phishingWarningManifest.version}/`;
@ -209,7 +231,7 @@ module.exports = createScriptTasks;
* LavaMoat at runtime or not.
* @param {string[]} options.browserPlatforms - A list of browser platforms to
* build bundles for.
* @param {BuildType} options.buildType - The current build type (e.g. "main",
* @param {string} options.buildType - The current build type (e.g. "main",
* "flask", etc.).
* @param {string[] | null} options.ignoredFiles - A list of files to exclude
* from the current build.
@ -455,7 +477,7 @@ function createScriptTasks({
* @param {string[]} options.browserPlatforms - A list of browser platforms to
* build bundles for.
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
* @param {BuildType} options.buildType - The current build type (e.g. "main",
* @param {string} options.buildType - The current build type (e.g. "main",
* "flask", etc.).
* @param {string[] | null} options.ignoredFiles - A list of files to exclude
* from the current build.
@ -541,7 +563,7 @@ async function createManifestV3AppInitializationBundle({
* @param {string[]} options.browserPlatforms - A list of browser platforms to
* build bundles for.
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
* @param {BuildType} options.buildType - The current build type (e.g. "main",
* @param {string} options.buildType - The current build type (e.g. "main",
* "flask", etc.).
* @param {string[]} options.entryFiles - A list of entry point file paths,
* relative to the repository root directory.
@ -576,18 +598,29 @@ function createFactoredBuild({
const reloadOnChange = isDevBuild(buildTarget);
const minify = !isDevBuild(buildTarget);
const envVars = await getEnvironmentVariables({
const environment = getEnvironment({ buildTarget });
const config = await getConfig(buildType, environment);
const { variables, activeBuild } = config;
await setEnvironmentVariables({
buildTarget,
buildType,
environment,
variables,
activeBuild,
version,
});
const features = {
active: new Set(activeBuild.features ?? []),
all: new Set(Object.keys(config.buildsYml.features)),
};
setupBundlerDefaults(buildConfiguration, {
buildTarget,
buildType,
envVars,
variables,
envVars: buildSafeVariableObject(variables),
ignoredFiles,
policyOnly,
minify,
features,
reloadOnChange,
shouldLintFenceFiles,
});
@ -760,7 +793,7 @@ function createFactoredBuild({
* @param {string[]} options.browserPlatforms - A list of browser platforms to
* build the bundle for.
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
* @param {BuildType} options.buildType - The current build type (e.g. "main",
* @param {string} options.buildType - The current build type (e.g. "main",
* "flask", etc.).
* @param {string} options.destFilepath - The file path the bundle should be
* written to.
@ -806,20 +839,31 @@ function createNormalBundle({
const reloadOnChange = Boolean(devMode);
const minify = Boolean(devMode) === false;
const envVars = {
...(await getEnvironmentVariables({
buildTarget,
buildType,
version,
})),
...extraEnvironmentVariables,
const environment = getEnvironment({ buildTarget });
const config = await getConfig(buildType, environment);
const { activeBuild, variables } = config;
await setEnvironmentVariables({
buildTarget,
buildType,
variables,
environment,
activeBuild,
version,
});
Object.entries(extraEnvironmentVariables ?? {}).forEach(([key, value]) =>
variables.set(key, value),
);
const features = {
active: new Set(activeBuild.features ?? []),
all: new Set(Object.keys(config.buildsYml.features)),
};
setupBundlerDefaults(buildConfiguration, {
buildType,
envVars,
envVars: buildSafeVariableObject(variables),
ignoredFiles,
policyOnly,
minify,
features,
reloadOnChange,
shouldLintFenceFiles,
applyLavaMoat,
@ -865,11 +909,11 @@ function setupBundlerDefaults(
buildConfiguration,
{
buildTarget,
buildType,
envVars,
ignoredFiles,
policyOnly,
minify,
features,
reloadOnChange,
shouldLintFenceFiles,
applyLavaMoat,
@ -882,7 +926,7 @@ function setupBundlerDefaults(
// Source transforms
transform: [
// // Remove code that should be excluded from builds of the current type
createRemoveFencedCodeTransform(buildType, shouldLintFenceFiles),
createRemoveFencedCodeTransform(features, shouldLintFenceFiles),
// Transpile top-level code
[
babelify,
@ -1074,6 +1118,11 @@ async function createBundle(buildConfiguration, { reloadOnChange }) {
logError(error);
process.exit(1);
});
pipeline.on('error', (error) => {
console.error('Pipeline failed! See details below.');
logError(error);
process.exit(1);
});
}
// trigger build pipeline instrumentations
events.emit('configurePipeline', { pipeline, bundleStream });
@ -1090,56 +1139,55 @@ async function createBundle(buildConfiguration, { reloadOnChange }) {
}
/**
* Get environment variables to inject in the current build.
* Sets environment variables to inject in the current build.
*
* @param {object} options - Build options.
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
* @param {BuildType} options.buildType - The current build type (e.g. "main",
* @param {string} options.buildType - The current build type (e.g. "main",
* "flask", etc.).
* @param {string} options.version - The current version of the extension.
* @returns {object} A map of environment variables to inject.
* @param options.activeBuild
* @param options.variables
* @param options.environment
*/
async function getEnvironmentVariables({ buildTarget, buildType, version }) {
const environment = getEnvironment({ buildTarget });
const config =
environment === ENVIRONMENT.PRODUCTION
? await getProductionConfig(buildType)
: await getConfig();
async function setEnvironmentVariables({
buildTarget,
buildType,
activeBuild,
environment,
variables,
version,
}) {
const devMode = isDevBuild(buildTarget);
const testing = isTestBuild(buildTarget);
const iconNames = await generateIconNames();
return {
variables.set({
ICON_NAMES: iconNames,
MULTICHAIN: config.MULTICHAIN === '1',
CONF: devMode ? config : {},
ENABLE_MV3: config.ENABLE_MV3,
IN_TEST: testing,
INFURA_PROJECT_ID: getInfuraProjectId({
buildType,
config,
activeBuild,
variables,
environment,
testing,
}),
METAMASK_DEBUG: devMode || config.METAMASK_DEBUG === '1',
METAMASK_DEBUG: devMode || variables.getMaybe('METAMASK_DEBUG') === true,
METAMASK_ENVIRONMENT: environment,
METAMASK_VERSION: version,
METAMASK_BUILD_TYPE: buildType,
NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION,
PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({ config, testing }),
PORTFOLIO_URL: config.PORTFOLIO_URL || 'https://portfolio.metamask.io',
SEGMENT_HOST: config.SEGMENT_HOST,
SEGMENT_WRITE_KEY: getSegmentWriteKey({ buildType, config, environment }),
SENTRY_DSN: config.SENTRY_DSN,
SENTRY_DSN_DEV: config.SENTRY_DSN_DEV,
SWAPS_USE_DEV_APIS: config.SWAPS_USE_DEV_APIS === '1',
TOKEN_ALLOWANCE_IMPROVEMENTS: config.TOKEN_ALLOWANCE_IMPROVEMENTS === '1',
TRANSACTION_SECURITY_PROVIDER: config.TRANSACTION_SECURITY_PROVIDER === '1',
// Desktop
COMPATIBILITY_VERSION_EXTENSION: config.COMPATIBILITY_VERSION_EXTENSION,
DISABLE_WEB_SOCKET_ENCRYPTION: config.DISABLE_WEB_SOCKET_ENCRYPTION === '1',
SKIP_OTP_PAIRING_FLOW: config.SKIP_OTP_PAIRING_FLOW === '1',
};
PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({
variables,
testing,
}),
SEGMENT_WRITE_KEY: getSegmentWriteKey({
buildType,
activeBuild,
variables,
environment,
}),
});
}
function renderHtmlFile({
@ -1172,6 +1220,29 @@ function renderHtmlFile({
});
}
/**
* Builds a javascript object that throws an error when trying to access a property that wasn't defined properly
*
* @param {Variables} variables
* @returns {{[key: string]: unknown }} Variable definitions
*/
function buildSafeVariableObject(variables) {
return new Proxy(
{},
{
has(_, key) {
return key !== '_'; // loose-envify uses "_" for settings
},
get(_, key) {
if (key === '_') {
return undefined; // loose-envify uses "_" for settings
}
return variables.get(key);
},
},
);
}
function beep() {
process.stdout.write('\x07');
}

View File

@ -4,7 +4,7 @@ const watch = require('gulp-watch');
const glob = require('fast-glob');
const locales = require('../../app/_locales/index.json');
const { BuildType } = require('../lib/build-type');
const { loadBuildTypesConfig } = require('../lib/build-type');
const { TASKS } = require('./constants');
const { createTask, composeSeries } = require('./task');
@ -31,41 +31,23 @@ module.exports = function createStaticAssetTasks({
copyTargetsDevs[browser] = copyTargetsDev;
});
const additionalBuildTargets = {
[BuildType.beta]: [
{
src: './app/build-types/beta/images/',
dest: `images`,
},
],
[BuildType.flask]: [
{
src: './app/build-types/flask/images/',
dest: `images`,
},
],
[BuildType.desktop]: [
{
src: './app/build-types/desktop/images/',
dest: `images`,
},
],
[BuildType.mmi]: [
{
src: './app/build-types/mmi/images/',
dest: `images`,
},
],
};
const buildConfig = loadBuildTypesConfig();
if (Object.keys(additionalBuildTargets).includes(buildType)) {
Object.entries(copyTargetsProds).forEach(([_, copyTargetsProd]) =>
copyTargetsProd.push(...additionalBuildTargets[buildType]),
);
Object.entries(copyTargetsDevs).forEach(([_, copyTargetsDev]) =>
copyTargetsDev.push(...additionalBuildTargets[buildType]),
);
}
const activeFeatures = buildConfig.buildTypes[buildType].features ?? [];
const additionalAssets = activeFeatures.flatMap(
(feature) =>
buildConfig.features[feature].assets?.filter(
(asset) => !('exclusiveInclude' in asset),
) ?? [],
);
Object.entries(copyTargetsProds).forEach(([_, copyTargetsProd]) =>
copyTargetsProd.push(...additionalAssets),
);
Object.entries(copyTargetsDevs).forEach(([_, copyTargetsDev]) =>
copyTargetsDev.push(...additionalAssets),
);
const prodTasks = [];
Object.entries(copyTargetsProds).forEach(([browser, copyTargetsProd]) => {
@ -120,7 +102,7 @@ module.exports = function createStaticAssetTasks({
await Promise.all(
sources.map(async (src) => {
const relativePath = path.relative(baseDir, src);
await fs.copy(src, `${dest}${relativePath}`);
await fs.copy(src, `${dest}${relativePath}`, { overwrite: true });
}),
);
}

View File

@ -66,12 +66,10 @@ function createStyleTasks({ livereload }) {
async function buildScssPipeline(src, dest, devMode, rtl) {
if (!sass) {
// eslint-disable-next-line node/global-require
sass = require('gulp-dart-sass');
// use our own compiler which runs sass in its own process
// in order to not pollute the intrinsics
// eslint-disable-next-line node/global-require
sass.compiler = require('./sass-compiler');
sass = require('gulp-sass')(require('./sass-compiler'));
}
await pump(
...[

View File

@ -80,12 +80,9 @@ function runInChildProcess(
);
// forward logs to main process
// skip the first stdout event (announcing the process command)
childProcess.stdout.once('data', () => {
childProcess.stdout.on('data', (data) =>
process.stdout.write(`${taskName}: ${data}`),
);
});
childProcess.stdout.on('data', (data) =>
process.stdout.write(`${taskName}: ${data}`),
);
childProcess.stderr.on('data', (data) =>
process.stderr.write(`${taskName}: ${data}`),

View File

@ -29,7 +29,7 @@ this.store.updateStructure({
...,
GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
SnapController: this.snapController,
///: END:ONLY_INCLUDE_IN
});
@ -49,7 +49,7 @@ Note that multiple build types can be specified by separating them with
commands inside the parameter parentheses:
```javascript
///: BEGIN:ONLY_INCLUDE_IN(beta,flask)
///: BEGIN:ONLY_INCLUDE_IN(build-beta,build-flask)
```
### Gotchas
@ -114,7 +114,7 @@ only be included in a particular build type, say `beta`, they should be wrapped
as follows:
```javascript
///: BEGIN:ONLY_INCLUDE_IN(beta)
///: BEGIN:ONLY_INCLUDE_IN(build-beta)
console.log('I am only included in beta builds.');
///: END:ONLY_INCLUDE_IN
```

View File

@ -0,0 +1,4 @@
export type Features = {
active: ReadonlySet<string>;
all: ReadonlySet<string>;
};

View File

@ -1,6 +1,5 @@
const path = require('path');
const { PassThrough, Transform } = require('stream');
const { BuildType, BuildTypeInheritance } = require('../../lib/build-type');
const { lintTransformedFile } = require('./utils');
const hasKey = (obj, key) => Reflect.hasOwnProperty.call(obj, key);
@ -18,14 +17,14 @@ class RemoveFencedCodeTransform extends Transform {
* Optionally lints the file if it was modified.
*
* @param {string} filePath - The path to the file being transformed.
* @param {string} buildType - The type of the current build process.
* @param {import('./remove-fenced-code').Features} features - Features that are currently enabled.
* @param {boolean} shouldLintTransformedFiles - Whether the file should be
* linted if modified by the transform.
*/
constructor(filePath, buildType, shouldLintTransformedFiles) {
constructor(filePath, features, shouldLintTransformedFiles) {
super();
this.filePath = filePath;
this.buildType = buildType;
this.features = features;
this.shouldLintTransformedFiles = shouldLintTransformedFiles;
this._fileBuffers = [];
}
@ -45,7 +44,7 @@ class RemoveFencedCodeTransform extends Transform {
try {
[fileContent, didModify] = removeFencedCode(
this.filePath,
this.buildType,
this.features,
Buffer.concat(this._fileBuffers).toString('utf8'),
);
} catch (error) {
@ -82,20 +81,14 @@ class RemoveFencedCodeTransform extends Transform {
* file is ignored by ESLint, since linting is our first line of defense against
* making un-syntactic modifications to files using code fences.
*
* @param {string} buildType - The type of the current build.
* @param {import('./remove-fenced-code').Features} features - Features that are currently enabled.
* @param {boolean} shouldLintTransformedFiles - Whether to lint transformed files.
* @returns {(filePath: string) => Transform} The transform function.
*/
function createRemoveFencedCodeTransform(
buildType,
features,
shouldLintTransformedFiles = true,
) {
if (!hasKey(BuildType, buildType)) {
throw new Error(
`Code fencing transform received unrecognized build type "${buildType}".`,
);
}
// Browserify transforms are functions that receive a file name and return a
// duplex stream. The stream receives the file contents piecemeal in the form
// of Buffers.
@ -117,7 +110,7 @@ function createRemoveFencedCodeTransform(
return new RemoveFencedCodeTransform(
filePath,
buildType,
features,
shouldLintTransformedFiles,
);
};
@ -132,31 +125,39 @@ const DirectiveCommands = {
ONLY_INCLUDE_IN: 'ONLY_INCLUDE_IN',
};
const CommandValidators = {
[DirectiveCommands.ONLY_INCLUDE_IN]: (params, filePath) => {
if (!params || params.length === 0) {
throw new Error(
getInvalidParamsMessage(
filePath,
DirectiveCommands.ONLY_INCLUDE_IN,
`No params specified.`,
),
);
}
params.forEach((param) => {
if (!hasKey(BuildType, param)) {
/**
* Factory function for command validators.
*
* @param {{ features: import('./remove-fenced-code').Features }} config - Configuration required for validation.
* @returns A mapping of command -> validator function.
*/
function CommandValidators({ features }) {
return {
[DirectiveCommands.ONLY_INCLUDE_IN]: (params, filePath) => {
if (!params || params.length === 0) {
throw new Error(
getInvalidParamsMessage(
filePath,
DirectiveCommands.ONLY_INCLUDE_IN,
`"${param}" is not a valid build type.`,
`No params specified.`,
),
);
}
});
},
};
for (const param of params) {
if (!features.all.has(param)) {
throw new Error(
getInvalidParamsMessage(
filePath,
DirectiveCommands.ONLY_INCLUDE_IN,
`"${param}" is not a declared build feature.`,
),
);
}
}
},
};
}
// Matches lines starting with "///:", and any preceding whitespace, except
// newlines. We except newlines to avoid eating blank lines preceding a fenced
@ -171,7 +172,8 @@ const fenceSentinelRegex = /^\s*\/\/\/:/u;
// At this stage of parsing, we are looking for one of:
// - TERMINUS:COMMAND(PARAMS)
// - TERMINUS:COMMAND
const directiveParsingRegex = /^([A-Z]+):([A-Z_]+)(?:\(((?:\w+,)*\w+)\))?$/u;
const directiveParsingRegex =
/^([A-Z]+):([A-Z_]+)(?:\(((?:\w[-\w]*,)*\w[-\w]*)\))?$/u;
/**
* Removes fenced code from the given JavaScript source string. "Fenced code"
@ -186,7 +188,7 @@ const directiveParsingRegex = /^([A-Z]+):([A-Z_]+)(?:\(((?:\w+,)*\w+)\))?$/u;
* Here's an example of a valid fence:
*
* ```javascript
* ///: BEGIN:ONLY_INCLUDE_IN(flask)
* ///: BEGIN:ONLY_INCLUDE_IN(build-flask)
* console.log('I am Flask.');
* ///: END:ONLY_INCLUDE_IN
* ```
@ -194,12 +196,12 @@ const directiveParsingRegex = /^([A-Z]+):([A-Z_]+)(?:\(((?:\w+,)*\w+)\))?$/u;
* For details, please see the documentation.
*
* @param {string} filePath - The path to the file being transformed.
* @param {string} typeOfCurrentBuild - The type of the current build.
* @param {import('./remove-fenced-code').Features} features - Features that are currently active.
* @param {string} fileContent - The contents of the file being transformed.
* @returns {[string, modified]} A tuple of the post-transform file contents and
* a boolean indicating whether they were modified.
*/
function removeFencedCode(filePath, typeOfCurrentBuild, fileContent) {
function removeFencedCode(filePath, features, fileContent) {
// Do not modify the file if we detect an inline sourcemap. For reasons
// yet to be determined, the transform receives every file twice while in
// watch mode, the second after Babel has transpiled the file. Babel adds
@ -318,6 +320,7 @@ function removeFencedCode(filePath, typeOfCurrentBuild, fileContent) {
const splicingIndices = [];
let shouldSplice = false;
let currentCommand;
const commandValidators = CommandValidators({ features });
for (let i = 0; i < parsedDirectives.length; i++) {
const { line, indices, terminus, command, parameters } =
@ -335,21 +338,17 @@ function removeFencedCode(filePath, typeOfCurrentBuild, fileContent) {
currentCommand = command;
// Throws an error if the command parameters are invalid
CommandValidators[command](parameters, filePath);
commandValidators[command](parameters, filePath);
const validBuildTypes = [
typeOfCurrentBuild,
...(BuildTypeInheritance[typeOfCurrentBuild] || []),
];
const buildTypeMatches = validBuildTypes.some((validBuildType) =>
parameters.includes(validBuildType),
const blockIsActive = parameters.some((param) =>
features.active.has(param),
);
if (buildTypeMatches) {
if (blockIsActive) {
shouldSplice = false;
} else {
shouldSplice = true;
// Add start index of BEGIN directive line to splicing indices
splicingIndices.push(indices[0]);
}

View File

@ -1,5 +1,5 @@
const deepFreeze = require('deep-freeze-strict');
const { BuildType } = require('../../lib/build-type');
const { loadBuildTypesConfig } = require('../../lib/build-type');
const {
createRemoveFencedCodeTransform,
removeFencedCode,
@ -14,12 +14,22 @@ jest.mock('./utils', () => ({
// file because it takes up a lot of lines and is very distracting.
const testData = getTestData();
const getMinimalFencedCode = (params = 'flask') =>
const MAIN_BUILD = 'build-main';
const FLASK_BUILD = 'build-flask';
const getMinimalFencedCode = (params = FLASK_BUILD) =>
`///: BEGIN:ONLY_INCLUDE_IN(${params})
Conditionally_Included
///: END:ONLY_INCLUDE_IN
`;
const getFeatures = ({ all, active }) => ({
all: new Set(all),
active: new Set(active),
});
const buildTypesConfig = loadBuildTypesConfig();
describe('build/transforms/remove-fenced-code', () => {
describe('createRemoveFencedCodeTransform', () => {
const { lintTransformedFile: lintTransformedFileMock } = transformUtils;
@ -29,15 +39,14 @@ describe('build/transforms/remove-fenced-code', () => {
lintTransformedFileMock.mockImplementation(() => Promise.resolve());
});
it('rejects invalid build types', () => {
expect(() => createRemoveFencedCodeTransform('foobar')).toThrow(
/received unrecognized build type "foobar".$/u,
);
});
it('returns a PassThrough stream for files with ignored extensions', async () => {
const fileContent = '"Valid JSON content"\n';
const stream = createRemoveFencedCodeTransform('main')('file.json');
const stream = createRemoveFencedCodeTransform(
getFeatures({
active: [MAIN_BUILD],
all: [MAIN_BUILD],
}),
)('file.json');
let streamOutput = '';
await new Promise((resolve) => {
@ -60,7 +69,12 @@ describe('build/transforms/remove-fenced-code', () => {
const filePrefix = '// A comment\n';
const fileContent = filePrefix.concat(getMinimalFencedCode());
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
const stream = createRemoveFencedCodeTransform(
getFeatures({
active: [MAIN_BUILD],
all: [MAIN_BUILD, FLASK_BUILD],
}),
)(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
@ -92,7 +106,12 @@ describe('build/transforms/remove-fenced-code', () => {
.filter((line) => line !== '')
.map((line) => `${line}\n`);
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
const stream = createRemoveFencedCodeTransform(
getFeatures({
active: [MAIN_BUILD],
all: [MAIN_BUILD, FLASK_BUILD],
}),
)(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
@ -116,9 +135,14 @@ describe('build/transforms/remove-fenced-code', () => {
});
it('handles file with fences that is unmodified by the transform', async () => {
const fileContent = getMinimalFencedCode('main');
const fileContent = getMinimalFencedCode(MAIN_BUILD);
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
const stream = createRemoveFencedCodeTransform(
getFeatures({
active: [MAIN_BUILD],
all: [MAIN_BUILD],
}),
)(mockJsFileName);
let streamOutput = '';
await new Promise((resolve) => {
@ -141,7 +165,7 @@ describe('build/transforms/remove-fenced-code', () => {
const fileContent = filePrefix.concat(getMinimalFencedCode());
const stream = createRemoveFencedCodeTransform(
'main',
getFeatures({ all: [MAIN_BUILD, FLASK_BUILD], active: [MAIN_BUILD] }),
false,
)(mockJsFileName);
let streamOutput = '';
@ -166,7 +190,9 @@ describe('build/transforms/remove-fenced-code', () => {
'///: END:ONLY_INCLUDE_IN',
);
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
const stream = createRemoveFencedCodeTransform(
getFeatures({ all: [MAIN_BUILD, FLASK_BUILD], active: [MAIN_BUILD] }),
)(mockJsFileName);
await new Promise((resolve) => {
stream.on('error', (error) => {
@ -191,7 +217,9 @@ describe('build/transforms/remove-fenced-code', () => {
const filePrefix = '// A comment\n';
const fileContent = filePrefix.concat(getMinimalFencedCode());
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
const stream = createRemoveFencedCodeTransform(
getFeatures({ all: [FLASK_BUILD], active: [] }),
)(mockJsFileName);
await new Promise((resolve) => {
stream.on('error', (error) => {
@ -213,12 +241,15 @@ describe('build/transforms/remove-fenced-code', () => {
const mockFileName = 'file.js';
// Valid inputs
Object.keys(BuildType).forEach((buildType) => {
['main', 'flask', 'beta'].forEach((buildType) => {
const active = buildTypesConfig.buildTypes[buildType].features;
const all = Object.keys(buildTypesConfig.features);
const features = getFeatures({ all, active });
it(`transforms file with fences for build type "${buildType}"`, () => {
expect(
removeFencedCode(
mockFileName,
buildType,
features,
testData.validInputs.withFences,
),
).toStrictEqual(testData.validOutputs[buildType]);
@ -226,15 +257,15 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
buildType,
features,
testData.validInputs.extraContentWithFences,
),
).toStrictEqual(testData.validOutputsWithExtraContent[buildType]);
// Ensure that the minimal input template is in fact valid
const minimalInput = getMinimalFencedCode(buildType);
const minimalInput = getMinimalFencedCode(`build-${buildType}`);
expect(
removeFencedCode(mockFileName, buildType, minimalInput),
removeFencedCode(mockFileName, features, minimalInput),
).toStrictEqual([minimalInput, false]);
});
@ -242,7 +273,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
buildType,
features,
testData.validInputs.withoutFences,
),
).toStrictEqual([testData.validInputs.withoutFences, false]);
@ -250,7 +281,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
buildType,
features,
testData.validInputs.extraContentWithoutFences,
),
).toStrictEqual([
@ -265,29 +296,17 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode('main'),
getFeatures({
active: [FLASK_BUILD],
all: [FLASK_BUILD, MAIN_BUILD],
}),
getMinimalFencedCode(MAIN_BUILD),
),
).toStrictEqual(['', true]);
});
it('keeps fences with inherited build types', () => {
// Desktop inherits from the flask build type
const minimalCode =
getMinimalFencedCode(BuildType.flask) +
getMinimalFencedCode(BuildType.desktop);
expect(
removeFencedCode(mockFileName, BuildType.desktop, minimalCode),
).toStrictEqual([minimalCode, false]);
expect(
removeFencedCode(mockFileName, BuildType.flask, minimalCode),
).toStrictEqual([getMinimalFencedCode(BuildType.flask), true]);
});
it('ignores sentinels preceded by non-whitespace', () => {
const validBeginDirective = '///: BEGIN:ONLY_INCLUDE_IN(flask)\n';
const validBeginDirective = '///: BEGIN:ONLY_INCLUDE_IN(build-flask)\n';
const ignoredLines = [
`a ${validBeginDirective}`,
`2 ${validBeginDirective}`,
@ -299,8 +318,11 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
BuildType.flask,
getMinimalFencedCode('main').concat(ignoredLine),
getFeatures({
active: [FLASK_BUILD],
all: [FLASK_BUILD, MAIN_BUILD],
}),
getMinimalFencedCode(MAIN_BUILD).concat(ignoredLine),
),
).toStrictEqual([ignoredLine, true]);
@ -311,7 +333,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
modifiedInputWithoutFences,
),
).toStrictEqual([modifiedInputWithoutFences, false]);
@ -341,7 +363,11 @@ describe('build/transforms/remove-fenced-code', () => {
inputs.forEach((input) => {
expect(() =>
removeFencedCode(mockFileName, BuildType.flask, input),
removeFencedCode(
mockFileName,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
input,
),
).toThrow(
`Empty fence found in file "${mockFileName}":\n${emptyFence}`,
);
@ -368,7 +394,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode().replace(
fenceSentinelAndTerminusRegex,
replacement,
@ -382,53 +408,53 @@ describe('build/transforms/remove-fenced-code', () => {
it('rejects malformed BEGIN directives', () => {
// This is the first line of the minimal input template
const directiveString = '///: BEGIN:ONLY_INCLUDE_IN(flask)';
const directiveString = '///: BEGIN:ONLY_INCLUDE_IN(build-flask)';
const replacements = [
// Invalid terminus
'///: BE_GIN:ONLY_INCLUDE_IN(flask)',
'///: BE6IN:ONLY_INCLUDE_IN(flask)',
'///: BEGIN7:ONLY_INCLUDE_IN(flask)',
'///: BeGIN:ONLY_INCLUDE_IN(flask)',
'///: BE3:ONLY_INCLUDE_IN(flask)',
'///: BEG-IN:ONLY_INCLUDE_IN(flask)',
'///: BEG N:ONLY_INCLUDE_IN(flask)',
'///: BE_GIN:BEGIN:ONLY_INCLUDE_IN(build-flask)',
'///: BE6IN:BEGIN:ONLY_INCLUDE_IN(build-flask)',
'///: BEGIN7:BEGIN:ONLY_INCLUDE_IN(build-flask)',
'///: BeGIN:ONLY_INCLUDE_IN(build-flask)',
'///: BE3:BEGIN:ONLY_INCLUDE_IN(build-flask)',
'///: BEG-IN:BEGIN:ONLY_INCLUDE_IN(build-flask)',
'///: BEG N:BEGIN:ONLY_INCLUDE_IN(build-flask)',
// Invalid commands
'///: BEGIN:ONLY-INCLUDE_IN(flask)',
'///: BEGIN:ONLY_INCLUDE:IN(flask)',
'///: BEGIN:ONL6_INCLUDE_IN(flask)',
'///: BEGIN:ONLY_IN@LUDE_IN(flask)',
'///: BEGIN:ONLy_INCLUDE_IN(flask)',
'///: BEGIN:ONLy_INCLUDE_IN(build-flask)',
'///: BEGIN:ONLY INCLUDE_IN(flask)',
// Invalid parameters
'///: BEGIN:ONLY_INCLUDE_IN(,flask)',
'///: BEGIN:ONLY_INCLUDE_IN(flask,)',
'///: BEGIN:ONLY_INCLUDE_IN(flask,,main)',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask,)',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask,,main)',
'///: BEGIN:ONLY_INCLUDE_IN(,)',
'///: BEGIN:ONLY_INCLUDE_IN()',
'///: BEGIN:ONLY_INCLUDE_IN( )',
'///: BEGIN:ONLY_INCLUDE_IN(flask]',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask]',
'///: BEGIN:ONLY_INCLUDE_IN[flask)',
'///: BEGIN:ONLY_INCLUDE_IN(flask.main)',
'///: BEGIN:ONLY_INCLUDE_IN(flask,@)',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask.main)',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask,@)',
'///: BEGIN:ONLY_INCLUDE_IN(fla k)',
// Stuff after the directive
'///: BEGIN:ONLY_INCLUDE_IN(flask) A',
'///: BEGIN:ONLY_INCLUDE_IN(flask) 9',
'///: BEGIN:ONLY_INCLUDE_IN(flask)A',
'///: BEGIN:ONLY_INCLUDE_IN(flask)9',
'///: BEGIN:ONLY_INCLUDE_IN(flask)_',
'///: BEGIN:ONLY_INCLUDE_IN(flask))',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask) A',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask) 9',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask)A',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask)9',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask)_',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask))',
];
replacements.forEach((replacement) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode().replace(directiveString, replacement),
),
).toThrow(
@ -474,7 +500,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode().replace(directiveString, replacement),
),
).toThrow(
@ -488,14 +514,14 @@ describe('build/transforms/remove-fenced-code', () => {
it('rejects files with uneven number of fence lines', () => {
const additions = [
'///: BEGIN:ONLY_INCLUDE_IN(flask)',
'///: BEGIN:ONLY_INCLUDE_IN(build-flask)',
'///: END:ONLY_INCLUDE_IN',
];
additions.forEach((addition) => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode().concat(addition),
),
).toThrow(
@ -515,7 +541,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode().replace(validTerminus, replacement),
),
).toThrow(
@ -539,7 +565,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode().replace(validCommand, replacement),
),
).toThrow(
@ -557,10 +583,30 @@ describe('build/transforms/remove-fenced-code', () => {
it('rejects invalid command parameters', () => {
const testCases = [
['bar', ['bar', 'flask,bar', 'flask,beta,main,bar']],
['Foo', ['Foo', 'flask,Foo', 'flask,beta,main,Foo']],
['b3ta', ['b3ta', 'flask,b3ta', 'flask,beta,main,b3ta']],
['bEta', ['bEta', 'flask,bEta', 'flask,beta,main,bEta']],
[
'bar',
['bar', 'build-flask,bar', 'build-flask,build-beta,build-main,bar'],
],
[
'Foo',
['Foo', 'build-flask,Foo', 'build-flask,build-beta,build-main,Foo'],
],
[
'b3ta',
[
'b3ta',
'build-flask,b3ta',
'build-flask,build-beta,build-main,b3ta',
],
],
[
'bEta',
[
'bEta',
'build-flask,bEta',
'build-flask,build-beta,build-main,bEta',
],
],
];
testCases.forEach(([invalidParam, replacements]) => {
@ -568,11 +614,17 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({
active: [FLASK_BUILD],
all: [FLASK_BUILD, MAIN_BUILD, 'build-beta'],
}),
getMinimalFencedCode(replacement),
),
).toThrow(
new RegExp(`"${invalidParam}" is not a valid build type.$`, 'u'),
new RegExp(
`"${invalidParam}" is not a declared build feature.$`,
'u',
),
);
});
});
@ -581,7 +633,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
getMinimalFencedCode('').replace('()', ''),
),
).toThrow(/No params specified.$/u);
@ -589,7 +641,9 @@ describe('build/transforms/remove-fenced-code', () => {
it('rejects directive pairs with wrong terminus order', () => {
// We need more than one directive pair for this test
const input = getMinimalFencedCode().concat(getMinimalFencedCode('beta'));
const input = getMinimalFencedCode().concat(
getMinimalFencedCode('build-beta'),
);
const expectedBeginError =
'The first directive of a pair must be a "BEGIN" directive.';
@ -597,17 +651,17 @@ describe('build/transforms/remove-fenced-code', () => {
'The second directive of a pair must be an "END" directive.';
const testCases = [
[
'BEGIN:ONLY_INCLUDE_IN(flask)',
'BEGIN:ONLY_INCLUDE_IN(build-flask)',
'END:ONLY_INCLUDE_IN',
expectedBeginError,
],
[
/END:ONLY_INCLUDE_IN/mu,
'BEGIN:ONLY_INCLUDE_IN(main)',
'BEGIN:ONLY_INCLUDE_IN(build-main)',
expectedEndError,
],
[
'BEGIN:ONLY_INCLUDE_IN(beta)',
'BEGIN:ONLY_INCLUDE_IN(build-beta)',
'END:ONLY_INCLUDE_IN',
expectedBeginError,
],
@ -617,7 +671,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildType.flask,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
input.replace(target, replacement),
),
).toThrow(expectedError);
@ -631,7 +685,11 @@ describe('build/transforms/remove-fenced-code', () => {
'\n//# sourceMappingURL=as32e32wcwc2234f2ew32cnin4243f4nv9nsdoivnxzoivnd',
);
expect(
removeFencedCode(mockFileName, BuildType.flask, input),
removeFencedCode(
mockFileName,
getFeatures({ active: [FLASK_BUILD], all: [FLASK_BUILD] }),
input,
),
).toStrictEqual([input, false]);
});
@ -644,14 +702,14 @@ function getTestData() {
const data = {
validInputs: {
withFences: `
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
Conditionally_Included
@ -660,7 +718,7 @@ Conditionally_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
@ -678,7 +736,7 @@ Conditionally_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
@ -690,14 +748,14 @@ Conditionally_Included
`,
extraContentWithFences: `
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
Conditionally_Included
@ -706,7 +764,7 @@ Conditionally_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
@ -723,7 +781,7 @@ Conditionally_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
@ -781,14 +839,14 @@ Always_Included
validOutputs: {
beta: [
`
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
Conditionally_Included
@ -810,14 +868,14 @@ Always_Included
],
flask: [
`
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
Conditionally_Included
@ -826,7 +884,7 @@ Conditionally_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
@ -836,16 +894,25 @@ Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(desktop)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(desktop)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
`,
true,
false,
],
mmi: [
`
@ -872,14 +939,14 @@ Always_Included
validOutputsWithExtraContent: {
beta: [
`
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
Conditionally_Included
@ -904,14 +971,14 @@ Always_Included
],
flask: [
`
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask,beta,desktop)
///: BEGIN:ONLY_INCLUDE_IN(build-flask,build-beta,desktop)
Conditionally_Included
Conditionally_Included
@ -920,7 +987,7 @@ Conditionally_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
@ -930,10 +997,19 @@ Always_Included
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(desktop)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
Always_Included
Always_Included
Always_Included
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(build-flask)
Conditionally_Included
Conditionally_Included
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(desktop)
Conditionally_Included
Conditionally_Included
@ -942,7 +1018,7 @@ Always_Included
Always_Included
Always_Included
`,
true,
false,
],
mmi: [
`

View File

@ -1,6 +1,6 @@
const path = require('path');
const semver = require('semver');
const { BuildType } = require('../lib/build-type');
const { loadBuildTypesConfig } = require('../lib/build-type');
const { BUILD_TARGETS, ENVIRONMENT } = require('./constants');
/**
@ -48,6 +48,8 @@ function getBrowserVersionMap(platforms, version) {
let buildType, buildVersionSummary, buildVersion;
if (prerelease) {
[buildType, buildVersionSummary] = prerelease;
// TODO(ritave): Figure out why the version 10.25.0-beta.1-flask.0 in the below comment is even possible
// since those are two different build types
// The version could be version: '10.25.0-beta.1-flask.0',
// That results in buildVersionSummary becomes 1-flask
// And we only want 1 from this string
@ -57,14 +59,7 @@ function getBrowserVersionMap(platforms, version) {
: buildVersionSummary;
if (!String(buildVersion).match(/^\d+$/u)) {
throw new Error(`Invalid prerelease build version: '${buildVersion}'`);
} else if (
![
BuildType.beta,
BuildType.flask,
BuildType.desktop,
BuildType.mmi,
].includes(buildType)
) {
} else if (!loadBuildTypesConfig().buildTypes[buildType].isPrerelease) {
throw new Error(`Invalid prerelease build type: ${buildType}`);
}
}

View File

@ -1,44 +0,0 @@
const {
EXCLUDE_E2E_TESTS_REGEX,
filterDiffAdditions,
filterDiffByFilePath,
hasNumberOfCodeBlocksIncreased,
} = require('./shared');
function checkMochaSyntax(diff) {
const ruleHeading = 'favor-jest-instead-of-mocha';
const codeBlocks = [
"import { strict as assert } from 'assert';",
'assert.deepEqual',
'assert.equal',
'assert.rejects',
'assert.strictEqual',
'sinon.',
];
console.log(`\nChecking ${ruleHeading}...`);
const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX);
const diffAdditions = filterDiffAdditions(diffByFilePath);
const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks);
Object.keys(hashmap).forEach((key) => {
if (hashmap[key]) {
console.error(`Number of occurences of "${key}" have increased.`);
}
});
if (Object.values(hashmap).includes(true)) {
console.error(
`...changes have not been accepted by the fitness function.\nFor more info, see: https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#${ruleHeading}`,
);
process.exit(1);
} else {
console.log(
`...number of occurences has not increased for any code block.`,
);
process.exit(0);
}
}
module.exports = { checkMochaSyntax };

View File

@ -0,0 +1,105 @@
import { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX } from './constants';
describe('Regular Expressions used in Fitness Functions', (): void => {
describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, (): void => {
const PATHS_IT_SHOULD_MATCH = [
'file.js',
'path/file.js',
'much/longer/path/file.js',
'file.ts',
'path/file.ts',
'much/longer/path/file.ts',
'file.jsx',
'path/file.jsx',
'much/longer/path/file.jsx',
];
const PATHS_IT_SHOULD_NOT_MATCH = [
// any without JS, TS, JSX or TSX extension
'file',
'file.extension',
'path/file.extension',
'much/longer/path/file.extension',
// any in the test/e2e directory
'test/e2e/file',
'test/e2e/file.extension',
'test/e2e/path/file.extension',
'test/e2e/much/longer/path/file.extension',
'test/e2e/file.js',
'test/e2e/path/file.ts',
'test/e2e/much/longer/path/file.jsx',
'test/e2e/much/longer/path/file.tsx',
// any in the development/fitness-functions directory
'development/fitness-functions/file',
'development/fitness-functions/file.extension',
'development/fitness-functions/path/file.extension',
'development/fitness-functions/much/longer/path/file.extension',
'development/fitness-functions/file.js',
'development/fitness-functions/path/file.ts',
'development/fitness-functions/much/longer/path/file.jsx',
'development/fitness-functions/much/longer/path/file.tsx',
];
describe('included paths', (): void => {
PATHS_IT_SHOULD_MATCH.forEach((path: string): void => {
it(`should match "${path}"`, (): void => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(true);
});
});
});
describe('excluded paths', (): void => {
PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => {
it(`should not match "${path}"`, (): void => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(false);
});
});
});
});
describe(`SHARED_FOLDER_JS_REGEX "${SHARED_FOLDER_JS_REGEX}"`, (): void => {
const PATHS_IT_SHOULD_MATCH = [
'shared/file.js',
'shared/path/file.js',
'shared/much/longer/path/file.js',
'shared/file.jsx',
'shared/path/file.jsx',
'shared/much/longer/path/file.jsx',
];
const PATHS_IT_SHOULD_NOT_MATCH = [
// any without JS or JSX extension
'file',
'file.extension',
'path/file.extension',
'much/longer/path/file.extension',
'file.ts',
'path/file.ts',
'much/longer/path/file.tsx',
];
describe('included paths', (): void => {
PATHS_IT_SHOULD_MATCH.forEach((path: string): void => {
it(`should match "${path}"`, (): void => {
const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path);
expect(result).toStrictEqual(true);
});
});
});
describe('excluded paths', (): void => {
PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => {
it(`should not match "${path}"`, (): void => {
const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path);
expect(result).toStrictEqual(false);
});
});
});
});
});

View File

@ -0,0 +1,15 @@
// include JS, TS, JSX, TSX files only excluding files in the e2e tests and
// fitness functions directories
const EXCLUDE_E2E_TESTS_REGEX =
'^(?!test/e2e)(?!development/fitness).*.(js|ts|jsx|tsx)$';
// include JS and JSX files in the shared directory only
const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$';
enum AUTOMATION_TYPE {
CI = 'ci',
PRE_COMMIT_HOOK = 'pre-commit-hook',
PRE_PUSH_HOOK = 'pre-push-hook',
}
export { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX, AUTOMATION_TYPE };

View File

@ -0,0 +1,54 @@
import { execSync } from 'child_process';
import fs from 'fs';
import { AUTOMATION_TYPE } from './constants';
function getDiffByAutomationType(
automationType: AUTOMATION_TYPE,
): string | undefined {
if (!Object.values(AUTOMATION_TYPE).includes(automationType)) {
console.error('Invalid automation type.');
process.exit(1);
}
let diff;
if (automationType === AUTOMATION_TYPE.CI) {
const optionalArguments = process.argv.slice(3);
if (optionalArguments.length !== 1) {
console.error('Invalid number of arguments.');
process.exit(1);
}
diff = getCIDiff(optionalArguments[0]);
} else if (automationType === AUTOMATION_TYPE.PRE_COMMIT_HOOK) {
diff = getPreCommitHookDiff();
} else if (automationType === AUTOMATION_TYPE.PRE_PUSH_HOOK) {
diff = getPrePushHookDiff();
}
return diff;
}
function getCIDiff(path: string): string {
return fs.readFileSync(path, {
encoding: 'utf8',
flag: 'r',
});
}
function getPreCommitHookDiff(): string {
return execSync(`git diff --cached HEAD`).toString().trim();
}
function getPrePushHookDiff(): string {
const currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`)
.toString()
.trim();
return execSync(
`git diff ${currentBranch} origin/${currentBranch} -- . ':(exclude)development/fitness-functions/'`,
)
.toString()
.trim();
}
export { getDiffByAutomationType };

View File

@ -1,46 +1,48 @@
const {
EXCLUDE_E2E_TESTS_REGEX,
filterDiffAdditions,
import {
filterDiffLineAdditions,
hasNumberOfCodeBlocksIncreased,
filterDiffByFilePath,
} = require('./shared');
filterDiffFileCreations,
} from './shared';
import { generateCreateFileDiff, generateModifyFilesDiff } from './test-data';
const generateCreateFileDiff = (filePath, content) => `
diff --git a/${filePath} b/${filePath}
new file mode 100644
index 000000000..30d74d258
--- /dev/null
+++ b/${filePath}
@@ -0,0 +1 @@
+${content}
`;
const generateModifyFilesDiff = (filePath, addition, removal) => `
diff --git a/${filePath} b/${filePath}
index 57d5de75c..808d8ba37 100644
--- a/${filePath}
+++ b/${filePath}
@@ -1,3 +1,8 @@
+${addition}
@@ -34,33 +39,4 @@
-${removal}
`;
describe('filterDiffAdditions()', () => {
it('should return code additions in the diff', () => {
describe('filterDiffLineAdditions()', (): void => {
it('should return code additions in the diff', (): void => {
const testFilePath = 'new-file.js';
const testAddition = 'foo';
const testFileDiff = generateCreateFileDiff(testFilePath, testAddition);
const actualResult = filterDiffAdditions(testFileDiff);
const actualResult = filterDiffLineAdditions(testFileDiff);
const expectedResult = `+${testAddition}`;
expect(actualResult).toStrictEqual(expectedResult);
});
});
describe('hasNumberOfCodeBlocksIncreased()', () => {
it('should show which code blocks have increased', () => {
describe('filterDiffFileCreations()', (): void => {
it('should return code additions in the diff', (): void => {
const testFileDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateCreateFileDiff('old-file.js', 'ping'),
generateModifyFilesDiff('old-file.jsx', 'yin', 'yang'),
].join('');
const actualResult = filterDiffFileCreations(testFileDiff);
expect(actualResult).toMatchInlineSnapshot(`
"diff --git a/old-file.js b/old-file.js
new file mode 100644
index 000000000..30d74d258
--- /dev/null
+++ b/old-file.js
@@ -0,0 +1 @@
+ping"
`);
});
});
describe('hasNumberOfCodeBlocksIncreased()', (): void => {
it('should show which code blocks have increased', (): void => {
const testDiffFragment = `
+foo
+bar
@ -57,14 +59,14 @@ describe('hasNumberOfCodeBlocksIncreased()', () => {
});
});
describe('filterDiffByFilePath()', () => {
describe('filterDiffByFilePath()', (): void => {
const testFileDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', 'ping', 'pong'),
generateModifyFilesDiff('old-file.jsx', 'yin', 'yang'),
].join('');
it('should return the right diff for a generic matcher', () => {
it('should return the right diff for a generic matcher', (): void => {
const actualResult = filterDiffByFilePath(
testFileDiff,
'.*/.*.(js|ts)$|.*.(js|ts)$',
@ -90,7 +92,7 @@ describe('filterDiffByFilePath()', () => {
`);
});
it('should return the right diff for a specific file in any dir matcher', () => {
it('should return the right diff for a specific file in any dir matcher', (): void => {
const actualResult = filterDiffByFilePath(testFileDiff, '.*old-file.js$');
expect(actualResult).toMatchInlineSnapshot(`
@ -105,7 +107,7 @@ describe('filterDiffByFilePath()', () => {
`);
});
it('should return the right diff for a multiple file extension (OR) matcher', () => {
it('should return the right diff for a multiple file extension (OR) matcher', (): void => {
const actualResult = filterDiffByFilePath(
testFileDiff,
'^(./)*old-file.(js|ts|jsx)$',
@ -131,7 +133,7 @@ describe('filterDiffByFilePath()', () => {
`);
});
it('should return the right diff for a file name negation matcher', () => {
it('should return the right diff for a file name negation matcher', (): void => {
const actualResult = filterDiffByFilePath(
testFileDiff,
'^(?!.*old-file.js$).*.[a-zA-Z]+$',
@ -157,51 +159,3 @@ describe('filterDiffByFilePath()', () => {
`);
});
});
describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, () => {
const PATHS_IT_SHOULD_MATCH = [
'file.js',
'path/file.js',
'much/longer/path/file.js',
'file.ts',
'path/file.ts',
'much/longer/path/file.ts',
'file.jsx',
'path/file.jsx',
'much/longer/path/file.jsx',
];
const PATHS_IT_SHOULD_NOT_MATCH = [
'test/e2e/file',
'test/e2e/file.extension',
'test/e2e/path/file.extension',
'test/e2e/much/longer/path/file.extension',
'test/e2e/file.js',
'test/e2e/path/file.ts',
'test/e2e/much/longer/path/file.jsx',
'file',
'file.extension',
'path/file.extension',
'much/longer/path/file.extension',
];
describe('included paths', () => {
PATHS_IT_SHOULD_MATCH.forEach((path) => {
it(`should match "${path}"`, () => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(true);
});
});
});
describe('excluded paths', () => {
PATHS_IT_SHOULD_NOT_MATCH.forEach((path) => {
it(`should not match "${path}"`, () => {
const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path);
expect(result).toStrictEqual(false);
});
});
});
});

View File

@ -1,6 +1,4 @@
const EXCLUDE_E2E_TESTS_REGEX = '^(?!test/e2e/).*.(js|ts|jsx)$';
function filterDiffByFilePath(diff, regex) {
function filterDiffByFilePath(diff: string, regex: string): string {
// split by `diff --git` and remove the first element which is empty
const diffBlocks = diff.split(`diff --git`).slice(1);
@ -34,7 +32,7 @@ function filterDiffByFilePath(diff, regex) {
return filteredDiff;
}
function filterDiffAdditions(diff) {
function filterDiffLineAdditions(diff: string): string {
const diffLines = diff.split('\n');
const diffAdditionLines = diffLines.filter((line) => {
@ -46,10 +44,36 @@ function filterDiffAdditions(diff) {
return diffAdditionLines.join('/n');
}
function hasNumberOfCodeBlocksIncreased(diffFragment, codeBlocks) {
function filterDiffFileCreations(diff: string): string {
// split by `diff --git` and remove the first element which is empty
const diffBlocks = diff.split(`diff --git`).slice(1);
const filteredDiff = diffBlocks
.map((block) => block.trim())
.filter((block) => {
const isFileCreationLine =
block
// get the second line of the block which has the file mode
.split('\n')[1]
.trim()
.substring(0, 13) === 'new file mode';
return isFileCreationLine;
})
// prepend `git --diff` to each block
.map((block) => `diff --git ${block}`)
.join('\n');
return filteredDiff;
}
function hasNumberOfCodeBlocksIncreased(
diffFragment: string,
codeBlocks: string[],
): { [codeBlock: string]: boolean } {
const diffLines = diffFragment.split('\n');
const codeBlockFound = {};
const codeBlockFound: { [codeBlock: string]: boolean } = {};
for (const codeBlock of codeBlocks) {
codeBlockFound[codeBlock] = false;
@ -65,9 +89,9 @@ function hasNumberOfCodeBlocksIncreased(diffFragment, codeBlocks) {
return codeBlockFound;
}
module.exports = {
EXCLUDE_E2E_TESTS_REGEX,
export {
filterDiffByFilePath,
filterDiffAdditions,
filterDiffLineAdditions,
filterDiffFileCreations,
hasNumberOfCodeBlocksIncreased,
};

View File

@ -0,0 +1,40 @@
const generateCreateFileDiff = (
filePath = 'file.txt',
content = 'Lorem ipsum',
): string => `
diff --git a/${filePath} b/${filePath}
new file mode 100644
index 000000000..30d74d258
--- /dev/null
+++ b/${filePath}
@@ -0,0 +1 @@
+${content}
`;
const generateModifyFilesDiff = (
filePath = 'file.txt',
addition = 'Lorem ipsum',
removal = '',
): string => {
const additionBlock = addition
? `
@@ -1,3 +1,8 @@
+${addition}`.trim()
: '';
const removalBlock = removal
? `
@@ -34,33 +39,4 @@
-${removal}`.trim()
: '';
return `
diff --git a/${filePath} b/${filePath}
index 57d5de75c..808d8ba37 100644
--- a/${filePath}
+++ b/${filePath}
${additionBlock}
${removalBlock}`;
};
export { generateCreateFileDiff, generateModifyFilesDiff };

View File

@ -1,53 +0,0 @@
const fs = require('fs');
const { execSync } = require('child_process');
const { checkMochaSyntax } = require('./check-mocha-syntax');
const AUTOMATION_TYPE = Object.freeze({
CI: 'ci',
PRE_COMMIT_HOOK: 'pre-commit-hook',
PRE_PUSH_HOOK: 'pre-push-hook',
});
const automationType = process.argv[2];
if (automationType === AUTOMATION_TYPE.CI) {
const optionalArguments = process.argv.slice(3);
if (optionalArguments.length !== 1) {
console.error('Invalid number of arguments.');
process.exit(1);
}
const diff = fs.readFileSync(optionalArguments[0], {
encoding: 'utf8',
flag: 'r',
});
checkMochaSyntax(diff);
} else if (automationType === AUTOMATION_TYPE.PRE_COMMIT_HOOK) {
const diff = getPreCommitHookDiff();
checkMochaSyntax(diff);
} else if (automationType === AUTOMATION_TYPE.PRE_PUSH_HOOK) {
const diff = getPrePushHookDiff();
checkMochaSyntax(diff);
} else {
console.error('Invalid automation type.');
process.exit(1);
}
function getPreCommitHookDiff() {
return execSync(`git diff --cached HEAD`).toString().trim();
}
function getPrePushHookDiff() {
const currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`)
.toString()
.trim();
return execSync(
`git diff ${currentBranch} origin/${currentBranch} -- . ':(exclude)development/fitness-functions/'`,
)
.toString()
.trim();
}

View File

@ -0,0 +1,11 @@
import { AUTOMATION_TYPE } from './common/constants';
import { getDiffByAutomationType } from './common/get-diff';
import { IRule, RULES, runFitnessFunctionRule } from './rules';
const automationType: AUTOMATION_TYPE = process.argv[2] as AUTOMATION_TYPE;
const diff = getDiffByAutomationType(automationType);
if (typeof diff === 'string') {
RULES.forEach((rule: IRule): void => runFitnessFunctionRule(rule, diff));
}

View File

@ -0,0 +1,42 @@
import { preventSinonAssertSyntax } from './sinon-assert-syntax';
import { preventJavaScriptFileAdditions } from './javascript-additions';
const RULES: IRule[] = [
{
name: "Don't use `sinon` or `assert` in unit tests",
fn: preventSinonAssertSyntax,
docURL:
'https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha',
},
{
name: "Don't add JS or JSX files to the `shared` directory",
fn: preventJavaScriptFileAdditions,
},
];
interface IRule {
name: string;
fn: (diff: string) => boolean;
docURL?: string;
}
function runFitnessFunctionRule(rule: IRule, diff: string): void {
const { name, fn, docURL } = rule;
console.log(`Checking rule "${name}"...`);
const hasRulePassed: boolean = fn(diff) as boolean;
if (hasRulePassed === true) {
console.log(`...OK`);
} else {
console.log(`...FAILED. Changes not accepted by the fitness function.`);
if (docURL) {
console.log(`For more info: ${docURL}.`);
}
process.exit(1);
}
}
export { RULES, runFitnessFunctionRule };
export type { IRule };

View File

@ -0,0 +1,51 @@
import {
generateModifyFilesDiff,
generateCreateFileDiff,
} from '../common/test-data';
import { preventJavaScriptFileAdditions } from './javascript-additions';
describe('preventJavaScriptFileAdditions()', (): void => {
it('should pass when receiving an empty diff', (): void => {
const testDiff = '';
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(true);
});
it('should pass when receiving a diff with a new TS file on the shared folder', (): void => {
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateCreateFileDiff('shared/test.ts', 'yada yada yada yada'),
].join('');
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(true);
});
it('should not pass when receiving a diff with a new JS file on the shared folder', (): void => {
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateCreateFileDiff('shared/test.js', 'yada yada yada yada'),
].join('');
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(false);
});
it('should not pass when receiving a diff with a new JSX file on the shared folder', (): void => {
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateCreateFileDiff('shared/test.jsx', 'yada yada yada yada'),
].join('');
const hasRulePassed = preventJavaScriptFileAdditions(testDiff);
expect(hasRulePassed).toBe(false);
});
});

View File

@ -0,0 +1,18 @@
import { SHARED_FOLDER_JS_REGEX } from '../common/constants';
import {
filterDiffByFilePath,
filterDiffFileCreations,
} from '../common/shared';
function preventJavaScriptFileAdditions(diff: string): boolean {
const sharedFolderDiff = filterDiffByFilePath(diff, SHARED_FOLDER_JS_REGEX);
const sharedFolderCreationDiff = filterDiffFileCreations(sharedFolderDiff);
const hasCreatedAtLeastOneJSFileInShared = sharedFolderCreationDiff !== '';
if (hasCreatedAtLeastOneJSFileInShared) {
return false;
}
return true;
}
export { preventJavaScriptFileAdditions };

View File

@ -0,0 +1,29 @@
import { generateModifyFilesDiff } from '../common/test-data';
import { preventSinonAssertSyntax } from './sinon-assert-syntax';
describe('preventSinonAssertSyntax()', (): void => {
it('should pass when receiving an empty diff', (): void => {
const testDiff = '';
const hasRulePassed = preventSinonAssertSyntax(testDiff);
expect(hasRulePassed).toBe(true);
});
it('should not pass when receiving a diff with one of the blocked expressions', (): void => {
const infringingExpression = 'assert.equal';
const testDiff = [
generateModifyFilesDiff('new-file.ts', 'foo', 'bar'),
generateModifyFilesDiff('old-file.js', undefined, 'pong'),
generateModifyFilesDiff(
'test.js',
`yada yada ${infringingExpression} yada yada`,
undefined,
),
].join('');
const hasRulePassed = preventSinonAssertSyntax(testDiff);
expect(hasRulePassed).toBe(false);
});
});

View File

@ -0,0 +1,30 @@
import { EXCLUDE_E2E_TESTS_REGEX } from '../common/constants';
import {
filterDiffLineAdditions,
filterDiffByFilePath,
hasNumberOfCodeBlocksIncreased,
} from '../common/shared';
const codeBlocks = [
"import { strict as assert } from 'assert';",
'assert.deepEqual',
'assert.equal',
'assert.rejects',
'assert.strictEqual',
'sinon.',
];
function preventSinonAssertSyntax(diff: string): boolean {
const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX);
const diffAdditions = filterDiffLineAdditions(diffByFilePath);
const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks);
const haveOccurencesOfAtLeastOneCodeBlockIncreased =
Object.values(hashmap).includes(true);
if (haveOccurencesOfAtLeastOneCodeBlockIncreased) {
return false;
}
return true;
}
export { preventSinonAssertSyntax };

View File

@ -2,11 +2,13 @@
const concurrently = require('concurrently');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { BuildType } = require('./lib/build-type');
const { loadBuildTypesConfig } = require('./lib/build-type');
const stableBuildTypes = Object.values(BuildType).filter(
const buildTypesConfig = loadBuildTypesConfig();
const stableBuildTypes = Object.keys(buildTypesConfig.buildTypes).filter(
// Skip generating policy for MMI until that build has stabilized
(buildType) => buildType !== BuildType.mmi,
(buildType) => buildType !== 'mmi',
);
start().catch((error) => {
@ -24,7 +26,7 @@ async function start() {
yargsInstance
.option('build-types', {
alias: ['t'],
choices: Object.values(BuildType),
choices: Object.keys(buildTypesConfig.buildTypes),
default: stableBuildTypes,
demandOption: true,
description: 'The build type(s) to generate policy files for.',

6
development/lib/build-type.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { Infer, Struct } from 'superstruct';
export type Unique<Element extends Struct<any>> = (
struct: Struct<Infer<Element>[], Infer<Element>>,
eq?: (a: Infer<Element>, b: Infer<Element>) => boolean,
) => Struct<Infer<Element>[], Infer<Element>>;

View File

@ -1,18 +1,169 @@
const fs = require('fs');
const { AssertionError } = require('assert');
const path = require('path');
const {
object,
string,
record,
optional,
array,
refine,
any,
boolean,
coerce,
union,
unknown,
validate,
nullable,
never,
} = require('superstruct');
const yaml = require('js-yaml');
const { uniqWith } = require('lodash');
const BUILDS_YML_PATH = path.resolve('./builds.yml');
/**
* The distribution this build is intended for.
*
* This should be kept in-sync with the `BuildType` map in `shared/constants/app.js`.
* @type {import('superstruct').Infer<typeof BuildTypesStruct> | null}
*/
const BuildType = {
beta: 'beta',
desktop: 'desktop',
flask: 'flask',
main: 'main',
mmi: 'mmi',
};
let cachedBuildTypes = null;
const BuildTypeInheritance = {
[BuildType.desktop]: [BuildType.flask],
};
/**
* Ensures that the array item contains only elements that are distinct from each other
*
* @template {Struct<any>} Element
* @type {import('./build-type').Unique<Element>}
*/
const unique = (struct, eq) =>
refine(struct, 'unique', (value) => {
if (uniqWith(value, eq).length === value.length) {
return true;
}
return 'Array contains duplicated values';
});
module.exports = { BuildType, BuildTypeInheritance };
const EnvDefinitionStruct = coerce(
object({ key: string(), value: unknown() }),
refine(record(string(), any()), 'Env variable declaration', (value) => {
if (Object.keys(value).length !== 1) {
return 'Declaration should have only one property, the name';
}
return true;
}),
(value) => ({ key: Object.keys(value)[0], value: Object.values(value)[0] }),
);
const EnvArrayStruct = unique(
array(union([string(), EnvDefinitionStruct])),
(a, b) => {
const keyA = typeof a === 'string' ? a : a.key;
const keyB = typeof b === 'string' ? b : b.key;
return keyA === keyB;
},
);
const BuildTypeStruct = object({
features: optional(unique(array(string()))),
env: optional(EnvArrayStruct),
isPrerelease: optional(boolean()),
manifestOverrides: optional(string()),
});
const CopyAssetStruct = object({ src: string(), dest: string() });
const ExclusiveIncludeAssetStruct = coerce(
object({ exclusiveInclude: string() }),
string(),
(exclusiveInclude) => ({ exclusiveInclude }),
);
const AssetStruct = union([CopyAssetStruct, ExclusiveIncludeAssetStruct]);
const FeatureStruct = object({
env: optional(EnvArrayStruct),
// TODO(ritave): Check if the paths exist
assets: optional(array(AssetStruct)),
});
const FeaturesStruct = refine(
record(
string(),
coerce(FeatureStruct, nullable(never()), () => ({})),
),
'feature definitions',
function* (value) {
let isValid = true;
const definitions = new Set();
for (const feature of Object.values(value)) {
for (const env of feature?.env ?? []) {
if (typeof env !== 'string') {
if (definitions.has(env.key)) {
isValid = false;
yield `Multiple defined features have a definition of "${env}" env variable, resulting in a conflict`;
}
definitions.add(env.key);
}
}
}
return isValid;
},
);
const BuildTypesStruct = refine(
object({
default: string(),
buildTypes: record(string(), BuildTypeStruct),
features: FeaturesStruct,
env: EnvArrayStruct,
}),
'BuildTypes',
(value) => {
if (!Object.keys(value.buildTypes).includes(value.default)) {
return `Default build type "${value.default}" does not exist in builds declarations`;
}
return true;
},
);
/**
* Loads definitions of build type and what they are composed of.
*
* @returns {import('superstruct').Infer<typeof BuildTypesStruct>}
*/
function loadBuildTypesConfig() {
if (cachedBuildTypes !== null) {
return cachedBuildTypes;
}
const buildsData = yaml.load(fs.readFileSync(BUILDS_YML_PATH, 'utf8'), {
json: true,
});
const [err, result] = validate(buildsData, BuildTypesStruct, {
coerce: true,
});
if (err !== undefined) {
throw new AssertionError({
message: constructFailureMessage(err),
});
}
cachedBuildTypes = result;
return buildsData;
}
/**
* Creates a user readable error message about parse failure.
*
* @param {import('superstruct').StructError} structError
* @returns {string}
*/
function constructFailureMessage(structError) {
return `Failed to parse builds.yml
-> ${structError
.failures()
.map(
(failure) =>
`${failure.message} (${BUILDS_YML_PATH}:.${failure.path.join('/')})`,
)
.join('\n -> ')}
`;
}
module.exports = { loadBuildTypesConfig };

View File

@ -1,5 +1,5 @@
const { version: manifestVersion } = require('../../package.json');
const { BuildType } = require('./build-type');
const { loadBuildTypesConfig } = require('./build-type');
/**
* Get the current version of the MetaMask extension. The base manifest version
@ -8,14 +8,14 @@ const { BuildType } = require('./build-type');
* The build version is needed because certain build types (such as beta) may
* be released multiple times during the release process.
*
* @param {BuildType} buildType - The build type.
* @param {string} buildType - The build type.
* @param {number} buildVersion - The build version.
* @returns {string} The MetaMask extension version.
*/
function getVersion(buildType, buildVersion) {
return buildType === BuildType.main || buildType === BuildType.beta
? manifestVersion
: `${manifestVersion}-${buildType}.${buildVersion}`;
return loadBuildTypesConfig().buildTypes[buildType].isPrerelease === true
? `${manifestVersion}-${buildType}.${buildVersion}`
: manifestVersion;
}
module.exports = { getVersion };

View File

@ -0,0 +1,114 @@
/* eslint-disable jsdoc/check-tag-names */
const assert = require('assert');
const DeclaredOnly = Symbol(
'This variable was declared only without being defined',
);
class Variables {
/**
* @type {Map<string, unknown | typeof DeclaredOnly>}
*/
#definitions = new Map();
/**
* @param {Iterable<string>} declarations
*/
constructor(declarations) {
for (const declaration of declarations) {
this.#definitions.set(declaration, DeclaredOnly);
}
}
/**
* @param {string} key - The name of the variable
* @throws {TypeError} If there is no definition of a variable.
*/
get(key) {
const value = this.getMaybe(key);
assert(
value !== DeclaredOnly,
new TypeError(
`Tried to access a declared, but not defined environmental variable "${key}"`,
),
);
return value;
}
/**
* Returns a declared, but maybe not defined variable.
*
* @param {string} key - The name of the variable
* @throws {TypeError} If there was no declaration of the variable.
* @returns The value, or undefined if the variables wasn't defined.
*/
getMaybe(key) {
assert(
this.isDeclared(key),
new TypeError(
`Tried to access an environmental variable "${key}" that wasn't declared in builds.yml`,
),
);
return this.#definitions.get(key);
}
/**
* Sets one key
*
* @overload
* @param {string} key
* @param {unknown} value
* @returns {void}
*/
/**
* @overload
* @param {Record<string, unknown>} records - Key-Value object
* @returns {void}
*/
/**
* @param {string | Record<string, unknown>} keyOrRecord
* @param {unknown} value
* @returns {void}
*/
set(keyOrRecord, value) {
if (typeof keyOrRecord === 'object') {
for (const [key, recordValue] of Object.entries(keyOrRecord)) {
this.set(key, recordValue);
}
return;
}
const key = keyOrRecord;
assert(
this.isDeclared(key),
`Tried to modify a variable "${key}" that wasn't declared in builds.yml`,
);
assert(value !== DeclaredOnly, `Tried to un-define "${key}" variable`);
this.#definitions.set(key, value);
}
isDeclared(key) {
return this.#definitions.has(key);
}
isDefined(key) {
return (
this.#definitions.has(key) && this.#definitions.get(key) !== DeclaredOnly
);
}
[Symbol.iterator] = this.declarations;
*declarations() {
yield* this.#definitions.keys();
}
*definitions() {
for (const [key, value] of this.#definitions.entries()) {
if (value !== DeclaredOnly) {
yield [key, value];
}
}
}
}
module.exports = { Variables };

View File

@ -5,7 +5,7 @@ const { hideBin } = require('yargs/helpers');
const { runCommand, runInShell } = require('./lib/run-command');
const { getVersion } = require('./lib/get-version');
const { BuildType } = require('./lib/build-type');
const { loadBuildTypesConfig } = require('./lib/build-type');
start().catch((error) => {
console.error(error);
@ -29,9 +29,9 @@ async function start() {
type: 'string',
})
.option('build-type', {
default: BuildType.main,
default: loadBuildTypesConfig().default,
description: 'The MetaMask extension build type',
choices: Object.values(BuildType),
choices: Object.keys(loadBuildTypesConfig().buildTypes),
})
.option('build-version', {
default: 0,
@ -86,7 +86,7 @@ async function start() {
}
const additionalUploadArgs = [];
if (buildType !== BuildType.main) {
if (buildType !== loadBuildTypesConfig().default) {
additionalUploadArgs.push('--dist-directory', `dist-${buildType}`);
}
// upload sentry source and sourcemaps

View File

@ -79,7 +79,6 @@
"app/scripts/lib/createRPCMethodTrackingMiddleware.test.js",
"app/scripts/lib/createStreamSink.js",
"app/scripts/lib/createTabIdMiddleware.js",
"app/scripts/lib/decrypt-message-manager.js",
"app/scripts/lib/ens-ipfs/contracts/registry.js",
"app/scripts/lib/ens-ipfs/contracts/resolver.js",
"app/scripts/lib/ens-ipfs/resolver.js",

View File

@ -8,7 +8,8 @@ import pify from 'pify';
import endOfStream from 'end-of-stream';
import pump from 'pump';
import gulp from 'gulp';
import gulpDartSass from 'gulp-dart-sass';
import gulpSass from 'gulp-sass';
import sass from 'sass';
import sourcemaps from 'gulp-sourcemaps';
import autoprefixer from 'gulp-autoprefixer';
import fg from 'fast-glob';
@ -94,7 +95,7 @@ async function compileStylesheets(src: string, dest: string): Promise<void> {
await promisifiedPump(
gulp.src(src),
sourcemaps.init(),
gulpDartSass().on('error', (error: unknown) => {
gulpSass(sass)().on('error', (error: unknown) => {
console.error(`Couldn't compile stylesheets: ${error}`);
}),
autoprefixer(),

View File

@ -6,6 +6,7 @@ module.exports = {
'!<rootDir>/app/scripts/controllers/network/**/test/*.ts',
'<rootDir>/app/scripts/controllers/permissions/**/*.js',
'<rootDir>/app/scripts/controllers/sign.ts',
'<rootDir>/app/scripts/controllers/decrypt-message.ts',
'<rootDir>/app/scripts/flask/**/*.js',
'<rootDir>/app/scripts/lib/**/*.js',
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.js',
@ -44,6 +45,7 @@ module.exports = {
'<rootDir>/app/scripts/controllers/network/**/*.test.ts',
'<rootDir>/app/scripts/controllers/permissions/**/*.test.js',
'<rootDir>/app/scripts/controllers/sign.test.ts',
'<rootDir>/app/scripts/controllers/decrypt-message.test.ts',
'<rootDir>/app/scripts/flask/**/*.test.js',
'<rootDir>/app/scripts/lib/**/*.test.js',
'<rootDir>/app/scripts/lib/**/*.test.ts',

View File

@ -716,7 +716,7 @@
"@metamask/assets-controllers>@metamask/abi-utils": {
"packages": {
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": {
@ -725,10 +725,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/assets-controllers>abort-controller": {
@ -750,6 +750,7 @@
},
"@metamask/controller-utils": {
"globals": {
"URL": true,
"console.error": true,
"fetch": true,
"setTimeout": true
@ -820,10 +821,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": {
@ -868,10 +869,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-provider": {
@ -1191,10 +1192,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/key-tree>@noble/ed25519": {
@ -1316,7 +1317,7 @@
"@metamask/key-tree": true,
"@metamask/key-tree>@noble/hashes": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/rpc-methods>@metamask/browser-passworder": {
@ -1442,10 +1443,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": {
@ -2658,10 +2659,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"eth-ens-namehash": {
@ -2779,7 +2780,6 @@
"@ethersproject/abi": true,
"bn.js": true,
"browserify>buffer": true,
"browserify>process": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true,
"eth-lattice-keyring>gridplus-sdk>bech32": true,

View File

@ -716,7 +716,7 @@
"@metamask/assets-controllers>@metamask/abi-utils": {
"packages": {
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": {
@ -725,10 +725,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/assets-controllers>abort-controller": {
@ -750,6 +750,7 @@
},
"@metamask/controller-utils": {
"globals": {
"URL": true,
"console.error": true,
"fetch": true,
"setTimeout": true
@ -826,7 +827,6 @@
"@metamask/desktop>otpauth": true,
"browserify>buffer": true,
"browserify>events": true,
"browserify>process": true,
"browserify>stream-browserify": true,
"end-of-stream": true,
"extension-port-stream": true,
@ -892,10 +892,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": {
@ -940,10 +940,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-provider": {
@ -1263,10 +1263,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/key-tree>@noble/ed25519": {
@ -1413,10 +1413,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/post-message-stream>readable-stream": {
@ -1478,8 +1478,8 @@
"@metamask/snaps-ui": true,
"@metamask/snaps-utils": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true,
"eth-rpc-errors": true
"eth-rpc-errors": true,
"superstruct": true
}
},
"@metamask/rpc-methods>@metamask/browser-passworder": {
@ -1770,7 +1770,7 @@
"@metamask/snaps-ui": {
"packages": {
"@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/snaps-utils": {
@ -1791,15 +1791,15 @@
"@metamask/snaps-utils>rfdc": true,
"@metamask/snaps-utils>validate-npm-package-name": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/key-tree>@noble/secp256k1": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/snaps-utils>cron-parser": {
@ -1835,10 +1835,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": {
@ -3051,10 +3051,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"eth-ens-namehash": {
@ -3172,7 +3172,6 @@
"@ethersproject/abi": true,
"bn.js": true,
"browserify>buffer": true,
"browserify>process": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true,
"eth-lattice-keyring>gridplus-sdk>bech32": true,

View File

@ -716,7 +716,7 @@
"@metamask/assets-controllers>@metamask/abi-utils": {
"packages": {
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": {
@ -725,10 +725,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/assets-controllers>abort-controller": {
@ -750,6 +750,7 @@
},
"@metamask/controller-utils": {
"globals": {
"URL": true,
"console.error": true,
"fetch": true,
"setTimeout": true
@ -826,7 +827,6 @@
"@metamask/desktop>otpauth": true,
"browserify>buffer": true,
"browserify>events": true,
"browserify>process": true,
"browserify>stream-browserify": true,
"end-of-stream": true,
"extension-port-stream": true,
@ -892,10 +892,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": {
@ -940,10 +940,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-provider": {
@ -1263,10 +1263,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/key-tree>@noble/ed25519": {
@ -1413,10 +1413,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/post-message-stream>readable-stream": {
@ -1478,8 +1478,8 @@
"@metamask/snaps-ui": true,
"@metamask/snaps-utils": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true,
"eth-rpc-errors": true
"eth-rpc-errors": true,
"superstruct": true
}
},
"@metamask/rpc-methods>@metamask/browser-passworder": {
@ -1770,7 +1770,7 @@
"@metamask/snaps-ui": {
"packages": {
"@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/snaps-utils": {
@ -1791,15 +1791,15 @@
"@metamask/snaps-utils>rfdc": true,
"@metamask/snaps-utils>validate-npm-package-name": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/key-tree>@noble/secp256k1": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/snaps-utils>cron-parser": {
@ -1835,10 +1835,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": {
@ -3051,10 +3051,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"eth-ens-namehash": {
@ -3172,7 +3172,6 @@
"@ethersproject/abi": true,
"bn.js": true,
"browserify>buffer": true,
"browserify>process": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true,
"eth-lattice-keyring>gridplus-sdk>bech32": true,

View File

@ -716,7 +716,7 @@
"@metamask/assets-controllers>@metamask/abi-utils": {
"packages": {
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": {
@ -725,10 +725,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/assets-controllers>abort-controller": {
@ -750,6 +750,7 @@
},
"@metamask/controller-utils": {
"globals": {
"URL": true,
"console.error": true,
"fetch": true,
"setTimeout": true
@ -820,10 +821,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": {
@ -868,10 +869,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/eth-json-rpc-provider": {
@ -1191,10 +1192,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/key-tree>@noble/ed25519": {
@ -1316,7 +1317,7 @@
"@metamask/key-tree": true,
"@metamask/key-tree>@noble/hashes": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true
"superstruct": true
}
},
"@metamask/rpc-methods>@metamask/browser-passworder": {
@ -1442,10 +1443,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": {
@ -2658,10 +2659,10 @@
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
"semver": true,
"superstruct": true
}
},
"eth-ens-namehash": {
@ -2779,7 +2780,6 @@
"@ethersproject/abi": true,
"bn.js": true,
"browserify>buffer": true,
"browserify>process": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true,
"eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true,
"eth-lattice-keyring>gridplus-sdk>bech32": true,

View File

@ -1272,16 +1272,6 @@
"typescript": true
}
},
"addons-linter>postcss>picocolors": {
"builtin": {
"tty.isatty": true
},
"globals": {
"process.argv.includes": true,
"process.env": true,
"process.platform": true
}
},
"babelify": {
"builtin": {
"path.extname": true,
@ -1926,10 +1916,10 @@
"packages": {
"chokidar>braces": true,
"chokidar>fsevents": true,
"chokidar>glob-parent": true,
"chokidar>is-binary-path": true,
"chokidar>normalize-path": true,
"depcheck>readdirp": true,
"eslint>glob-parent": true,
"eslint>is-glob": true,
"watchify>anymatch": true
}
@ -1959,15 +1949,6 @@
},
"native": true
},
"chokidar>glob-parent": {
"builtin": {
"os.platform": true,
"path.posix.dirname": true
},
"packages": {
"eslint>is-glob": true
}
},
"chokidar>is-binary-path": {
"builtin": {
"path.extname": true
@ -3074,9 +3055,9 @@
"process.cwd": true
},
"packages": {
"eslint>glob-parent": true,
"fast-glob>@nodelib/fs.stat": true,
"fast-glob>@nodelib/fs.walk": true,
"fast-glob>glob-parent": true,
"globby>merge2": true,
"stylelint>micromatch": true
}
@ -3131,15 +3112,6 @@
"fast-glob>@nodelib/fs.walk>fastq>reusify": true
}
},
"fast-glob>glob-parent": {
"builtin": {
"os.platform": true,
"path.posix.dirname": true
},
"packages": {
"eslint>is-glob": true
}
},
"fs-extra": {
"builtin": {
"assert": true,
@ -3287,9 +3259,9 @@
"process.env.AUTOPREFIXER_GRID": true
},
"packages": {
"addons-linter>postcss>picocolors": true,
"gulp-autoprefixer>autoprefixer>fraction.js": true,
"gulp-autoprefixer>postcss": true,
"gulp-sass>picocolors": true,
"stylelint>autoprefixer>normalize-range": true,
"stylelint>postcss-value-parser": true,
"webpack>browserslist": true,
@ -3324,85 +3296,9 @@
"process.env.NODE_ENV": true
},
"packages": {
"addons-linter>postcss>picocolors": true,
"addons-linter>postcss>source-map-js": true,
"gulp-autoprefixer>postcss>nanoid": true
}
},
"gulp-dart-sass": {
"builtin": {
"path.basename": true,
"path.dirname": true,
"path.extname": true,
"path.join": true,
"path.relative": true
},
"globals": {
"process.cwd": true,
"process.stderr.write": true
},
"packages": {
"gulp-dart-sass>chalk": true,
"gulp-dart-sass>lodash.clonedeep": true,
"gulp-dart-sass>strip-ansi": true,
"gulp-dart-sass>through2": true,
"gulp-zip>plugin-error": true,
"sass": true,
"vinyl-sourcemaps-apply": true,
"vinyl>replace-ext": true
}
},
"gulp-dart-sass>chalk": {
"globals": {
"process.env.TERM": true,
"process.platform": true
},
"packages": {
"gulp-dart-sass>chalk>ansi-styles": true,
"gulp-dart-sass>chalk>escape-string-regexp": true,
"gulp-dart-sass>chalk>supports-color": true
}
},
"gulp-dart-sass>chalk>ansi-styles": {
"packages": {
"@metamask/jazzicon>color>color-convert": true
}
},
"gulp-dart-sass>chalk>supports-color": {
"builtin": {
"os.release": true
},
"globals": {
"process.env": true,
"process.platform": true,
"process.stderr": true,
"process.stdout": true,
"process.versions.node.split": true
},
"packages": {
"gulp-dart-sass>chalk>supports-color>has-flag": true
}
},
"gulp-dart-sass>chalk>supports-color>has-flag": {
"globals": {
"process.argv": true
}
},
"gulp-dart-sass>strip-ansi": {
"packages": {
"gulp-dart-sass>strip-ansi>ansi-regex": true
}
},
"gulp-dart-sass>through2": {
"builtin": {
"util.inherits": true
},
"globals": {
"process.nextTick": true
},
"packages": {
"readable-stream": true,
"watchify>xtend": true
"gulp-autoprefixer>postcss>nanoid": true,
"gulp-sass>picocolors": true
}
},
"gulp-livereload": {
@ -3424,8 +3320,8 @@
"process.platform": true
},
"packages": {
"gulp-dart-sass>chalk>escape-string-regexp": true,
"gulp-livereload>chalk>ansi-styles": true,
"gulp-livereload>chalk>escape-string-regexp": true,
"gulp-livereload>chalk>supports-color": true
}
},
@ -3698,7 +3594,7 @@
"process.platform": true
},
"packages": {
"gulp-dart-sass>chalk>escape-string-regexp": true,
"gulp-livereload>chalk>escape-string-regexp": true,
"gulp-rtlcss>rtlcss>chalk>ansi-styles": true,
"gulp-rtlcss>rtlcss>chalk>supports-color": true
}
@ -3757,6 +3653,48 @@
"watchify>xtend": true
}
},
"gulp-sass": {
"builtin": {
"path.basename": true,
"path.dirname": true,
"path.extname": true,
"path.join": true,
"path.relative": true,
"stream.Transform": true
},
"globals": {
"process.cwd": true,
"process.exit": true,
"process.stderr.write": true
},
"packages": {
"eslint>strip-ansi": true,
"gulp-sass>lodash.clonedeep": true,
"gulp-sass>picocolors": true,
"gulp-sass>replace-ext": true,
"gulp-zip>plugin-error": true,
"vinyl-sourcemaps-apply": true
}
},
"gulp-sass>picocolors": {
"builtin": {
"tty.isatty": true
},
"globals": {
"process.argv.includes": true,
"process.env": true,
"process.platform": true
}
},
"gulp-sass>replace-ext": {
"builtin": {
"path.basename": true,
"path.dirname": true,
"path.extname": true,
"path.join": true,
"path.sep": true
}
},
"gulp-sort": {
"packages": {
"gulp-sort>through2": true
@ -4073,11 +4011,11 @@
"setTimeout": true
},
"packages": {
"eslint>glob-parent": true,
"gulp-watch>ansi-colors": true,
"gulp-watch>anymatch": true,
"gulp-watch>chokidar": true,
"gulp-watch>fancy-log": true,
"gulp-watch>glob-parent": true,
"gulp-watch>path-is-absolute": true,
"gulp-watch>slash": true,
"gulp-watch>vinyl-file": true,
@ -4168,8 +4106,8 @@
"gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic": {
"packages": {
"@babel/register>clone-deep>kind-of": true,
"gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic>math-random": true,
"gulp>undertaker>bach>array-last>is-number": true
"gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic>is-number": true,
"gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic>math-random": true
}
},
"gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic>math-random": {
@ -4221,15 +4159,7 @@
"path.dirname": true
},
"packages": {
"gulp-watch>anymatch>micromatch>parse-glob>glob-base>glob-parent": true,
"gulp-watch>anymatch>micromatch>parse-glob>glob-base>is-glob": true
}
},
"gulp-watch>anymatch>micromatch>parse-glob>glob-base>glob-parent": {
"builtin": {
"path.dirname": true
},
"packages": {
"eslint>glob-parent": true,
"gulp-watch>anymatch>micromatch>parse-glob>glob-base>is-glob": true
}
},
@ -4282,6 +4212,7 @@
},
"packages": {
"chokidar>normalize-path": true,
"eslint>glob-parent": true,
"eslint>is-glob": true,
"gulp-watch>chokidar>anymatch": true,
"gulp-watch>chokidar>async-each": true,
@ -4457,20 +4388,10 @@
},
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values": {
"packages": {
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number": true,
"gulp-watch>chokidar>braces>fill-range>is-number": true,
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>kind-of": true
}
},
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number": {
"packages": {
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": true
}
},
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": {
"packages": {
"browserify>insert-module-globals>is-buffer": true
}
},
"gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>kind-of": {
"packages": {
"browserify>insert-module-globals>is-buffer": true
@ -4885,30 +4806,6 @@
"fancy-log>time-stamp": true
}
},
"gulp-watch>glob-parent": {
"builtin": {
"os.platform": true,
"path": true
},
"packages": {
"gulp-watch>glob-parent>is-glob": true,
"gulp-watch>glob-parent>path-dirname": true
}
},
"gulp-watch>glob-parent>is-glob": {
"packages": {
"gulp-watch>glob-parent>is-glob>is-extglob": true
}
},
"gulp-watch>glob-parent>path-dirname": {
"builtin": {
"path": true,
"util.inspect": true
},
"globals": {
"process.platform": true
}
},
"gulp-watch>path-is-absolute": {
"globals": {
"process.platform": true
@ -5206,9 +5103,9 @@
},
"packages": {
"chokidar>normalize-path": true,
"eslint>glob-parent": true,
"eslint>is-glob": true,
"gulp-watch>chokidar>async-each": true,
"gulp-watch>glob-parent": true,
"gulp-watch>path-is-absolute": true,
"gulp>glob-watcher>anymatch": true,
"gulp>glob-watcher>chokidar>braces": true,
@ -5239,24 +5136,14 @@
},
"packages": {
"gulp-watch>chokidar>braces>extend-shallow": true,
"gulp>glob-watcher>chokidar>braces>fill-range>is-number": true,
"gulp-watch>chokidar>braces>fill-range>is-number": true,
"gulp>glob-watcher>chokidar>braces>fill-range>to-regex-range": true,
"stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true
}
},
"gulp>glob-watcher>chokidar>braces>fill-range>is-number": {
"packages": {
"gulp>glob-watcher>chokidar>braces>fill-range>is-number>kind-of": true
}
},
"gulp>glob-watcher>chokidar>braces>fill-range>is-number>kind-of": {
"packages": {
"browserify>insert-module-globals>is-buffer": true
}
},
"gulp>glob-watcher>chokidar>braces>fill-range>to-regex-range": {
"packages": {
"gulp>glob-watcher>chokidar>braces>fill-range>is-number": true,
"gulp-watch>chokidar>braces>fill-range>is-number": true,
"stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true
}
},
@ -5406,7 +5293,6 @@
"gulp>gulp-cli>matchdep>micromatch>fragment-cache": true,
"gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": true,
"gulp>gulp-cli>matchdep>micromatch>nanomatch>extend-shallow": true,
"gulp>gulp-cli>matchdep>micromatch>nanomatch>is-odd": true,
"gulp>gulp-cli>matchdep>micromatch>regex-not": true,
"nyc>spawn-wrap>is-windows": true
}
@ -5428,11 +5314,6 @@
"@babel/register>clone-deep>is-plain-object": true
}
},
"gulp>gulp-cli>matchdep>micromatch>nanomatch>is-odd": {
"packages": {
"gulp>undertaker>bach>array-last>is-number": true
}
},
"gulp>gulp-cli>matchdep>micromatch>regex-not": {
"packages": {
"gulp-watch>chokidar>braces>to-regex>safe-regex": true,
@ -5522,7 +5403,7 @@
},
"gulp>undertaker>bach>array-initial": {
"packages": {
"gulp>undertaker>bach>array-last>is-number": true,
"gulp>undertaker>bach>array-initial>is-number": true,
"gulp>undertaker>object.defaults>array-slice": true
}
},
@ -5652,7 +5533,7 @@
"process.nextTick": true
},
"packages": {
"gulp-watch>glob-parent": true,
"eslint>glob-parent": true,
"gulp>glob-watcher>is-negated-glob": true,
"gulp>vinyl-fs>glob-stream>ordered-read-streams": true,
"gulp>vinyl-fs>glob-stream>pumpify": true,
@ -6066,7 +5947,7 @@
"process.platform": true
},
"packages": {
"gulp-dart-sass>chalk>escape-string-regexp": true,
"gulp-livereload>chalk>escape-string-regexp": true,
"lavamoat>@babel/highlight>chalk>ansi-styles": true,
"lavamoat>@babel/highlight>chalk>supports-color": true
}
@ -6688,6 +6569,7 @@
"url.resolve": true
},
"globals": {
"TextDecoder": true,
"setImmediate": true
},
"packages": {
@ -7610,7 +7492,7 @@
},
"terser": {
"globals": {
"Buffer.from": true,
"Buffer": true,
"atob": true,
"btoa": true,
"console.log": true,

View File

@ -91,15 +91,18 @@
"test-storybook": "test-storybook -c .storybook",
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn storybook:build && npx http-server storybook-build --port 6006 \" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2\"",
"githooks:install": "husky install",
"fitness-functions": "node development/fitness-functions/index.js",
"fitness-functions": "ts-node development/fitness-functions/index.ts",
"generate-beta-commit": "node ./development/generate-beta-commit.js"
},
"resolutions": {
"analytics-node/axios": "^0.21.2",
"ganache-core/lodash": "^4.17.21",
"git-url-parse@^12.0.0": "^13.1.0",
"glob-parent": "^6.0.2",
"netmask": "^2.0.1",
"json-schema": "^0.4.0",
"ast-types": "^0.14.2",
"x-default-browser": "^0.5.2",
"web3-provider-engine/eth-json-rpc-filters": "^6.0.0",
"typescript@~4.4.0": "patch:typescript@npm:4.4.4#.yarn/patches/typescript-npm-4.4.4-3fedcc07a3.patch",
"acorn@^7.0.0": "patch:acorn@npm:7.4.1#.yarn/patches/acorn-npm-7.4.1-f450b4646c.patch",
@ -229,7 +232,7 @@
"@metamask/assets-controllers": "^6.0.0",
"@metamask/base-controller": "^2.0.0",
"@metamask/contract-metadata": "^2.3.1",
"@metamask/controller-utils": "^3.2.0",
"@metamask/controller-utils": "^3.3.0",
"@metamask/design-tokens": "^1.9.0",
"@metamask/desktop": "^0.3.0",
"@metamask/eth-json-rpc-infura": "^8.0.0",
@ -243,7 +246,7 @@
"@metamask/jazzicon": "^2.0.0",
"@metamask/key-tree": "^7.0.0",
"@metamask/logo": "^3.1.1",
"@metamask/message-manager": "^3.0.0",
"@metamask/message-manager": "^3.1.1",
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/notification-controller": "^2.0.0",
"@metamask/obs-store": "^8.1.0",
@ -255,7 +258,7 @@
"@metamask/rpc-methods": "^0.32.2",
"@metamask/safe-event-emitter": "^2.0.0",
"@metamask/scure-bip39": "^2.0.3",
"@metamask/slip44": "^2.1.0",
"@metamask/slip44": "^3.0.0",
"@metamask/smart-transactions-controller": "^3.1.0",
"@metamask/snaps-controllers": "^0.32.2",
"@metamask/snaps-ui": "^0.32.2",
@ -375,7 +378,7 @@
"@metamask/eslint-config-typescript": "^9.0.1",
"@metamask/forwarder": "^1.1.0",
"@metamask/phishing-warning": "^2.1.0",
"@metamask/test-dapp": "^5.6.0",
"@metamask/test-dapp": "^6.0.0",
"@sentry/cli": "^1.58.0",
"@storybook/addon-a11y": "^6.5.13",
"@storybook/addon-actions": "^6.5.13",
@ -405,7 +408,7 @@
"@types/fs-extra": "^9.0.13",
"@types/gulp": "^4.0.9",
"@types/gulp-autoprefixer": "^0.0.33",
"@types/gulp-dart-sass": "^1.0.1",
"@types/gulp-sass": "^5.0.0",
"@types/gulp-sourcemaps": "^0.0.35",
"@types/jest": "^29.1.2",
"@types/jest-when": "^3.5.2",
@ -417,6 +420,7 @@
"@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.25",
"@types/remote-redux-devtools": "^0.5.5",
"@types/sass": "^1.43.1",
"@types/sinon": "^10.0.13",
"@types/w3c-web-hid": "^1.0.3",
"@types/watchify": "^3.11.1",
@ -465,10 +469,10 @@
"globby": "^11.0.4",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^8.0.0",
"gulp-dart-sass": "^1.0.2",
"gulp-livereload": "4.0.0",
"gulp-rename": "^2.0.0",
"gulp-rtlcss": "^1.4.0",
"gulp-sass": "^5.1.0",
"gulp-sort": "^2.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-stylelint": "^13.0.0",
@ -528,6 +532,7 @@
"string.prototype.matchall": "^4.0.2",
"style-loader": "^0.21.0",
"stylelint": "^13.6.1",
"superstruct": "^1.0.3",
"terser": "^5.7.0",
"through2": "^4.0.2",
"ts-node": "^10.5.0",

View File

@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { DialogType } from '@metamask/rpc-methods';
///: END:ONLY_INCLUDE_IN
import { RestrictedMethods } from './permissions';
@ -20,18 +20,6 @@ export const ENVIRONMENT_TYPE_NOTIFICATION = 'notification';
export const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen';
export const ENVIRONMENT_TYPE_BACKGROUND = 'background';
/**
* The distribution this build is intended for.
*
* This should be kept in-sync with the `BuildType` map in `development/build/utils.js`.
*/
export const BuildType = {
beta: 'beta',
desktop: 'desktop',
flask: 'flask',
main: 'main',
} as const;
export const PLATFORM_BRAVE = 'Brave';
export const PLATFORM_CHROME = 'Chrome';
export const PLATFORM_EDGE = 'Edge';
@ -57,12 +45,12 @@ export const MESSAGE_TYPE = {
WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions',
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
SNAP_DIALOG_ALERT: `${RestrictedMethods.snap_dialog}:alert`,
SNAP_DIALOG_CONFIRMATION: `${RestrictedMethods.snap_dialog}:confirmation`,
SNAP_DIALOG_PROMPT: `${RestrictedMethods.snap_dialog}:prompt`,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(mmi)
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
MMI_AUTHENTICATE: 'metamaskinstitutional_authenticate',
MMI_REAUTHENTICATE: 'metamaskinstitutional_reauthenticate',
MMI_REFRESH_TOKEN: 'metamaskinstitutional_refresh_token',
@ -75,7 +63,7 @@ export const MESSAGE_TYPE = {
///: END:ONLY_INCLUDE_IN
} as const;
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
export const SNAP_DIALOG_TYPES = {
[DialogType.Alert]: MESSAGE_TYPE.SNAP_DIALOG_ALERT,
[DialogType.Confirmation]: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,

View File

@ -1,2 +0,0 @@
export const isMain = process.env.METAMASK_BUILD_TYPE === 'main';
export const isFlask = process.env.METAMASK_BUILD_TYPE === 'flask';

View File

@ -33,7 +33,7 @@ export enum HardwareAffiliateLinks {
ledger = 'https://shop.ledger.com/?r=17c4991a03fa',
gridplus = 'https://gridplus.io/?afmc=7p',
trezor = 'https://shop.trezor.io/product/trezor-one-black?offer_id=35&aff_id=11009',
keystone = 'https://shop.keyst.one/?rfsn=6088257.656b3e9&utm_source=refersion&utm_medium=affiliate&utm_campaign=6088257.656b3e9',
keystone = 'https://keyst.one/metamask?rfsn=6088257.656b3e9&utm_source=refersion&utm_medium=affiliate&utm_campaign=6088257.656b3e9',
airgap = 'https://airgap.it/',
coolwallet = 'https://www.coolwallet.io/',
dcent = 'https://dcentwallet.com/',

View File

@ -538,6 +538,9 @@ export enum MetaMetricsEventName {
OnboardingWalletVideoPlay = 'SRP Intro Video Played',
OnboardingTwitterClick = 'External Link Clicked',
ServiceWorkerRestarted = 'Service Worker Restarted',
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
UserClickedDeepLink = 'User clicked deeplink',
///: END:ONLY_INCLUDE_IN
}
export enum MetaMetricsEventAccountType {
@ -577,6 +580,9 @@ export enum MetaMetricsEventCategory {
Wallet = 'Wallet',
Desktop = 'Desktop',
ServiceWorkers = 'service_workers',
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
MMI = 'Institutional',
///: END:ONLY_INCLUDE_IN
}
export enum MetaMetricsEventLinkType {

View File

@ -540,16 +540,13 @@ export const BUYABLE_CHAINS_MAP: {
| typeof CHAIN_IDS.MOONRIVER
| typeof CHAIN_IDS.AURORA
| typeof CHAIN_IDS.LINEA_TESTNET
| typeof CHAIN_IDS.GOERLI
>]: BuyableChainSettings;
} = {
[CHAIN_IDS.MAINNET]: {
nativeCurrency: CURRENCY_SYMBOLS.ETH,
network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME,
},
[CHAIN_IDS.GOERLI]: {
nativeCurrency: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI],
network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME,
},
[CHAIN_IDS.SEPOLIA]: {
nativeCurrency: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA],
network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME,

View File

@ -4,7 +4,7 @@ export const CaveatTypes = Object.freeze({
export const RestrictedMethods = Object.freeze({
eth_accounts: 'eth_accounts',
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
snap_dialog: 'snap_dialog',
snap_notify: 'snap_notify',
snap_manageState: 'snap_manageState',
@ -16,7 +16,7 @@ export const RestrictedMethods = Object.freeze({
///: END:ONLY_INCLUDE_IN
} as const);
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
/**
* Exclude permissions by code fencing them to avoid any potential usage of excluded permissions at runtime. See: https://github.com/MetaMask/metamask-extension/pull/17321#pullrequestreview-1287014285.
* This is a fix for https://github.com/MetaMask/snaps-monorepo/issues/1103 and https://github.com/MetaMask/snaps-monorepo/issues/990.

View File

@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import type { SupportedCurve } from '@metamask/key-tree';
type SnapsMetadata = {

View File

@ -308,7 +308,7 @@ interface DappSuggestedGasFees {
* An object representing a transaction, in whatever state it is in.
*/
export interface TransactionMeta {
///: BEGIN:ONLY_INCLUDE_IN(mmi)
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
custodyStatus: string;
custodyId?: string;
///: END:ONLY_INCLUDE_IN

View File

@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
import browser from 'webextension-polyfill';
///: END:ONLY_INCLUDE_IN
import { memoize } from 'lodash';
@ -7,7 +7,7 @@ import {
fetchLocale,
loadRelativeTimeFormatLocaleData,
} from '../../ui/helpers/utils/i18n-helper';
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
import { renderDesktopError } from '../../ui/pages/desktop-error/render-desktop-error';
import { EXTENSION_ERROR_PAGE_TYPES } from '../constants/desktop';
import { openCustomProtocol } from './deep-linking';
@ -44,7 +44,7 @@ export async function getErrorHtml(
errorKey,
supportLink,
metamaskState,
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
err,
///: END:ONLY_INCLUDE_IN
) {
@ -65,7 +65,7 @@ export async function getErrorHtml(
const { currentLocaleMessages, enLocaleMessages } = response;
const t = getLocaleContext(currentLocaleMessages, enLocaleMessages);
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
const isDesktopEnabled = metamaskState?.desktopEnabled === true;
if (isDesktopEnabled) {
@ -120,7 +120,7 @@ export async function getErrorHtml(
`;
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
///: BEGIN:ONLY_INCLUDE_IN(desktop)
export const MMD_DOWNLOAD_LINK =
'https://github.com/MetaMask/metamask-desktop/releases';

View File

@ -1,14 +1,6 @@
let _supportLink = 'https://support.metamask.io';
///: BEGIN:ONLY_INCLUDE_IN(mmi)
_supportLink = 'https://mmi-support.zendesk.com/hc/en-us';
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(flask)
_supportLink = 'https://metamask-flask.zendesk.com/hc';
///: END:ONLY_INCLUDE_IN
export const SUPPORT_LINK = _supportLink;
// no destructuring as process.env detection stops working
// eslint-disable-next-line prefer-destructuring
export const SUPPORT_LINK = process.env.SUPPORT_LINK;
export const COINGECKO_LINK = 'https://www.coingecko.com/';
export const CRYPTOCOMPARE_LINK = 'https://www.cryptocompare.com/';

View File

@ -57,7 +57,43 @@
"priorityFeeTrend": "down",
"networkCongestion": 0.90625
},
"snaps": [{}],
"snaps": {
"npm:@metamask/test-snap-bip44": {
"id": "npm:@metamask/test-snap-bip44",
"origin": "npm:@metamask/test-snap-bip44",
"version": "5.1.2",
"iconUrl": null,
"initialPermissions": {
"endowment:ethereum-provider": {}
},
"manifest": {
"description": "An example Snap that signs messages using BLS.",
"proposedName": "BIP-44 Test Snap",
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/test-snaps.git"
},
"source": {
"location": {
"npm": {
"filePath": "dist/bundle.js",
"packageName": "@metamask/test-snap-bip44",
"registry": "https://registry.npmjs.org"
}
},
"shasum": "L1k+dT9Q+y3KfIqzaH09MpDZVPS9ZowEh9w01ZMTWMU="
},
"version": "5.1.2"
},
"versionHistory": [
{
"date": 1680686075921,
"origin": "https://metamask.github.io",
"version": "5.1.2"
}
]
}
},
"preferences": {
"hideZeroBalanceTokens": false,
"showFiatInTestnets": false,

View File

@ -14,7 +14,7 @@ const {
} = require('@metamask/test-dapp/dist/constants.json');
const hstFactory = {
initialAmount: 100,
initialAmount: 10,
tokenName: 'TST',
decimalUnits: 4,
tokenSymbol: 'TST',

View File

@ -92,6 +92,10 @@ describe('Test Snap Management', function () {
await driver.delay(1000);
// try to disable the snap
await driver.clickElement({
text: 'Notification Test Snap',
tag: 'p',
});
await driver.clickElement('.toggle-button > div');
// switch back to test-snaps window
@ -130,12 +134,11 @@ describe('Test Snap Management', function () {
);
assert.equal(await notificationResult.getText(), '1');
// click on see details
await driver.clickElement({ text: 'See details', tag: 'button' });
await driver.delay(1000);
// try to remove snap
await driver.clickElement({ text: 'Remove snap', tag: 'button' });
await driver.clickElement({
text: 'Remove Notification Test Snap',
tag: 'p',
});
await driver.delay(1000);
// try to click remove on popover

View File

@ -336,11 +336,11 @@ describe('Send ETH from dapp using advanced gas controls', function () {
await driver.clickElement({ text: 'Save', tag: 'button' });
await driver.waitForSelector({
css: '.transaction-detail-item:nth-of-type(1) h6:nth-of-type(2)',
text: '0.02367237 ETH',
text: '0.04503836 ETH',
});
await driver.waitForSelector({
css: '.transaction-detail-item:nth-of-type(2) h6:nth-of-type(2)',
text: '0.02367237 ETH',
text: '0.04503836 ETH',
});
await driver.clickElement({ text: 'Confirm', tag: 'button' });

View File

@ -155,7 +155,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
});
await driver.waitForSelector({
css: '.transaction-detail-item',
text: '0.00008346 ETH',
text: '0.00008455 ETH',
});
await driver.clickElement({ text: 'Next', tag: 'button' });
@ -220,7 +220,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
});
await driver.waitForSelector({
css: '.transaction-detail-item',
text: '0.00008346 ETH',
text: '0.00008455 ETH',
});
await driver.clickElement({ text: 'Next', tag: 'button' });

View File

@ -1 +1,2 @@
process.env.METAMASK_ENV = 'test';
process.env.METAMASK_ENVIRONMENT = 'test';
process.env.SUPPORT_LINK = 'https://support.metamask.io';

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