Merge branch 'develop' into master-sync
@ -3,10 +3,10 @@ version: 2.1
|
||||
executors:
|
||||
node-browsers:
|
||||
docker:
|
||||
- image: circleci/node:16-browsers
|
||||
- image: cimg/node:16.20-browsers
|
||||
node-browsers-medium-plus:
|
||||
docker:
|
||||
- image: circleci/node:16-browsers
|
||||
- image: cimg/node:16.20-browsers
|
||||
resource_class: medium+
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=2048
|
||||
|
@ -4,6 +4,8 @@ set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
# To get the latest version, see <https://www.ubuntuupdates.org/ppa/google_chrome?dist=stable>
|
||||
CHROME_VERSION='111.0.5563.64-1'
|
||||
CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||
|
@ -9,10 +9,6 @@ FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2"
|
||||
FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}"
|
||||
FIREFOX_PATH='/opt/firefox'
|
||||
|
||||
printf '%s\n' "Removing old Firefox installation"
|
||||
|
||||
sudo rm -r "${FIREFOX_PATH}"
|
||||
|
||||
printf '%s\n' "Downloading & installing Firefox ${FIREFOX_VERSION}"
|
||||
|
||||
wget --quiet --show-progress -O- "${FIREFOX_BINARY_URL}" | sudo tar xj -C /opt
|
||||
|
@ -10,6 +10,14 @@ ignores:
|
||||
- '@fortawesome/fontawesome-free'
|
||||
- 'punycode'
|
||||
|
||||
#
|
||||
# snaps flask deps
|
||||
#
|
||||
- '@metamask/rpc-methods-flask'
|
||||
- '@metamask/snaps-controllers-flask'
|
||||
- '@metamask/snaps-ui-flask'
|
||||
- '@metamask/snaps-utils-flask'
|
||||
|
||||
#
|
||||
# dev deps
|
||||
#
|
||||
|
14
.github/CODEOWNERS
vendored
@ -1,20 +1,22 @@
|
||||
# Lines starting with '#' are comments.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
# Owners bear a responsibility to the organization and the users of this
|
||||
# application. Repository administrators have the ability to merge pull
|
||||
# requests that have not yet received the requisite reviews as outlined
|
||||
# in this file. Do not force merge any PR without confidence that it
|
||||
# follows all policies or without full understanding of the impact of
|
||||
# application. Repository administrators have the ability to merge pull
|
||||
# requests that have not yet received the requisite reviews as outlined
|
||||
# in this file. Do not force merge any PR without confidence that it
|
||||
# follows all policies or without full understanding of the impact of
|
||||
# those changes on build, release and publishing outcomes.
|
||||
|
||||
* @MetaMask/extension-devs
|
||||
**/snaps/** @MetaMask/snaps-devs
|
||||
**/flask/** @MetaMask/snaps-devs
|
||||
development/ @MetaMask/extension-devs @kumavis
|
||||
lavamoat/ @MetaMask/supply-chain
|
||||
lavamoat/ @MetaMask/extension-devs @MetaMask/supply-chain @MetaMask/snaps-devs
|
||||
|
||||
# The .circleci/ folder instructs Circle CI on the process by which it
|
||||
# should test, build and publish releases of our application. Due to the
|
||||
# impact that changes to the files contained within this folder may have
|
||||
# on our releases, only those with the knowledge and responsibility to
|
||||
# on our releases, only those with the knowledge and responsibility to
|
||||
# publish libraries under the MetaMask name may approve those changes.
|
||||
# Note to reviewers: We employ the use of CircleCI "Orbs", which are
|
||||
# remotely hosted sections of CircleCI configuration and scripts, to
|
||||
|
17
.github/workflows/do-not-merge.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Fails the pull request if it has the "DO-NOT-MERGE" label
|
||||
|
||||
name: Check "DO-NOT-MERGE" label
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
|
||||
jobs:
|
||||
do-not-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'DO-NOT-MERGE') }}
|
||||
steps:
|
||||
- name: 'Check for label "DO-NOT-MERGE"'
|
||||
run: |
|
||||
echo 'This check fails PRs with the "DO-NOT-MERGE" label to block merging'
|
||||
exit 1
|
16
.github/workflows/fitness-functions.yml
vendored
@ -11,18 +11,24 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
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"
|
||||
# The following command generates a diff of changes between the common
|
||||
# ancestor of $BASE_REF and HEAD, and the current commit (HEAD), for
|
||||
# files in the current directory and its subdirectories. The output is
|
||||
# then saved to a file called "diff".
|
||||
git diff "$(git merge-base "origin/$BASE_REF" HEAD)" HEAD -- . > ./diff
|
||||
npm run fitness-functions -- "ci" "./diff"
|
10
.iyarc
@ -6,16 +6,6 @@ GHSA-257v-vj4p-3w2h
|
||||
# 7.1.1 and is included only via remote-redux-devtools which is a devDependency
|
||||
GHSA-6fc8-4gx4-v693
|
||||
|
||||
# yarn npm audit reports on a fast-json-patch version < 3.1.1 but due to patch
|
||||
# resolution, the only version of fast-json-patch that we use is 3.1.1. We also
|
||||
# have 2.2.1 installed but it is a dev only dependency. The "violation" reports
|
||||
# smart-transacton-controller as the culprit but if you run
|
||||
# `yarn info -A -R dependents fast-json-patch` you can see that only 2.2.1 and
|
||||
# 3.3.1 are installed and that smart-transaction-controller resolves to the
|
||||
# patched version of 3.3.1. We can remove this once the
|
||||
# smart-transaction-controller updates its dependency.
|
||||
GHSA-8gh8-hqwg-xf34
|
||||
|
||||
# request library is subject to SSRF.
|
||||
# addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch
|
||||
GHSA-p8p7-x288-28g6
|
||||
|
@ -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=
|
||||
|
@ -599,7 +599,7 @@ const state = {
|
||||
connectedStatusPopoverHasBeenShown: true,
|
||||
swapsWelcomeMessageHasBeenShown: true,
|
||||
defaultHomeActiveTabName: 'Assets',
|
||||
provider: {
|
||||
providerConfig: {
|
||||
type: 'goerli',
|
||||
ticker: 'ETH',
|
||||
nickname: '',
|
||||
|
59
.yarn/patches/@eslint-eslintrc-npm-2.0.2-d308674d86.patch
Normal file
@ -1,8 +1,8 @@
|
||||
diff --git a/lib/linter/linter.js b/lib/linter/linter.js
|
||||
index 29d78da3969e2a3560d056af5683a08083562984..a6ae07b7142a353fcd8d58b55a7e68b8f81b2846 100644
|
||||
index 0f1bd4f77611aa5ccc43c94385efd4f9b5639327..59ff9f14727e22bbec2cc7539af30305f759b23a 100644
|
||||
--- a/lib/linter/linter.js
|
||||
+++ b/lib/linter/linter.js
|
||||
@@ -704,7 +704,7 @@ function createLanguageOptions({ globals: configuredGlobals, parser, parserOptio
|
||||
@@ -708,7 +708,7 @@ function createLanguageOptions({ globals: configuredGlobals, parser, parserOptio
|
||||
*/
|
||||
function resolveGlobals(providedGlobals, enabledEnvironments) {
|
||||
return Object.assign(
|
34
app/_locales/de/messages.json
generated
@ -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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,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"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Was Sie beachten sollten:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Dies basiert auf Informationen von "
|
||||
},
|
||||
"time": {
|
||||
"message": "Zeit"
|
||||
},
|
||||
|
34
app/_locales/el/messages.json
generated
@ -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."
|
||||
@ -3227,10 +3211,6 @@
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "Δε βρέθηκαν αποτελέσματα που να ταιριάζουν."
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
|
||||
},
|
||||
"show": {
|
||||
"message": "Εμφάνιση"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "Διαχειριστείτε τα Snaps σας"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "Η κατάσταση του Snap εξαρτάται από τη δραστηριότητα."
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "Ένα snap θα εκτελεστεί μόνο εάν είναι ενεργοποιημένο"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Πράγματα που πρέπει να έχετε υπόψη σας:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Αυτό βασίζεται σε πληροφορίες από"
|
||||
},
|
||||
"time": {
|
||||
"message": "Ώρα"
|
||||
},
|
||||
|
246
app/_locales/en/messages.json
generated
@ -103,9 +103,6 @@
|
||||
"SIWEWarningTitle": {
|
||||
"message": "Are you sure?"
|
||||
},
|
||||
"ShowMore": {
|
||||
"message": "Show more"
|
||||
},
|
||||
"about": {
|
||||
"message": "About"
|
||||
},
|
||||
@ -333,6 +330,12 @@
|
||||
"alerts": {
|
||||
"message": "Alerts"
|
||||
},
|
||||
"allCustodianAccountsConnectedSubtitle": {
|
||||
"message": "You have either already connected all your custodian accounts or don’t 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 +469,9 @@
|
||||
"average": {
|
||||
"message": "Average"
|
||||
},
|
||||
"awaitingApproval": {
|
||||
"message": "Awaiting approval..."
|
||||
},
|
||||
"back": {
|
||||
"message": "Back"
|
||||
},
|
||||
@ -546,6 +552,9 @@
|
||||
"message": "View account at $1",
|
||||
"description": "$1 replaced by URL for custom block explorer"
|
||||
},
|
||||
"blockaid": {
|
||||
"message": "Blockaid"
|
||||
},
|
||||
"blockies": {
|
||||
"message": "Blockies"
|
||||
},
|
||||
@ -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."
|
||||
},
|
||||
@ -951,6 +983,9 @@
|
||||
"custodyRefreshTokenModalTitle": {
|
||||
"message": "Your custodian session has expired"
|
||||
},
|
||||
"custodySessionExpired": {
|
||||
"message": "Custodian session expired."
|
||||
},
|
||||
"custom": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
@ -1061,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."
|
||||
},
|
||||
@ -1325,6 +1364,9 @@
|
||||
"enableSmartTransactions": {
|
||||
"message": "Enable smart transactions"
|
||||
},
|
||||
"enableSnap": {
|
||||
"message": "Enable"
|
||||
},
|
||||
"enableToken": {
|
||||
"message": "enable $1",
|
||||
"description": "$1 is a token symbol, e.g. ETH"
|
||||
@ -1361,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"
|
||||
},
|
||||
@ -1370,6 +1415,9 @@
|
||||
"enterPasswordContinue": {
|
||||
"message": "Enter password to continue"
|
||||
},
|
||||
"enterYourPassword": {
|
||||
"message": "Enter your password"
|
||||
},
|
||||
"errorCode": {
|
||||
"message": "Code: $1",
|
||||
"description": "Displayed error code for debugging purposes. $1 is the error code"
|
||||
@ -1475,18 +1523,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."
|
||||
@ -1630,6 +1666,12 @@
|
||||
"general": {
|
||||
"message": "General"
|
||||
},
|
||||
"globalTitle": {
|
||||
"message": "Global menu"
|
||||
},
|
||||
"globalTourDescription": {
|
||||
"message": "See your portfolio, connected sites, settings, and more"
|
||||
},
|
||||
"goBack": {
|
||||
"message": "Go back"
|
||||
},
|
||||
@ -1843,6 +1885,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"
|
||||
},
|
||||
@ -1971,6 +2020,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"
|
||||
},
|
||||
@ -1987,16 +2039,16 @@
|
||||
"message": "Prior to clicking confirm:"
|
||||
},
|
||||
"ledgerConnectionInstructionStepFour": {
|
||||
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device"
|
||||
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device."
|
||||
},
|
||||
"ledgerConnectionInstructionStepOne": {
|
||||
"message": "Enable Use Ledger Live under Settings > Advanced"
|
||||
"message": "Enable Use Ledger Live under Settings > Advanced."
|
||||
},
|
||||
"ledgerConnectionInstructionStepThree": {
|
||||
"message": "Plug in your Ledger device and select the Ethereum app"
|
||||
"message": "Be sure your Ledger is plugged in and to select the Ethereum app."
|
||||
},
|
||||
"ledgerConnectionInstructionStepTwo": {
|
||||
"message": "Open and unlock Ledger Live App"
|
||||
"message": "Open and unlock Ledger Live App."
|
||||
},
|
||||
"ledgerConnectionPreferenceDescription": {
|
||||
"message": "Customize how you connect your Ledger to MetaMask. $1 is recommended, but other options are available. Read more here: $2",
|
||||
@ -2143,6 +2195,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."
|
||||
@ -2181,6 +2236,9 @@
|
||||
"mmiAuthenticate": {
|
||||
"message": "The page at $1 would like to authorise the following project’s compliance settings in MetaMask Institutional"
|
||||
},
|
||||
"more": {
|
||||
"message": "more"
|
||||
},
|
||||
"moreComingSoon": {
|
||||
"message": "More coming soon..."
|
||||
},
|
||||
@ -2229,6 +2287,9 @@
|
||||
"networkIsBusy": {
|
||||
"message": "Network is busy. Gas prices are high and estimates are less accurate."
|
||||
},
|
||||
"networkMenu": {
|
||||
"message": "Network Menu"
|
||||
},
|
||||
"networkMenuHeading": {
|
||||
"message": "Select a network"
|
||||
},
|
||||
@ -2393,6 +2454,9 @@
|
||||
"noNFTs": {
|
||||
"message": "No NFTs yet"
|
||||
},
|
||||
"noReport": {
|
||||
"message": "No Report"
|
||||
},
|
||||
"noSnaps": {
|
||||
"message": "You don't have any snaps installed."
|
||||
},
|
||||
@ -2795,18 +2859,21 @@
|
||||
"message": "Open Codefi Compliance"
|
||||
},
|
||||
"openFullScreenForLedgerWebHid": {
|
||||
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
|
||||
"message": "Go to full screen to connect your Ledger.",
|
||||
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."
|
||||
},
|
||||
"openInBlockExplorer": {
|
||||
"message": "Open in block explorer"
|
||||
},
|
||||
"openSea": {
|
||||
"message": "OpenSea (Beta)"
|
||||
"message": "OpenSea + Blockaid (Beta)"
|
||||
},
|
||||
"openSeaNew": {
|
||||
"message": "OpenSea"
|
||||
},
|
||||
"operationFailed": {
|
||||
"message": "Operation Failed"
|
||||
},
|
||||
"optional": {
|
||||
"message": "Optional"
|
||||
},
|
||||
@ -2866,6 +2933,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"
|
||||
@ -3046,6 +3116,12 @@
|
||||
"permissions": {
|
||||
"message": "Permissions"
|
||||
},
|
||||
"permissionsTitle": {
|
||||
"message": "Permissions"
|
||||
},
|
||||
"permissionsTourDescription": {
|
||||
"message": "Find your connected accounts and manage permissions here"
|
||||
},
|
||||
"personalAddressDetected": {
|
||||
"message": "Personal address detected. Input the token contract address."
|
||||
},
|
||||
@ -3097,6 +3173,10 @@
|
||||
"message": "Private Key",
|
||||
"description": "select this type of file to use to import an account"
|
||||
},
|
||||
"privateKeyCopyWarning": {
|
||||
"message": "Private key for $1",
|
||||
"description": "$1 represents the account name"
|
||||
},
|
||||
"privateKeyWarning": {
|
||||
"message": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account."
|
||||
},
|
||||
@ -3208,6 +3288,12 @@
|
||||
"removeAccountDescription": {
|
||||
"message": "This account will be removed from your wallet. Please make sure you have the original Secret Recovery Phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. "
|
||||
},
|
||||
"removeJWT": {
|
||||
"message": "Remove custodian token"
|
||||
},
|
||||
"removeJWTDescription": {
|
||||
"message": "Are you sure you want to remove this token? All accounts assigned to this token will be removed from extension as well: "
|
||||
},
|
||||
"removeNFT": {
|
||||
"message": "Remove NFT"
|
||||
},
|
||||
@ -3227,6 +3313,12 @@
|
||||
"replace": {
|
||||
"message": "replace"
|
||||
},
|
||||
"reportLastRun": {
|
||||
"message": "Report last run"
|
||||
},
|
||||
"reportLastRunTooltip": {
|
||||
"message": "The date and time of when the last AML/CFT report was run"
|
||||
},
|
||||
"requestFailed": {
|
||||
"message": "Request failed"
|
||||
},
|
||||
@ -3356,9 +3448,18 @@
|
||||
"revokeSpendingCapTooltipText": {
|
||||
"message": "This third party will be unable to spend any more of your current or future tokens."
|
||||
},
|
||||
"riskRating": {
|
||||
"message": "Risk rating"
|
||||
},
|
||||
"riskRatingTooltip": {
|
||||
"message": "The risk rating of the address you are interacting with based on your risk settings"
|
||||
},
|
||||
"rpcUrl": {
|
||||
"message": "New RPC URL"
|
||||
},
|
||||
"runReport": {
|
||||
"message": "Run report"
|
||||
},
|
||||
"safeTransferFrom": {
|
||||
"message": "Safe transfer from"
|
||||
},
|
||||
@ -3398,6 +3499,9 @@
|
||||
"security": {
|
||||
"message": "Security"
|
||||
},
|
||||
"securityAlert": {
|
||||
"message": "Security alert from $1 and $2"
|
||||
},
|
||||
"securityAndPrivacy": {
|
||||
"message": "Security & privacy"
|
||||
},
|
||||
@ -3455,18 +3559,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"
|
||||
},
|
||||
@ -3538,9 +3651,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"
|
||||
@ -3567,12 +3680,21 @@
|
||||
"message": "This relies on $1 which will have access to your Ethereum address and your IP address. $2",
|
||||
"description": "$1 is the link to etherscan url and $2 is the link to the privacy policy of consensys APIs"
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
},
|
||||
"showPermissions": {
|
||||
"message": "Show permissions"
|
||||
},
|
||||
"showPrivateKey": {
|
||||
"message": "Show private key"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Show Private Keys"
|
||||
},
|
||||
"showReport": {
|
||||
"message": "Show report"
|
||||
},
|
||||
"showTestnetNetworks": {
|
||||
"message": "Show test networks"
|
||||
},
|
||||
@ -3615,14 +3737,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."
|
||||
@ -3643,7 +3757,7 @@
|
||||
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
|
||||
},
|
||||
"snapInstallWarningCheck": {
|
||||
"message": "Ensure that the permission below align with your intended actions. Only proceed with authors you trust."
|
||||
"message": "Ensure that the permission below aligns with your intended actions. Only proceed with authors you trust."
|
||||
},
|
||||
"snapInstallWarningCheckPlural": {
|
||||
"message": "Ensure that the permissions below align with your intended actions. Only proceed with authors you trust."
|
||||
@ -3694,9 +3808,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"
|
||||
},
|
||||
@ -3790,6 +3901,9 @@
|
||||
"stableLowercase": {
|
||||
"message": "stable"
|
||||
},
|
||||
"stake": {
|
||||
"message": "Stake"
|
||||
},
|
||||
"stateLogError": {
|
||||
"message": "Error in retrieving state logs."
|
||||
},
|
||||
@ -4270,6 +4384,12 @@
|
||||
"switchedTo": {
|
||||
"message": "You have switched to"
|
||||
},
|
||||
"switcherTitle": {
|
||||
"message": "Network switcher"
|
||||
},
|
||||
"switcherTourDescription": {
|
||||
"message": "Click the icon to switch networks or add a new network"
|
||||
},
|
||||
"switchingNetworksCancelsPendingConfirmations": {
|
||||
"message": "Switching networks will cancel all pending confirmations"
|
||||
},
|
||||
@ -4315,9 +4435,6 @@
|
||||
"thisCollection": {
|
||||
"message": "this collection"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "This is based on information from "
|
||||
},
|
||||
"thisServiceIsExperimental": {
|
||||
"message": "This service is experimental. By enabling this feature, you agree to OpenSea's $1.",
|
||||
"description": "$1 is link to open sea terms of use"
|
||||
@ -4335,11 +4452,44 @@
|
||||
"message": "To: $1",
|
||||
"description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress"
|
||||
},
|
||||
"toggleEthSignBannerDescription": {
|
||||
"message": "You’re at risk for phishing attacks. Protect yourself by turning off eth_sign."
|
||||
},
|
||||
"toggleEthSignDescriptionField": {
|
||||
"message": "Turn this on to let dapps request your signature using eth_sign requests. eth_sign is an open-ended signing method that lets you sign an arbitrary hash, making it a dangerous phishing risk. Only sign eth_sign requests if you can read what you are signing and trust the origin of the request."
|
||||
"message": "If you enable this setting, you might get signature requests that aren’t readable. By signing a message you don't understand, you could be agreeing to give away your funds and NFTs."
|
||||
},
|
||||
"toggleEthSignField": {
|
||||
"message": "Toggle eth_sign requests"
|
||||
"message": "Eth_sign requests"
|
||||
},
|
||||
"toggleEthSignModalBannerBoldText": {
|
||||
"message": " you might be getting scammed"
|
||||
},
|
||||
"toggleEthSignModalBannerText": {
|
||||
"message": "If you've been asked to turn this setting on,"
|
||||
},
|
||||
"toggleEthSignModalCheckBox": {
|
||||
"message": "I understand that I can lose all of my funds and NFTs if I enable eth_sign requests. "
|
||||
},
|
||||
"toggleEthSignModalDescription": {
|
||||
"message": "Allowing eth_sign requests can make you vulnerable to phishing attacks. Always review the URL and be careful when signing messages that contain code."
|
||||
},
|
||||
"toggleEthSignModalFormError": {
|
||||
"message": "The text is incorrect"
|
||||
},
|
||||
"toggleEthSignModalFormLabel": {
|
||||
"message": "Enter “I only sign what I understand” to continue"
|
||||
},
|
||||
"toggleEthSignModalFormValidation": {
|
||||
"message": "I only sign what I understand"
|
||||
},
|
||||
"toggleEthSignModalTitle": {
|
||||
"message": "Use at your own risk"
|
||||
},
|
||||
"toggleEthSignOff": {
|
||||
"message": "OFF (Recommended)"
|
||||
},
|
||||
"toggleEthSignOn": {
|
||||
"message": "ON (Not recommended)"
|
||||
},
|
||||
"toggleTestNetworks": {
|
||||
"message": "$1 test networks",
|
||||
@ -4472,6 +4622,9 @@
|
||||
"transactionErrored": {
|
||||
"message": "Transaction encountered an error."
|
||||
},
|
||||
"transactionFailed": {
|
||||
"message": "Transaction Failed"
|
||||
},
|
||||
"transactionFee": {
|
||||
"message": "Transaction fee"
|
||||
},
|
||||
@ -4500,7 +4653,7 @@
|
||||
"message": "Transaction resubmitted with estimated gas fee increased to $1 at $2"
|
||||
},
|
||||
"transactionSecurityCheck": {
|
||||
"message": "Enable transaction security providers"
|
||||
"message": "Enable security alerts"
|
||||
},
|
||||
"transactionSecurityCheckDescription": {
|
||||
"message": "We use third-party APIs to detect and display risks involved in unsigned transaction and signature requests before you sign them. These services will have access to your unsigned transaction and signature requests, your account address, and your preferred language."
|
||||
@ -4698,6 +4851,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"
|
||||
},
|
||||
|
34
app/_locales/es/messages.json
generated
@ -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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,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"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Cosas a tener en cuenta:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Esto se basa en información de "
|
||||
},
|
||||
"time": {
|
||||
"message": "Tiempo"
|
||||
},
|
||||
|
12
app/_locales/es_419/messages.json
generated
@ -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."
|
||||
|
34
app/_locales/fr/messages.json
generated
@ -695,10 +695,6 @@
|
||||
"message": "$1 n’est 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": "L’importation 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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "Gérez vos Snaps"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "L’état du Snap dépend de l’activité."
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "Un snap ne s’exécute que s’il est activé"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Les choses que vous devez garder à l’esprit :"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Ces informations proviennent de "
|
||||
},
|
||||
"time": {
|
||||
"message": "Temps"
|
||||
},
|
||||
|
34
app/_locales/hi/messages.json
generated
@ -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."
|
||||
@ -3227,10 +3211,6 @@
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "कोई मेल खाने वाला परिणाम नहीं मिला।"
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
|
||||
},
|
||||
"show": {
|
||||
"message": "दिखाएं"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "अपने स्नैप्स प्रबंधित करें"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "स्नैप स्टेटस एक्टिविटी पर निर्भर करता है।"
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "कोई स्नैप तभी चलेगा जब उसे सक्षम किया गया हो"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "ध्यान रखने योग्य बातें"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "से प्राप्त जानकारी पर आधारित है"
|
||||
},
|
||||
"time": {
|
||||
"message": "समय"
|
||||
},
|
||||
|
34
app/_locales/id/messages.json
generated
@ -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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "Kelola Snap Anda"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "Status snap tergantung pada aktivitas."
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "Snap hanya akan beroperasi jika diaktifkan"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Hal-hal yang perlu diingat:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Hal ini berdasarkan informasi dari "
|
||||
},
|
||||
"time": {
|
||||
"message": "Waktu"
|
||||
},
|
||||
|
4
app/_locales/it/messages.json
generated
@ -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..."
|
||||
},
|
||||
|
34
app/_locales/ja/messages.json
generated
@ -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."
|
||||
@ -3227,10 +3211,6 @@
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "一致する結果が見つかりませんでした。"
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
|
||||
},
|
||||
"show": {
|
||||
"message": "表示"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "スナップの管理"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "スナップのステータスはアクティビティによります。"
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "スナップは有効になっている場合にのみ実行されます"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "留意点:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "これは次の情報源からの情報に基づくものです: "
|
||||
},
|
||||
"time": {
|
||||
"message": "時間"
|
||||
},
|
||||
|
82
app/_locales/ko/messages.json
generated
@ -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."
|
||||
@ -2173,7 +2157,7 @@
|
||||
"message": "다음"
|
||||
},
|
||||
"nextNonceWarning": {
|
||||
"message": "임시값이 권장 임시값인 $1보다 큽니다.",
|
||||
"message": "논스값이 권장 논스값인 $1보다 큽니다.",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nftAddFailedMessage": {
|
||||
@ -2240,13 +2224,13 @@
|
||||
"message": "웹캠을 찾을 수 없음"
|
||||
},
|
||||
"nonce": {
|
||||
"message": "임시값"
|
||||
"message": "논스"
|
||||
},
|
||||
"nonceField": {
|
||||
"message": "거래 임시값 맞춤화"
|
||||
"message": "거래 논스 맞춤화"
|
||||
},
|
||||
"nonceFieldDescription": {
|
||||
"message": "이 기능을 켜면 확인 화면에서 임시값(거래 번호)을 변경할 수 있습니다. 이는 고급 기능으로, 주의해서 사용해야 합니다."
|
||||
"message": "이 기능을 켜면 확인 화면에서 논스(거래 번호)를 변경할 수 있습니다. 이는 고급 기능으로, 주의해서 사용해야 합니다."
|
||||
},
|
||||
"nonceFieldHeading": {
|
||||
"message": "커스텀 논스"
|
||||
@ -2346,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": {
|
||||
@ -3227,10 +3211,6 @@
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "검색 결과가 없습니다."
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
|
||||
},
|
||||
"show": {
|
||||
"message": "보기"
|
||||
},
|
||||
@ -3284,7 +3264,7 @@
|
||||
"message": "본 메시지에 서명하는 행위는 위험의 가능성을 내포하고 있습니다. 본 서명을 통해 메시지 발신 당사자에게 귀하의 계정 및 모든 자산에 대해 완전한 권한을 부여할 수 있기 때문입니다. 이를 통해 계정의 모든 잔액을 인출할 수 있기도 하다는 뜻입니다. 주의하여 진행하세요. $1"
|
||||
},
|
||||
"signed": {
|
||||
"message": "서명완료"
|
||||
"message": "서명 완료"
|
||||
},
|
||||
"signin": {
|
||||
"message": "로그인"
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "스냅 관리"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "스냅 상태는 활동에 따라 달라집니다."
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "스냅은 활성화된 상태에서만 작동합니다."
|
||||
},
|
||||
@ -3606,7 +3575,7 @@
|
||||
"message": "보장 금액"
|
||||
},
|
||||
"swapAmountReceivedInfo": {
|
||||
"message": "수신하는 최소 금액입니다. 슬리패지에 따라 추가 금액을 받을 수도 있습니다."
|
||||
"message": "수신하는 최소 금액입니다. 슬리피지에 따라 추가 금액을 받을 수도 있습니다."
|
||||
},
|
||||
"swapApproval": {
|
||||
"message": "스왑을 위해 $1 승인",
|
||||
@ -3637,7 +3606,7 @@
|
||||
"message": "맞춤형"
|
||||
},
|
||||
"swapDecentralizedExchange": {
|
||||
"message": "분산형 교환"
|
||||
"message": "탈중앙화 거래소"
|
||||
},
|
||||
"swapDirectContract": {
|
||||
"message": "직접 계약"
|
||||
@ -3700,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 수수료"
|
||||
@ -3746,7 +3715,7 @@
|
||||
"message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수가 만족스러운지 확인하시기 바랍니다."
|
||||
},
|
||||
"swapPriceUnavailableTitle": {
|
||||
"message": "진행하기 전에 요율 확인"
|
||||
"message": "진행하기 전에 비율 확인"
|
||||
},
|
||||
"swapProcessing": {
|
||||
"message": "처리 중"
|
||||
@ -3758,25 +3727,25 @@
|
||||
"message": "견적 소스"
|
||||
},
|
||||
"swapQuotesExpiredErrorDescription": {
|
||||
"message": "새 견적을 요청해 최신 요율을 확인하세요."
|
||||
"message": "새 견적을 요청해 최신 비율을 확인하세요."
|
||||
},
|
||||
"swapQuotesExpiredErrorTitle": {
|
||||
"message": "견적 시간 초과"
|
||||
},
|
||||
"swapQuotesNotAvailableErrorDescription": {
|
||||
"message": "금액 또는 슬리패지 설정을 조정한 후 다시 시도해 보세요."
|
||||
"message": "금액 또는 슬리피지 설정을 조정한 후 다시 시도해 보세요."
|
||||
},
|
||||
"swapQuotesNotAvailableErrorTitle": {
|
||||
"message": "사용 가능한 견적 없음"
|
||||
},
|
||||
"swapRate": {
|
||||
"message": "요율"
|
||||
"message": "비율"
|
||||
},
|
||||
"swapReceiving": {
|
||||
"message": "수신 중"
|
||||
},
|
||||
"swapReceivingInfoTooltip": {
|
||||
"message": "이것은 예상치입니다. 정확한 금액은 슬리패지에 따라 달라집니다."
|
||||
"message": "이것은 예상치입니다. 정확한 금액은 슬리피지에 따라 달라집니다."
|
||||
},
|
||||
"swapRequestForQuotation": {
|
||||
"message": "견적 요청"
|
||||
@ -3800,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": "제안 스왑"
|
||||
@ -3881,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": "슬리피지 허용치"
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "유의 사항:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "다음에 기반한 정보입니다: "
|
||||
},
|
||||
"time": {
|
||||
"message": "시간"
|
||||
},
|
||||
@ -4294,7 +4260,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": {
|
||||
|
34
app/_locales/pt/messages.json
generated
@ -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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,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"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Informações importantes:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Isso se baseia em informações de "
|
||||
},
|
||||
"time": {
|
||||
"message": "Hora"
|
||||
},
|
||||
|
12
app/_locales/pt_BR/messages.json
generated
@ -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."
|
||||
|
34
app/_locales/ru/messages.json
generated
@ -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."
|
||||
@ -3227,10 +3211,6 @@
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "Совпадений не найдено."
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
|
||||
},
|
||||
"show": {
|
||||
"message": "Показать"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "Управление вашим снапами"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "Статус снапа зависит от активности."
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "Снап будет работать только в том случае, если он включен"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Что нужно помнить:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Это основано на информации от "
|
||||
},
|
||||
"time": {
|
||||
"message": "Время"
|
||||
},
|
||||
|
34
app/_locales/tl/messages.json
generated
@ -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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,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"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Mga bagay na dapat tandaan:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Ito ay batay sa impormasyon mula sa "
|
||||
},
|
||||
"time": {
|
||||
"message": "Oras"
|
||||
},
|
||||
|
34
app/_locales/tr/messages.json
generated
@ -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."
|
||||
@ -3227,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"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,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"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Unutulmaması gerekenler:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Bu, şu kaynaktan alınan bilgilere dayanır: "
|
||||
},
|
||||
"time": {
|
||||
"message": "Zaman"
|
||||
},
|
||||
|
34
app/_locales/vi/messages.json
generated
@ -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."
|
||||
@ -3227,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ị"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,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"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "Những điều cần lưu ý:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "Điều này dựa trên thông tin từ "
|
||||
},
|
||||
"time": {
|
||||
"message": "Thời gian"
|
||||
},
|
||||
|
36
app/_locales/zh_CN/messages.json
generated
@ -427,7 +427,7 @@
|
||||
"message": "设置 MetaMask 将被锁定前的空闲时间(单位:分钟)。"
|
||||
},
|
||||
"average": {
|
||||
"message": "平均值"
|
||||
"message": "中等"
|
||||
},
|
||||
"back": {
|
||||
"message": "返回"
|
||||
@ -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."
|
||||
@ -3227,10 +3211,6 @@
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "没有找到匹配的结果."
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
"description": "$1 is replaced by a version string (e.g. 1.2.3)"
|
||||
},
|
||||
"show": {
|
||||
"message": "显示"
|
||||
},
|
||||
@ -3304,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."
|
||||
@ -3348,9 +3320,6 @@
|
||||
"snapsSettingsDescription": {
|
||||
"message": "管理您的Snap"
|
||||
},
|
||||
"snapsStatus": {
|
||||
"message": "Snap状态取决于活动。"
|
||||
},
|
||||
"snapsToggle": {
|
||||
"message": "Snap仅在启用后才会运行"
|
||||
},
|
||||
@ -3951,9 +3920,6 @@
|
||||
"thingsToKeep": {
|
||||
"message": "注意事项:"
|
||||
},
|
||||
"thisIsBasedOn": {
|
||||
"message": "所根据的信息是来自"
|
||||
},
|
||||
"time": {
|
||||
"message": "时间"
|
||||
},
|
||||
|
14
app/build-types/mmi/images/icons/stake.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_735_24127)">
|
||||
<path
|
||||
d="M3.99902 19C7.24796 17.5 8.87242 16.75 10.4969 16.75M16.9948 19C13.7458 17.5 12.1214 16.75 10.4969 16.75M10.4969 16.75V11.5M10.4969 11.5L10 10.4091M10.4969 11.5V9.5L10.9967 8.5M10 10.4091C10 10.4091 5.00889 11.0985 2.99935 9.5C1.29118 8.14126 1 4.5 1 4.5C1 4.5 5.55008 3.95155 7.54545 5.90909C8.91802 7.25563 10 10.4091 10 10.4091ZM10.9967 8.5C10.9967 8.5 11.5374 4.11404 13.4959 2.5C15.2137 1.08439 18.9941 1 18.9941 1C18.9941 1 19.1777 5.2683 17.4946 7C15.6792 8.86783 10.9967 8.5 10.9967 8.5Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_735_24127">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 873 B |
1
app/images/icons/ban.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m43 256c0-118 95-213 213-213 118 0 213 95 213 213 0 118-95 213-213 213-118 0-213-95-213-213z m77-109c-24 30-39 68-39 109 0 96 79 175 175 175 41 0 79-15 109-39z m27-27l245 245c24-30 39-68 39-109 0-96-79-175-175-175-41 0-79 15-109 39z"/></svg>
|
After Width: | Height: | Size: 313 B |
1
app/images/icons/bold.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m107 85c0-11 9-21 21-21l171 0c28 0 55 11 75 31 20 20 31 47 31 76 0 28-11 55-31 75-1 1-1 1-2 2 8 5 16 11 23 18 20 20 32 47 32 75 0 29-12 56-32 76-20 20-47 31-75 31l-192 0c-12 0-21-10-21-21z m42 150l150 0c17 0 33-7 45-19 12-12 19-28 19-45 0-17-7-34-19-46-12-12-28-18-45-18l-150 0z m0 42l0 128 171 0c17 0 33-6 45-18 12-12 19-29 19-46 0-17-7-33-19-45-12-12-28-19-45-19z"/></svg>
|
After Width: | Height: | Size: 446 B |
1
app/images/icons/circle-x.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m256 43c-118 0-213 95-213 213 0 118 95 213 213 213 118 0 213-95 213-213 0-118-95-213-213-213z m73 140c8 9 8 22 0 30l-43 43 43 43c8 8 8 21 0 30-9 8-22 8-30 0l-43-43-43 43c-8 8-21 8-30 0-8-9-8-22 0-30l43-43-43-43c-8-8-8-21 0-30 9-8 22-8 30 0l43 43 43-43c8-8 21-8 30 0z"/></svg>
|
After Width: | Height: | Size: 347 B |
1
app/images/icons/download.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m277 64c0-12-9-21-21-21-12 0-21 9-21 21l0 204-71-70c-8-8-21-8-30 0-8 9-8 22 0 30l107 107c0 0 0 0 0 0 2 2 4 4 7 5 2 1 5 1 8 1 0 0 0 0 0 0 0 0 0 0 0 0 6 0 11-2 15-6m0 0l107-107c8-8 8-21 0-30-9-8-22-8-30 0l-71 70 0-204m-213 235c12 0 21 9 21 21l0 85c0 6 3 11 7 15 4 4 9 7 15 7l298 0c6 0 11-3 15-7 4-4 7-9 7-15l0-85c0-12 9-21 21-21 12 0 21 9 21 21l0 85c0 17-6 34-18 46-12 12-29 18-46 18l-298 0c-17 0-34-6-46-18-12-12-18-29-18-46l0-85c0-12 9-21 21-21z"/></svg>
|
After Width: | Height: | Size: 526 B |
1
app/images/icons/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m276 43c9 0 18 8 18 18l0 118 118 0c10 0 18 8 18 18 0 10-8 18-18 18l-136 0c-10 0-18-8-18-18l0-136c0-10 8-18 18-18z m-178 16c11-10 26-16 41-16l137 0c4 0 9 2 12 5l137 137c3 3 5 8 5 12l0 215c0 15-6 30-16 41-11 10-26 16-41 16l-234 0c-15 0-30-6-41-16-10-11-16-26-16-41l0-312c0-15 6-30 16-41z m41 20c-6 0-11 2-15 6-4 4-6 9-6 15l0 312c0 6 2 11 6 15 4 4 9 6 15 6l234 0c6 0 11-2 15-6 4-4 6-9 6-15l0-207-126-126z"/></svg>
|
After Width: | Height: | Size: 482 B |
1
app/images/icons/flask.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m434 379l-98-157 0-126 7 0c11 0 20-9 20-20l0-13c0-11-9-20-20-20l-174 0c-11 0-20 9-20 20l0 13c0 11 9 20 20 20l7 0 0 126-98 157c-24 39 4 90 50 90l256 0c46 0 74-51 50-90z m-250-70l40-64c3-5 5-10 5-16l0-133 54 0 0 133c0 6 1 11 4 16l41 64z"/></svg>
|
After Width: | Height: | Size: 315 B |
1
app/images/icons/plug.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m309 163l0-94c0-14 12-26 27-26 15 0 27 12 27 26l0 94z m94 13l-294 0c-7 0-13 6-13 13l0 27c0 7 6 13 13 13l14 0 0 27c0 65 45 118 106 131l0 82 54 0 0-82c61-13 106-66 106-131l0-27 14 0c7 0 13-6 13-13l0-27c0-7-6-13-13-13z m-200-13l0-94c0-14-12-26-27-26-15 0-27 12-27 26l0 94z"/></svg>
|
After Width: | Height: | Size: 350 B |
1
app/images/icons/qr-code.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m64 229l165 0 0-165-165 0z m55-110l55 0 0 55-55 0z m164-55l0 165 165 0 0-165z m110 110l-55 0 0-55 55 0z m-329 274l165 0 0-165-165 0z m55-110l55 0 0 55-55 0z m302-55l27 0 0 110-82 0 0-27-28 0 0 82-55 0 0-165 83 0 0 28 55 0z m0 138l27 0 0 27-27 0z m-55 0l27 0 0 27-27 0z"/></svg>
|
After Width: | Height: | Size: 349 B |
1
app/images/icons/share.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m329 188c1-12 12-20 24-19 41 3 75 16 97 44 22 27 30 64 30 109l0 3c0 50-10 90-37 118-27 27-68 37-118 37l-139 0c-49 0-90-10-117-37-28-28-37-68-37-118l0-3c0-44 8-82 29-109 22-27 55-40 96-44 12-1 23 7 24 19 1 12-8 22-20 23-34 4-54 14-67 29-12 16-20 41-20 82l0 3c0 46 10 72 25 88 16 15 42 24 87 24l139 0c46 0 72-9 88-24 15-16 25-42 25-88l0-3c0-41-8-66-21-82-12-16-33-26-68-29-12-1-21-11-20-23z m-58-150c-8-8-22-8-30 0l-72 72c-8 8-8 22 0 30 9 8 22 8 31 0l35-35 0 215c0 12 9 21 21 21 12 0 21-9 21-21l0-215 35 35c9 8 22 8 31 0 8-8 8-22 0-30z"/></svg>
|
After Width: | Height: | Size: 614 B |
1
app/images/icons/square.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m107 85c-12 0-22 10-22 22l0 298c0 12 10 22 22 22l298 0c12 0 22-10 22-22l0-298c0-12-10-22-22-22z m-64 22c0-36 28-64 64-64l298 0c36 0 64 28 64 64l0 298c0 36-28 64-64 64l-298 0c-36 0-64-28-64-64z"/></svg>
|
After Width: | Height: | Size: 273 B |
1
app/images/icons/tint.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path d="m256 43c6 0 11 2 15 6l112 112c0 0 0 0 0 0 26 26 43 58 50 93 7 35 3 71-10 104-14 33-37 61-67 81-29 20-64 30-100 30-36 0-71-10-100-30-30-20-53-48-67-81-13-33-17-69-10-104 7-35 24-67 50-93 0 0 0 0 0 0l112-112c4-4 9-6 15-6z m0 51l-97 98c-20 19-33 43-38 70-5 27-3 54 8 80 10 25 28 46 50 61 23 16 50 24 77 24 27 0 54-8 77-24 22-15 40-36 50-61 11-26 13-53 8-80-5-27-18-51-38-70z"/></svg>
|
After Width: | Height: | Size: 454 B |
1
app/images/icons/twitter.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m299 85c19-8 40-9 59-4 15 4 29 11 41 21 13-5 25-12 37-20 7-5 17-5 24 0 7 5 11 14 9 23-6 24-18 47-35 66 0 2 0 5 0 7l0 0c0 108-52 187-127 227-74 40-169 39-253-8-9-4-13-14-11-24 3-9 12-16 22-15 26 1 52-4 77-14-24-16-41-34-54-54-18-28-24-58-26-85-1-26 3-50 8-68 2-8 4-15 6-20 1-3 2-5 2-6 1-1 1-1 1-2l0 0 0-1 0 0 0 0c0 0 0 0 20 9l-20-9c4-7 10-11 18-12 7-1 15 2 19 9 16 22 61 52 61 52 0 0 38 15 58 17 0-19 6-37 17-52 11-17 28-30 47-37z"/></svg>
|
After Width: | Height: | Size: 510 B |
1
app/images/icons/upload.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m263 65c3 1 6 3 8 5l95 95c8 8 8 22 0 30-8 9-22 9-30 0l-59-58 0 176c0 12-9 21-21 21-12 0-21-9-21-21l0-176-59 58c-8 9-22 9-30 0-8-8-8-22 0-30l95-95c0 0 1-1 2-2 1 0 2-1 4-2 2-1 6-2 9-2m0 0c3 0 5 0 7 1z m-171 228c12 0 22 9 22 21l0 76c0 4 1 8 5 11 3 4 7 5 11 5l266 0c4 0 8-1 11-5 4-3 5-7 5-11l0-76c0-12 10-21 22-21 11 0 21 9 21 21l0 76c0 15-6 31-17 42-11 11-27 17-42 17l-266 0c-15 0-31-6-42-17-11-11-17-27-17-42l0-76c0-12 10-21 21-21z"/></svg>
|
After Width: | Height: | Size: 510 B |
1
app/images/icons/usb.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m469 256c0 2-1 4-3 5l-59 36c-1 0-2 0-3 0-1 0-2 0-3 0-2-1-3-3-3-5l0-24-159 0c17 26 27 71 47 71l17 0 0-18c0-3 3-6 6-6l60 0c3 0 5 3 5 6l0 59c0 4-2 6-5 6l-60 0c-3 0-6-2-6-6l0-17-17 0c-51 0-54-95-83-95l-67 0c-5 20-24 35-46 35-26 0-47-21-47-47 0-26 21-47 47-47 22 0 41 15 46 35 26 0 29 6 50-40 26-59 38-55 72-55 5-14 18-23 34-23 19 0 35 16 35 35 0 20-16 36-35 36-16 0-29-10-34-24l-20 0c-19 0-29 45-46 71l206 0 0-23c0-3 1-4 3-6 2-1 4-1 6 1l59 35c2 1 3 3 3 5z"/></svg>
|
After Width: | Height: | Size: 532 B |
1
app/images/icons/user-check.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m279 169c0 46-38 83-84 83-46 0-84-37-84-83 0-46 38-84 84-84 46 0 84 38 84 84z m-236 249c0-69 68-125 152-125 84 0 152 56 152 125 0 5-3 9-8 9l-288 0c-5 0-8-4-8-9z m420-200c8-9 8-22 0-30-8-9-22-9-30 0l-55 56-21-21c-8-8-21-8-30 0-8 8-8 22 0 30l36 36c8 8 21 8 30 0z"/></svg>
|
After Width: | Height: | Size: 341 B |
1
app/images/icons/wifi.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m257 232c-42 0-82 15-114 41-9 8-23 7-30-2-8-10-7-23 2-31 40-33 90-51 142-51 51 0 101 18 141 51 9 8 10 21 3 31-8 9-21 10-30 2-32-26-73-41-114-41z m-1-91c-66 0-129 24-178 67-9 8-22 7-30-1-8-9-7-23 2-31 57-50 130-77 206-77 76 0 149 27 206 77 9 8 10 22 2 31-8 8-21 9-30 1-49-43-113-67-178-67z m0 182c-18 0-36 5-51 16-9 7-23 4-29-5-7-10-5-23 5-30 22-16 48-24 75-24 27 0 53 8 76 24 9 7 11 20 5 30-7 9-21 12-30 5-15-11-33-16-51-16z m-21 69c0-12 9-21 21-21l0 0c12 0 21 9 21 21 0 12-9 21-21 21l0 0c-12 0-21-9-21-21z"/></svg>
|
After Width: | Height: | Size: 587 B |
@ -18,11 +18,10 @@ 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';
|
||||
import { SECOND } from '../../shared/constants/time';
|
||||
import {
|
||||
REJECT_NOTIFICATION_CLOSE,
|
||||
REJECT_NOTIFICATION_CLOSE_SIG,
|
||||
@ -33,6 +32,7 @@ import {
|
||||
import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils';
|
||||
import { isManifestV3 } from '../../shared/modules/mv3.utils';
|
||||
import { maskObject } from '../../shared/modules/object.utils';
|
||||
import { getEnvironmentType, deferredPromise, getPlatform } from './lib/util';
|
||||
import migrations from './migrations';
|
||||
import Migrator from './lib/migrator';
|
||||
import ExtensionPlatform from './platforms/extension';
|
||||
@ -51,12 +51,11 @@ import rawFirstTimeState from './first-time-state';
|
||||
import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code';
|
||||
import getObjStructure from './lib/getObjStructure';
|
||||
import setupEnsIpfsResolver from './lib/ens-ipfs/setup';
|
||||
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,
|
||||
@ -85,6 +84,7 @@ let popupIsOpen = false;
|
||||
let notificationIsOpen = false;
|
||||
let uiIsTriggering = false;
|
||||
const openMetamaskTabsIDs = {};
|
||||
const openMetamaskConnections = new Map();
|
||||
const requestAccountTabIds = {};
|
||||
let controller;
|
||||
|
||||
@ -106,7 +106,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',
|
||||
@ -185,10 +185,28 @@ let connectExternal;
|
||||
browser.runtime.onConnect.addListener(async (...args) => {
|
||||
// Queue up connection attempts here, waiting until after initialization
|
||||
await isInitialized;
|
||||
const remotePort = args[0];
|
||||
const { sender } = remotePort;
|
||||
|
||||
// This is set in `setupController`, which is called as part of initialization
|
||||
connectRemote(...args);
|
||||
const url = sender?.url;
|
||||
const detectedProcessName = url ? getEnvironmentType(url) : '';
|
||||
|
||||
const connectionId = generateConnectionId(remotePort, detectedProcessName);
|
||||
const openConnections = openMetamaskConnections.get(connectionId) || 0;
|
||||
|
||||
if (
|
||||
openConnections === 0 ||
|
||||
(detectedProcessName === 'background' && openConnections < 2)
|
||||
// 2 background connections are allowed, one for phishing warning page and one for the ledger bridge keyring
|
||||
) {
|
||||
// This is set in `setupController`, which is called as part of initialization
|
||||
connectRemote(...args);
|
||||
openMetamaskConnections.set(connectionId, openConnections + 1);
|
||||
} else {
|
||||
throw new Error('CONNECTION_ALREADY_EXISTS');
|
||||
}
|
||||
});
|
||||
|
||||
browser.runtime.onConnectExternal.addListener(async (...args) => {
|
||||
// Queue up connection attempts here, waiting until after initialization
|
||||
await isInitialized;
|
||||
@ -220,9 +238,9 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
|
||||
* @property {object} featureFlags - An object for optional feature flags.
|
||||
* @property {boolean} welcomeScreen - True if welcome screen should be shown.
|
||||
* @property {string} currentLocale - A locale string matching the user's preferred display language.
|
||||
* @property {object} provider - The current selected network provider.
|
||||
* @property {string} provider.rpcUrl - The address for the RPC API, if using an RPC API.
|
||||
* @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks.
|
||||
* @property {object} providerConfig - The current selected network provider.
|
||||
* @property {string} providerConfig.rpcUrl - The address for the RPC API, if using an RPC API.
|
||||
* @property {string} providerConfig.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks.
|
||||
* @property {string} networkId - The stringified number of the current network ID.
|
||||
* @property {string} networkStatus - Either "unknown", "available", "unavailable", or "blocked", depending on the status of the currently selected network.
|
||||
* @property {object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values.
|
||||
@ -264,7 +282,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
|
||||
|
||||
@ -417,6 +435,21 @@ export async function loadStateFromPersistence() {
|
||||
return versionedData.data;
|
||||
}
|
||||
|
||||
function generateConnectionId(remotePort, detectedProcessName) {
|
||||
const { sender } = remotePort;
|
||||
const id = sender?.tab ? sender.tab.id : sender?.id;
|
||||
if (!id || !detectedProcessName) {
|
||||
console.error(
|
||||
'Must provide id and detectedProcessName to generate connection id.',
|
||||
id,
|
||||
detectedProcessName,
|
||||
); // eslint-disable-line no-console
|
||||
throw new Error(
|
||||
'Must provide id and detectedProcessName to generate connection id.',
|
||||
);
|
||||
}
|
||||
return `${id}-${detectedProcessName}`;
|
||||
}
|
||||
/**
|
||||
* Initializes the MetaMask Controller with any initial state and default language.
|
||||
* Configures platform-specific error reporting strategy.
|
||||
@ -442,7 +475,6 @@ export function setupController(
|
||||
infuraProjectId: process.env.INFURA_PROJECT_ID,
|
||||
// User confirmation callbacks:
|
||||
showUserConfirmation: triggerUi,
|
||||
openPopup,
|
||||
// initial state
|
||||
initState,
|
||||
// initial locale code
|
||||
@ -464,7 +496,7 @@ export function setupController(
|
||||
|
||||
setupEnsIpfsResolver({
|
||||
getCurrentChainId: () =>
|
||||
controller.networkController.store.getState().provider.chainId,
|
||||
controller.networkController.store.getState().providerConfig.chainId,
|
||||
getIpfsGateway: controller.preferencesController.getIpfsGateway.bind(
|
||||
controller.preferencesController,
|
||||
),
|
||||
@ -527,7 +559,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?.()
|
||||
@ -572,7 +604,6 @@ export function setupController(
|
||||
// communication with popup
|
||||
controller.isClientOpen = true;
|
||||
controller.setupTrustedCommunication(portStream, remotePort.sender);
|
||||
|
||||
if (isManifestV3) {
|
||||
// If we get a WORKER_KEEP_ALIVE message, we respond with an ACK
|
||||
remotePort.onMessage.addListener((message) => {
|
||||
@ -587,9 +618,11 @@ export function setupController(
|
||||
});
|
||||
}
|
||||
|
||||
const connectionId = generateConnectionId(remotePort, processName);
|
||||
if (processName === ENVIRONMENT_TYPE_POPUP) {
|
||||
popupIsOpen = true;
|
||||
endOfStream(portStream, () => {
|
||||
openMetamaskConnections.set(connectionId, 0);
|
||||
popupIsOpen = false;
|
||||
const isClientOpen = isClientOpenStatus();
|
||||
controller.isClientOpen = isClientOpen;
|
||||
@ -599,8 +632,8 @@ export function setupController(
|
||||
|
||||
if (processName === ENVIRONMENT_TYPE_NOTIFICATION) {
|
||||
notificationIsOpen = true;
|
||||
|
||||
endOfStream(portStream, () => {
|
||||
openMetamaskConnections.set(connectionId, 0);
|
||||
notificationIsOpen = false;
|
||||
const isClientOpen = isClientOpenStatus();
|
||||
controller.isClientOpen = isClientOpen;
|
||||
@ -616,6 +649,7 @@ export function setupController(
|
||||
openMetamaskTabsIDs[tabId] = true;
|
||||
|
||||
endOfStream(portStream, () => {
|
||||
openMetamaskConnections.set(connectionId, 0);
|
||||
delete openMetamaskTabsIDs[tabId];
|
||||
const isClientOpen = isClientOpenStatus();
|
||||
controller.isClientOpen = isClientOpen;
|
||||
@ -653,7 +687,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?.()
|
||||
@ -684,7 +718,7 @@ export function setupController(
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
controller.decryptMessageManager.on(
|
||||
controller.decryptMessageController.hub.on(
|
||||
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
|
||||
updateBadge,
|
||||
);
|
||||
@ -727,14 +761,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(
|
||||
@ -755,14 +786,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,
|
||||
);
|
||||
@ -771,7 +797,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);
|
||||
@ -793,7 +819,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);
|
||||
@ -839,22 +865,6 @@ async function triggerUi() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the browser popup for user confirmation of watchAsset
|
||||
* then it waits until user interact with the UI
|
||||
*/
|
||||
async function openPopup() {
|
||||
await triggerUi();
|
||||
await new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (!notificationIsOpen) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, SECOND);
|
||||
});
|
||||
}
|
||||
|
||||
// It adds the "App Installed" event into a queue of events, which will be tracked only after a user opts into metrics.
|
||||
const addAppInstalledEvent = () => {
|
||||
if (controller) {
|
||||
|
@ -46,6 +46,7 @@ export default class AppStateController extends EventEmitter {
|
||||
nftsDetectionNoticeDismissed: false,
|
||||
showTestnetMessageInDropdown: true,
|
||||
showBetaHeader: isBeta(),
|
||||
showProductTour: true,
|
||||
trezorModel: null,
|
||||
currentPopupId: undefined,
|
||||
...initState,
|
||||
@ -331,6 +332,15 @@ export default class AppStateController extends EventEmitter {
|
||||
this.store.updateState({ showBetaHeader });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the product tour should be shown
|
||||
*
|
||||
* @param showProductTour
|
||||
*/
|
||||
setShowProductTour(showProductTour) {
|
||||
this.store.updateState({ showProductTour });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a property indicating the model of the user's Trezor hardware wallet
|
||||
*
|
||||
|
222
app/scripts/controllers/decrypt-message.test.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
394
app/scripts/controllers/decrypt-message.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -209,7 +209,7 @@ export default class DetectTokensController {
|
||||
}
|
||||
|
||||
getChainIdFromNetworkStore(network) {
|
||||
return network?.store.getState().provider.chainId;
|
||||
return network?.store.getState().providerConfig.chainId;
|
||||
}
|
||||
|
||||
/* eslint-disable accessor-pairs */
|
||||
|
@ -235,7 +235,7 @@ describe('DetectTokensController', function () {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
...networkState.providerConfig,
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -254,8 +254,10 @@ describe('DetectTokensController', function () {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
chainId: convertHexToDecimal(networkState.provider.chainId),
|
||||
...networkState.providerConfig,
|
||||
chainId: convertHexToDecimal(
|
||||
networkState.providerConfig.chainId,
|
||||
),
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -65,7 +65,7 @@ const DEFAULT_PAGE_PROPERTIES = {
|
||||
|
||||
function getMockNetworkController() {
|
||||
let state = {
|
||||
provider: {
|
||||
providerConfig: {
|
||||
type: NETWORK_TYPES.GOERLI,
|
||||
chainId: FAKE_CHAIN_ID,
|
||||
},
|
||||
@ -134,7 +134,7 @@ function getMetaMetricsController({
|
||||
return new MetaMetricsController({
|
||||
segment: segmentInstance || segment,
|
||||
getCurrentChainId: () =>
|
||||
networkController.store.getState().provider.chainId,
|
||||
networkController.store.getState().providerConfig.chainId,
|
||||
onNetworkDidChange:
|
||||
networkController.onNetworkDidChange.bind(networkController),
|
||||
preferencesStore,
|
||||
@ -203,7 +203,7 @@ describe('MetaMetricsController', function () {
|
||||
networkController,
|
||||
});
|
||||
networkController.store.updateState({
|
||||
provider: {
|
||||
providerConfig: {
|
||||
type: 'NEW_NETWORK',
|
||||
chainId: '0xaab',
|
||||
},
|
||||
|
@ -266,7 +266,7 @@ type NetworkConfigurations = Record<
|
||||
* The state that NetworkController holds after combining its individual stores.
|
||||
*/
|
||||
export type NetworkControllerState = {
|
||||
provider: ProviderConfiguration;
|
||||
providerConfig: ProviderConfiguration;
|
||||
networkId: NetworkIdState;
|
||||
networkStatus: NetworkStatus;
|
||||
networkDetails: NetworkDetails;
|
||||
@ -279,7 +279,7 @@ export type NetworkControllerState = {
|
||||
export type NetworkControllerOptions = {
|
||||
messenger: NetworkControllerMessenger;
|
||||
state?: {
|
||||
provider?: ProviderConfiguration;
|
||||
providerConfig?: ProviderConfiguration;
|
||||
networkDetails?: NetworkDetails;
|
||||
networkConfigurations?: NetworkConfigurations;
|
||||
};
|
||||
@ -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,
|
||||
@ -386,7 +386,7 @@ function buildDefaultNetworkConfigurationsState(): NetworkConfigurations {
|
||||
*/
|
||||
function buildDefaultState() {
|
||||
return {
|
||||
provider: buildDefaultProviderConfigState(),
|
||||
providerConfig: buildDefaultProviderConfigState(),
|
||||
networkId: buildDefaultNetworkIdState(),
|
||||
networkStatus: buildDefaultNetworkStatusState(),
|
||||
networkDetails: buildDefaultNetworkDetailsState(),
|
||||
@ -477,7 +477,7 @@ export class NetworkController extends EventEmitter {
|
||||
...buildDefaultState(),
|
||||
...state,
|
||||
});
|
||||
this.#previousProviderConfig = this.store.getState().provider;
|
||||
this.#previousProviderConfig = this.store.getState().providerConfig;
|
||||
|
||||
// provider and block tracker
|
||||
this.#provider = null;
|
||||
@ -508,7 +508,7 @@ export class NetworkController extends EventEmitter {
|
||||
* using the provider to gather details about the network.
|
||||
*/
|
||||
async initializeProvider(): Promise<void> {
|
||||
const { type, rpcUrl, chainId } = this.store.getState().provider;
|
||||
const { type, rpcUrl, chainId } = this.store.getState().providerConfig;
|
||||
this.#configureProvider({ type, rpcUrl, chainId });
|
||||
await this.lookupNetwork();
|
||||
}
|
||||
@ -575,7 +575,7 @@ export class NetworkController extends EventEmitter {
|
||||
* blocking requests, or if the network is not Infura-supported.
|
||||
*/
|
||||
async lookupNetwork(): Promise<void> {
|
||||
const { chainId, type } = this.store.getState().provider;
|
||||
const { chainId, type } = this.store.getState().providerConfig;
|
||||
const { provider } = this.getProviderAndBlockTracker();
|
||||
let networkChanged = false;
|
||||
let networkId: NetworkIdState = null;
|
||||
@ -754,7 +754,7 @@ export class NetworkController extends EventEmitter {
|
||||
* Re-initializes the provider and block tracker for the current network.
|
||||
*/
|
||||
async resetConnection() {
|
||||
await this.#setProviderConfig(this.store.getState().provider);
|
||||
await this.#setProviderConfig(this.store.getState().providerConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -765,7 +765,7 @@ export class NetworkController extends EventEmitter {
|
||||
async rollbackToPreviousProvider() {
|
||||
const config = this.#previousProviderConfig;
|
||||
this.store.updateState({
|
||||
provider: config,
|
||||
providerConfig: config,
|
||||
});
|
||||
await this.#switchNetwork(config);
|
||||
}
|
||||
@ -850,8 +850,8 @@ export class NetworkController extends EventEmitter {
|
||||
* @param providerConfig - The provider configuration.
|
||||
*/
|
||||
async #setProviderConfig(providerConfig: ProviderConfiguration) {
|
||||
this.#previousProviderConfig = this.store.getState().provider;
|
||||
this.store.updateState({ provider: providerConfig });
|
||||
this.#previousProviderConfig = this.store.getState().providerConfig;
|
||||
this.store.updateState({ providerConfig });
|
||||
await this.#switchNetwork(providerConfig);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
EndowmentPermissions,
|
||||
RestrictedMethods,
|
||||
ExcludedSnapEndowments,
|
||||
} from '../../../../../shared/constants/permissions';
|
||||
import {
|
||||
buildSnapEndowmentSpecifications,
|
||||
@ -39,6 +40,13 @@ describe('buildSnapEndowmentSpecifications', () => {
|
||||
it('creates valid permission specification objects', () => {
|
||||
expect(
|
||||
Object.keys(buildSnapEndowmentSpecifications()).sort(),
|
||||
).toStrictEqual(Object.keys(EndowmentPermissions).sort());
|
||||
).toStrictEqual(
|
||||
Object.keys(EndowmentPermissions)
|
||||
.filter(
|
||||
(targetKey) =>
|
||||
!Object.keys(ExcludedSnapEndowments).includes(targetKey),
|
||||
)
|
||||
.sort(),
|
||||
);
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
import { handlers as permittedSnapMethods } from '@metamask/rpc-methods/dist/permitted';
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
import { permittedMethods as permittedSnapMethods } from '@metamask/rpc-methods';
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
import { permissionRpcMethods } from '@metamask/permission-controller';
|
||||
import { selectHooks } from '@metamask/rpc-methods/dist/utils';
|
||||
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
@ -57,7 +56,7 @@ export const SENTRY_STATE = {
|
||||
nextNonce: true,
|
||||
participateInMetaMetrics: true,
|
||||
preferences: true,
|
||||
provider: {
|
||||
providerConfig: {
|
||||
nickname: true,
|
||||
ticker: true,
|
||||
type: true,
|
||||
@ -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}`;
|
||||
|
||||
|
@ -6,6 +6,7 @@ try {
|
||||
errorTaming: 'unsafe',
|
||||
mathTaming: 'unsafe',
|
||||
dateTaming: 'unsafe',
|
||||
domainTaming: 'unsafe',
|
||||
overrideTaming: 'severe',
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -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
|
||||
@ -287,7 +285,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
|
||||
this.tokenListController = new TokenListController({
|
||||
chainId: hexToDecimal(
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
),
|
||||
preventPollingOnNetworkRestart: initState.TokenListController
|
||||
? initState.TokenListController.preventPollingOnNetworkRestart
|
||||
@ -297,8 +295,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
chainId: hexToDecimal(networkState.provider.chainId),
|
||||
...networkState.providerConfig,
|
||||
chainId: hexToDecimal(networkState.providerConfig.chainId),
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -332,13 +330,21 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
...networkState.providerConfig,
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
}),
|
||||
config: { provider: this.provider },
|
||||
state: initState.TokensController,
|
||||
messenger: this.controllerMessenger.getRestricted({
|
||||
name: 'TokensController',
|
||||
allowedActions: [
|
||||
`${this.approvalController.name}:addRequest`,
|
||||
`${this.approvalController.name}:acceptRequest`,
|
||||
`${this.approvalController.name}:rejectRequest`,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
this.assetsContractController = new AssetsContractController(
|
||||
@ -361,8 +367,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
chainId: hexToDecimal(networkState.provider.chainId),
|
||||
...networkState.providerConfig,
|
||||
chainId: hexToDecimal(networkState.providerConfig.chainId),
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -386,8 +392,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
chainId: hexToDecimal(networkState.provider.chainId),
|
||||
...networkState.providerConfig,
|
||||
chainId: hexToDecimal(networkState.providerConfig.chainId),
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -446,8 +452,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
chainId: hexToDecimal(networkState.provider.chainId),
|
||||
...networkState.providerConfig,
|
||||
chainId: hexToDecimal(networkState.providerConfig.chainId),
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -470,11 +476,11 @@ export default class MetamaskController extends EventEmitter {
|
||||
),
|
||||
getNetworkIdentifier: () => {
|
||||
const { type, rpcUrl } =
|
||||
this.networkController.store.getState().provider;
|
||||
this.networkController.store.getState().providerConfig;
|
||||
return type === NETWORK_TYPES.RPC ? rpcUrl : type;
|
||||
},
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
version: this.platform.getVersion(),
|
||||
environment: process.env.METAMASK_ENVIRONMENT,
|
||||
extension: this.extension,
|
||||
@ -516,13 +522,14 @@ export default class MetamaskController extends EventEmitter {
|
||||
legacyAPIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/gasPrices`,
|
||||
EIP1559APIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/suggestedGasFees`,
|
||||
getCurrentNetworkLegacyGasAPICompatibility: () => {
|
||||
const { chainId } = this.networkController.store.getState().provider;
|
||||
const { chainId } =
|
||||
this.networkController.store.getState().providerConfig;
|
||||
return process.env.IN_TEST || chainId === CHAIN_IDS.MAINNET;
|
||||
},
|
||||
getChainId: () => {
|
||||
return process.env.IN_TEST
|
||||
? CHAIN_IDS.MAINNET
|
||||
: this.networkController.store.getState().provider.chainId;
|
||||
: this.networkController.store.getState().providerConfig.chainId;
|
||||
},
|
||||
});
|
||||
|
||||
@ -552,7 +559,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
messenger: currencyRateMessenger,
|
||||
state: {
|
||||
...initState.CurrencyController,
|
||||
nativeCurrency: this.networkController.store.getState().provider.ticker,
|
||||
nativeCurrency:
|
||||
this.networkController.store.getState().providerConfig.ticker,
|
||||
},
|
||||
});
|
||||
|
||||
@ -592,8 +600,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
chainId: hexToDecimal(networkState.provider.chainId),
|
||||
...networkState.providerConfig,
|
||||
chainId: hexToDecimal(networkState.providerConfig.chainId),
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -626,7 +634,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.ensController = new EnsController({
|
||||
provider: this.provider,
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
|
||||
networkControllerMessenger,
|
||||
NetworkControllerEventType.NetworkDidChange,
|
||||
@ -644,7 +652,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
NetworkControllerEventType.NetworkDidChange,
|
||||
),
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
preferencesController: this.preferencesController,
|
||||
onboardingController: this.onboardingController,
|
||||
initState: initState.IncomingTransactionsController,
|
||||
@ -655,10 +663,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
provider: this.provider,
|
||||
blockTracker: this.blockTracker,
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
getNetworkIdentifier: () => {
|
||||
const { type, rpcUrl } =
|
||||
this.networkController.store.getState().provider;
|
||||
this.networkController.store.getState().providerConfig;
|
||||
return type === NETWORK_TYPES.RPC ? rpcUrl : type;
|
||||
},
|
||||
preferencesController: this.preferencesController,
|
||||
@ -695,14 +703,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.cachedBalancesController = new CachedBalancesController({
|
||||
accountTracker: this.accountTracker,
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
initState: initState.CachedBalancesController,
|
||||
});
|
||||
|
||||
this.tokensController.hub.on('pendingSuggestedAsset', async () => {
|
||||
await opts.openPopup();
|
||||
});
|
||||
|
||||
let additionalKeyrings = [keyringBuilderFactory(QRHardwareKeyring)];
|
||||
|
||||
if (this.canUseHardwareWallets()) {
|
||||
@ -782,7 +786,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
);
|
||||
},
|
||||
}),
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
...this.getSnapPermissionSpecifications(),
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
},
|
||||
@ -803,7 +807,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({
|
||||
@ -848,6 +852,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: {
|
||||
@ -859,8 +866,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
messenger: snapControllerMessenger,
|
||||
featureFlags: {
|
||||
dappsCanUpdateSnaps: true,
|
||||
allowLocalSnaps: isFlask,
|
||||
requireAllowlist: isMain,
|
||||
allowLocalSnaps,
|
||||
requireAllowlist,
|
||||
},
|
||||
});
|
||||
|
||||
@ -929,8 +936,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',
|
||||
@ -939,6 +946,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
'0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6',
|
||||
});
|
||||
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
|
||||
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
||||
this.desktopController = new DesktopController({
|
||||
initState: initState.DesktopController,
|
||||
});
|
||||
@ -979,7 +989,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
initState:
|
||||
initState.TransactionController || initState.TransactionManager,
|
||||
getPermittedAccounts: this.getPermittedAccounts.bind(this),
|
||||
getProviderConfig: () => this.networkController.store.getState().provider,
|
||||
getProviderConfig: () =>
|
||||
this.networkController.store.getState().providerConfig,
|
||||
getCurrentNetworkEIP1559Compatibility:
|
||||
this.networkController.getEIP1559Compatibility.bind(
|
||||
this.networkController,
|
||||
@ -1000,7 +1011,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
});
|
||||
},
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
preferencesStore: this.preferencesController.store,
|
||||
txHistoryLimit: 60,
|
||||
signTransaction: this.keyringController.signTransaction.bind(
|
||||
@ -1136,7 +1147,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
networkControllerMessenger.subscribe(
|
||||
NetworkControllerEventType.NetworkDidChange,
|
||||
async () => {
|
||||
const { ticker } = this.networkController.store.getState().provider;
|
||||
const { ticker } =
|
||||
this.networkController.store.getState().providerConfig;
|
||||
try {
|
||||
await this.currencyRateController.setNativeCurrency(ticker);
|
||||
} catch (error) {
|
||||
@ -1147,7 +1159,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,
|
||||
),
|
||||
@ -1195,10 +1217,11 @@ export default class MetamaskController extends EventEmitter {
|
||||
onNetworkStateChange: (listener) =>
|
||||
this.networkController.store.subscribe(listener),
|
||||
provider: this.provider,
|
||||
getProviderConfig: () => this.networkController.store.getState().provider,
|
||||
getProviderConfig: () =>
|
||||
this.networkController.store.getState().providerConfig,
|
||||
getTokenRatesState: () => this.tokenRatesController.state,
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
getEIP1559GasFeeEstimates:
|
||||
this.gasFeeController.fetchGasFeeEstimates.bind(this.gasFeeController),
|
||||
});
|
||||
@ -1209,7 +1232,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
providerConfig: {
|
||||
...networkState.provider,
|
||||
...networkState.providerConfig,
|
||||
},
|
||||
};
|
||||
return cb(modifiedNetworkState);
|
||||
@ -1247,12 +1270,12 @@ export default class MetamaskController extends EventEmitter {
|
||||
() => {
|
||||
this.txController.txStateManager.clearUnapprovedTxs();
|
||||
this.encryptionPublicKeyController.clearUnapproved();
|
||||
this.decryptMessageManager.clearUnapproved();
|
||||
this.decryptMessageController.clearUnapproved();
|
||||
this.signController.clearUnapproved();
|
||||
},
|
||||
);
|
||||
|
||||
if (isManifestV3 && globalThis.isFirstTimeProfileLoaded === false) {
|
||||
if (isManifestV3 && globalThis.isFirstTimeProfileLoaded === undefined) {
|
||||
const { serviceWorkerLastActiveTime } =
|
||||
this.appStateController.store.getState();
|
||||
const metametricsPayload = {
|
||||
@ -1312,11 +1335,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({
|
||||
@ -1338,7 +1364,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,
|
||||
@ -1370,11 +1396,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,
|
||||
@ -1404,11 +1432,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,
|
||||
@ -1420,7 +1450,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,
|
||||
),
|
||||
@ -1500,7 +1532,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) {
|
||||
@ -1513,7 +1545,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
/**
|
||||
* Constructor helper for getting Snap permission specifications.
|
||||
*/
|
||||
@ -1655,7 +1687,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`,
|
||||
@ -1756,7 +1788,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
updatePublicConfigStore(this.getState());
|
||||
|
||||
function updatePublicConfigStore(memState) {
|
||||
const { chainId } = networkController.store.getState().provider;
|
||||
const { chainId } = networkController.store.getState().providerConfig;
|
||||
if (memState.networkStatus === NetworkStatus.Available) {
|
||||
publicConfigStore.putState(selectPublicState(chainId, memState));
|
||||
}
|
||||
@ -1797,7 +1829,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
getProviderNetworkState(memState) {
|
||||
const { networkId } = memState || this.getState();
|
||||
return {
|
||||
chainId: this.networkController.store.getState().provider.chainId,
|
||||
chainId: this.networkController.store.getState().providerConfig.chainId,
|
||||
networkVersion: networkId ?? 'loading',
|
||||
};
|
||||
}
|
||||
@ -1956,6 +1988,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,
|
||||
@ -2057,6 +2093,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
),
|
||||
setShowBetaHeader:
|
||||
appStateController.setShowBetaHeader.bind(appStateController),
|
||||
setShowProductTour:
|
||||
appStateController.setShowProductTour.bind(appStateController),
|
||||
updateNftDropDownState:
|
||||
appStateController.updateNftDropDownState.bind(appStateController),
|
||||
setFirstTimeUsedNetwork:
|
||||
@ -2122,10 +2160,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:
|
||||
@ -2159,7 +2205,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,
|
||||
@ -2183,6 +2229,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,
|
||||
@ -2482,7 +2530,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
|
||||
@ -2608,7 +2656,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);
|
||||
}
|
||||
@ -2753,7 +2801,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.appStateController.setTrezorModel(model);
|
||||
}
|
||||
|
||||
keyring.network = this.networkController.store.getState().provider.type;
|
||||
keyring.network =
|
||||
this.networkController.store.getState().providerConfig.type;
|
||||
|
||||
return keyring;
|
||||
}
|
||||
@ -3141,95 +3190,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
|
||||
*/
|
||||
@ -3537,7 +3497,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;
|
||||
}
|
||||
@ -3585,7 +3545,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
/**
|
||||
* For snaps running in workers.
|
||||
*
|
||||
@ -3714,9 +3674,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
),
|
||||
|
||||
getCurrentChainId: () =>
|
||||
this.networkController.store.getState().provider.chainId,
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
getCurrentRpcUrl: () =>
|
||||
this.networkController.store.getState().provider.rpcUrl,
|
||||
this.networkController.store.getState().providerConfig.rpcUrl,
|
||||
// network configuration-related
|
||||
getNetworkConfigurations: () =>
|
||||
this.networkController.store.getState().networkConfigurations,
|
||||
@ -3743,7 +3703,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(
|
||||
@ -4241,7 +4201,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(
|
||||
@ -4308,7 +4268,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
|
||||
if (transactionSecurityCheckEnabled) {
|
||||
const chainId = Number(
|
||||
hexToDecimal(this.networkController.store.getState().provider.chainId),
|
||||
hexToDecimal(
|
||||
this.networkController.store.getState().providerConfig.chainId,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -116,7 +116,7 @@ const MAINNET_CHAIN_ID = '0x1';
|
||||
const firstTimeState = {
|
||||
config: {},
|
||||
NetworkController: {
|
||||
provider: {
|
||||
providerConfig: {
|
||||
type: NETWORK_TYPES.RPC,
|
||||
rpcUrl: ALT_MAINNET_RPC_URL,
|
||||
chainId: MAINNET_CHAIN_ID,
|
||||
|
88
app/scripts/migrations/086.test.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { migrate, version } from './086';
|
||||
|
||||
jest.mock('uuid', () => {
|
||||
const actual = jest.requireActual('uuid');
|
||||
|
||||
return {
|
||||
...actual,
|
||||
v4: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('migration #86', () => {
|
||||
it('should update the version metadata', async () => {
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 85,
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
const newStorage = await migrate(oldStorage);
|
||||
expect(newStorage.meta).toStrictEqual({
|
||||
version,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return state unaltered if there is no network controller state', async () => {
|
||||
const oldData = {
|
||||
other: 'data',
|
||||
};
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 85,
|
||||
},
|
||||
data: oldData,
|
||||
};
|
||||
|
||||
const newStorage = await migrate(oldStorage);
|
||||
expect(newStorage.data).toStrictEqual(oldData);
|
||||
});
|
||||
|
||||
it('should return state unaltered if there is no network controller provider state', async () => {
|
||||
const oldData = {
|
||||
other: 'data',
|
||||
NetworkController: {
|
||||
networkConfigurations: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
};
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 85,
|
||||
},
|
||||
data: oldData,
|
||||
};
|
||||
|
||||
const newStorage = await migrate(oldStorage);
|
||||
expect(newStorage.data).toStrictEqual(oldData);
|
||||
});
|
||||
|
||||
it('should rename the provider config state', async () => {
|
||||
const oldData = {
|
||||
other: 'data',
|
||||
NetworkController: {
|
||||
provider: {
|
||||
some: 'provider',
|
||||
},
|
||||
},
|
||||
};
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 85,
|
||||
},
|
||||
data: oldData,
|
||||
};
|
||||
|
||||
const newStorage = await migrate(oldStorage);
|
||||
expect(newStorage.data).toStrictEqual({
|
||||
other: 'data',
|
||||
NetworkController: {
|
||||
providerConfig: {
|
||||
some: 'provider',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
41
app/scripts/migrations/086.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { hasProperty, isObject } from '@metamask/utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export const version = 86;
|
||||
|
||||
/**
|
||||
* Rename network controller `provider` state to `providerConfig`.
|
||||
*
|
||||
* @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist.
|
||||
* @param originalVersionedData.meta - State metadata.
|
||||
* @param originalVersionedData.meta.version - The current state version.
|
||||
* @param originalVersionedData.data - The persisted MetaMask state, keyed by controller.
|
||||
* @returns Updated versioned MetaMask extension state.
|
||||
*/
|
||||
export async function migrate(originalVersionedData: {
|
||||
meta: { version: number };
|
||||
data: Record<string, unknown>;
|
||||
}) {
|
||||
const versionedData = cloneDeep(originalVersionedData);
|
||||
versionedData.meta.version = version;
|
||||
versionedData.data = transformState(versionedData.data);
|
||||
return versionedData;
|
||||
}
|
||||
|
||||
function transformState(state: Record<string, unknown>) {
|
||||
if (
|
||||
hasProperty(state, 'NetworkController') &&
|
||||
isObject(state.NetworkController) &&
|
||||
hasProperty(state.NetworkController, 'provider')
|
||||
) {
|
||||
const networkControllerState = state.NetworkController;
|
||||
networkControllerState.providerConfig = networkControllerState.provider;
|
||||
delete networkControllerState.provider;
|
||||
|
||||
return {
|
||||
...state,
|
||||
NetworkController: networkControllerState,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
@ -89,6 +89,7 @@ import * as m082 from './082';
|
||||
import * as m083 from './083';
|
||||
import * as m084 from './084';
|
||||
import * as m085 from './085';
|
||||
import * as m086 from './086';
|
||||
|
||||
const migrations = [
|
||||
m002,
|
||||
@ -175,6 +176,7 @@ const migrations = [
|
||||
m083,
|
||||
m084,
|
||||
m085,
|
||||
m086,
|
||||
];
|
||||
|
||||
export default migrations;
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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
@ -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
|
@ -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: 70.26,
|
||||
branches: 57.91,
|
||||
statements: 69.59,
|
||||
functions: 62.97,
|
||||
},
|
||||
transforms: {
|
||||
branches: 100,
|
||||
|
@ -1,125 +1,157 @@
|
||||
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 VARIABLES_REQUIRED_IN_PRODUCTION = {
|
||||
main: ['INFURA_PROD_PROJECT_ID', 'SEGMENT_PROD_WRITE_KEY', 'SENTRY_DSN'],
|
||||
beta: ['INFURA_BETA_PROJECT_ID', 'SEGMENT_BETA_WRITE_KEY', 'SENTRY_DSN'],
|
||||
flask: ['INFURA_FLASK_PROJECT_ID', 'SEGMENT_FLASK_WRITE_KEY', 'SENTRY_DSN'],
|
||||
};
|
||||
|
||||
const productionConfigurationPropertyNames = [
|
||||
'INFURA_BETA_PROJECT_ID',
|
||||
'INFURA_FLASK_PROJECT_ID',
|
||||
'INFURA_PROD_PROJECT_ID',
|
||||
'SEGMENT_BETA_WRITE_KEY',
|
||||
'SEGMENT_FLASK_WRITE_KEY',
|
||||
'SEGMENT_PROD_WRITE_KEY',
|
||||
'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[
|
||||
buildType
|
||||
].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,
|
||||
};
|
||||
|
@ -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(
|
||||
|
@ -10,9 +10,11 @@ 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 { TASKS, ENVIRONMENT } = require('./constants');
|
||||
const { loadBuildTypesConfig } = require('../lib/build-type');
|
||||
const { TASKS } = require('./constants');
|
||||
const {
|
||||
createTask,
|
||||
composeSeries,
|
||||
@ -25,7 +27,7 @@ const createStyleTasks = require('./styles');
|
||||
const createStaticAssetTasks = require('./static');
|
||||
const createEtcTasks = require('./etc');
|
||||
const { getBrowserVersionMap, getEnvironment } = require('./utils');
|
||||
const { getConfig, getProductionConfig } = require('./config');
|
||||
const { getConfig } = require('./config');
|
||||
const { BUILD_TARGETS } = require('./constants');
|
||||
|
||||
// Packages required dynamically via browserify configuration in dependencies
|
||||
@ -74,61 +76,67 @@ async function defineAndRunBuildTasks() {
|
||||
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'})`,
|
||||
);
|
||||
|
||||
// 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',
|
||||
'JSON',
|
||||
'Date',
|
||||
// globals sentry needs to function
|
||||
'__SENTRY__',
|
||||
'appState',
|
||||
'extra',
|
||||
'stateHooks',
|
||||
'sentryHooks',
|
||||
'sentry',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const browserPlatforms = platform ? [platform] : ['firefox', 'chrome'];
|
||||
|
||||
@ -272,9 +280,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,
|
||||
@ -354,13 +362,8 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`,
|
||||
const highLevelTasks = Object.values(BUILD_TARGETS);
|
||||
if (highLevelTasks.includes(task)) {
|
||||
const environment = getEnvironment({ buildTarget: task });
|
||||
if (environment === ENVIRONMENT.PRODUCTION) {
|
||||
// Output ignored, this is only called to ensure config is validated
|
||||
await getProductionConfig(buildType);
|
||||
} else {
|
||||
// Output ignored, this is only called to ensure config is validated
|
||||
await getConfig();
|
||||
}
|
||||
// Output ignored, this is only called to ensure config is validated
|
||||
await getConfig(buildType, environment);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -386,29 +389,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;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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');
|
||||
@ -24,15 +27,15 @@ const Sqrl = require('squirrelly');
|
||||
const lavapack = require('@lavamoat/lavapack');
|
||||
const lavamoatBrowserify = require('lavamoat-browserify');
|
||||
const terser = require('terser');
|
||||
const moduleResolver = require('babel-plugin-module-resolver');
|
||||
|
||||
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 +104,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 = variables.get('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 = variables.get('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 +232,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 +478,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 +564,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 +599,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 +794,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 +840,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 +910,11 @@ function setupBundlerDefaults(
|
||||
buildConfiguration,
|
||||
{
|
||||
buildTarget,
|
||||
buildType,
|
||||
envVars,
|
||||
ignoredFiles,
|
||||
policyOnly,
|
||||
minify,
|
||||
features,
|
||||
reloadOnChange,
|
||||
shouldLintFenceFiles,
|
||||
applyLavaMoat,
|
||||
@ -878,26 +923,36 @@ function setupBundlerDefaults(
|
||||
const { bundlerOpts } = buildConfiguration;
|
||||
const extensions = ['.js', '.ts', '.tsx'];
|
||||
|
||||
const isSnapsFlask =
|
||||
features.active.has('snaps') && features.active.has('build-flask');
|
||||
|
||||
Object.assign(bundlerOpts, {
|
||||
// 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,
|
||||
// Run TypeScript files through Babel
|
||||
{ extensions },
|
||||
],
|
||||
// Transpile libraries that use ES2020 unsupported by Chrome v78
|
||||
[
|
||||
babelify,
|
||||
{
|
||||
only: [
|
||||
'./**/node_modules/@ethereumjs/util',
|
||||
'./**/node_modules/superstruct',
|
||||
],
|
||||
global: true,
|
||||
extensions,
|
||||
plugins: isSnapsFlask
|
||||
? [
|
||||
[
|
||||
moduleResolver,
|
||||
{
|
||||
alias: {
|
||||
'@metamask/snaps-controllers':
|
||||
'@metamask/snaps-controllers-flask',
|
||||
'@metamask/snaps-ui': '@metamask/snaps-ui-flask',
|
||||
'@metamask/snaps-utils': '@metamask/snaps-utils-flask',
|
||||
'@metamask/rpc-methods': '@metamask/rpc-methods-flask',
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
// Inline `fs.readFileSync` files
|
||||
@ -1085,6 +1140,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 });
|
||||
@ -1101,56 +1161,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({
|
||||
@ -1183,6 +1242,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');
|
||||
}
|
||||
|
@ -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 });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
...[
|
||||
|
@ -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}`),
|
||||
|
@ -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
|
||||
```
|
||||
|
4
development/build/transforms/remove-fenced-code.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export type Features = {
|
||||
active: ReadonlySet<string>;
|
||||
all: ReadonlySet<string>;
|
||||
};
|
@ -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]);
|
||||
}
|
||||
|
@ -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: [
|
||||
`
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
105
development/fitness-functions/common/constants.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
15
development/fitness-functions/common/constants.ts
Normal 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 };
|
54
development/fitness-functions/common/get-diff.ts
Normal 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 };
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
40
development/fitness-functions/common/test-data.ts
Normal 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 };
|
@ -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();
|
||||
}
|
11
development/fitness-functions/index.ts
Normal 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));
|
||||
}
|
42
development/fitness-functions/rules/index.ts
Normal 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 };
|
@ -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);
|
||||
});
|
||||
});
|