From 35ac762e104d5baf44fcfd7e985ab10b2d604252 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 14 Feb 2022 16:02:51 -0800 Subject: [PATCH] Add Snaps via Flask (#13462) This PR adds `snaps` under Flask build flags to the extension. This branch is mostly equivalent to the current production version of Flask, excepting some bug fixes and tweaks. Closes #11626 --- .eslintrc.js | 4 +- .mocharc.js | 2 +- .storybook/test-data.js | 129 +++++- app/_locales/de/messages.json | 8 +- app/_locales/el/messages.json | 8 +- app/_locales/en/messages.json | 99 ++++- app/_locales/es/messages.json | 8 +- app/_locales/es_419/messages.json | 8 +- app/_locales/fr/messages.json | 8 +- app/_locales/hi/messages.json | 8 +- app/_locales/id/messages.json | 8 +- app/_locales/it/messages.json | 8 +- app/_locales/ja/messages.json | 8 +- app/_locales/ko/messages.json | 8 +- app/_locales/ph/messages.json | 8 +- app/_locales/pt_BR/messages.json | 8 +- app/_locales/ru/messages.json | 8 +- app/_locales/tl/messages.json | 8 +- app/_locales/tr/messages.json | 8 +- app/_locales/vi/messages.json | 8 +- app/_locales/zh_CN/messages.json | 8 +- .../permissions/flask/snap-permissions.js | 34 ++ .../flask/snap-permissions.test.js | 46 +++ app/scripts/controllers/permissions/index.js | 3 + .../controllers/permissions/specifications.js | 6 +- .../createMethodMiddleware.js | 43 +- .../lib/rpc-method-middleware/index.js | 2 +- app/scripts/metamask-controller.js | 387 ++++++++++++++++-- development/build/transforms/README.md | 4 +- jest.config.js | 6 +- lavamoat/browserify/beta/policy-override.json | 5 + lavamoat/browserify/beta/policy.json | 98 ++++- lavamoat/browserify/flask/policy.json | 117 +++++- lavamoat/browserify/main/policy-override.json | 5 + lavamoat/browserify/main/policy.json | 98 ++++- package.json | 6 +- shared/constants/app.js | 10 +- shared/constants/permissions.js | 17 + shared/constants/permissions.test.js | 22 + ui/components/app/app-components.scss | 7 +- .../app/flask/snap-install-warning/index.js | 1 + .../app/flask/snap-install-warning/index.scss | 30 ++ .../snap-install-warning.js | 79 ++++ .../snap-settings-card/snap-settings-card.js | 57 +-- ui/components/app/modals/index.scss | 1 - ...ission-page-container-content.component.js | 3 + .../app/permissions-connect-header/index.scss | 3 - .../permissions-connect-header.component.js | 46 ++- .../index.scss | 1 - .../permissions-connect-permission-list.js | 118 +++++- ui/helpers/constants/design-system.js | 2 +- ui/helpers/constants/routes.js | 10 + ui/helpers/utils/util.js | 16 + ui/hooks/useOriginMetadata.js | 33 +- ui/pages/confirmation/confirmation.scss | 2 +- .../templates/add-ethereum-chain.js | 1 + .../templates/flask/snap-confirm/index.scss | 10 + .../flask/snap-confirm/snap-confirm.js | 105 +++++ ui/pages/confirmation/templates/index.js | 6 + .../templates/switch-ethereum-chain.js | 1 + ui/pages/home/home.component.js | 55 ++- ui/pages/home/home.container.js | 10 + ui/pages/home/index.scss | 4 + .../flask/snap-install/index.js | 1 + .../flask/snap-install/index.scss | 28 ++ .../flask/snap-install/snap-install.js | 153 +++++++ ui/pages/permissions-connect/index.scss | 1 + .../permissions-connect.component.js | 66 ++- .../permissions-connect.container.js | 62 ++- .../settings/flask/snaps-list-tab/index.js | 1 + .../settings/flask/snaps-list-tab/index.scss | 21 + .../flask/snaps-list-tab/snap-list-tab.js | 94 +++++ .../snaps-list-tab/snap-list-tab.stories.js | 50 +++ ui/pages/settings/flask/view-snap/index.js | 1 + ui/pages/settings/flask/view-snap/index.scss | 114 ++++++ .../settings/flask/view-snap/view-snap.js | 171 ++++++++ ui/pages/settings/index.scss | 2 + ui/pages/settings/settings.component.js | 50 ++- ui/pages/settings/settings.container.js | 9 +- ui/pages/settings/settings.stories.js | 9 +- ui/selectors/permissions.js | 22 + ui/selectors/selectors.js | 46 ++- ui/store/actions.js | 27 ++ yarn.lock | 241 ++++++----- 84 files changed, 2706 insertions(+), 343 deletions(-) create mode 100644 app/scripts/controllers/permissions/flask/snap-permissions.js create mode 100644 app/scripts/controllers/permissions/flask/snap-permissions.test.js create mode 100644 shared/constants/permissions.test.js create mode 100644 ui/components/app/flask/snap-install-warning/index.js create mode 100644 ui/components/app/flask/snap-install-warning/index.scss create mode 100644 ui/components/app/flask/snap-install-warning/snap-install-warning.js create mode 100644 ui/pages/confirmation/templates/flask/snap-confirm/index.scss create mode 100644 ui/pages/confirmation/templates/flask/snap-confirm/snap-confirm.js create mode 100644 ui/pages/permissions-connect/flask/snap-install/index.js create mode 100644 ui/pages/permissions-connect/flask/snap-install/index.scss create mode 100644 ui/pages/permissions-connect/flask/snap-install/snap-install.js create mode 100644 ui/pages/settings/flask/snaps-list-tab/index.js create mode 100644 ui/pages/settings/flask/snaps-list-tab/index.scss create mode 100644 ui/pages/settings/flask/snaps-list-tab/snap-list-tab.js create mode 100644 ui/pages/settings/flask/snaps-list-tab/snap-list-tab.stories.js create mode 100644 ui/pages/settings/flask/view-snap/index.js create mode 100644 ui/pages/settings/flask/view-snap/index.scss create mode 100644 ui/pages/settings/flask/view-snap/view-snap.js diff --git a/.eslintrc.js b/.eslintrc.js index 148448172..9c0bc07e9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -171,7 +171,7 @@ module.exports = { 'app/scripts/migrations/*.test.js', 'app/scripts/platforms/*.test.js', 'app/scripts/controllers/network/**/*.test.js', - 'app/scripts/controllers/permissions/*.test.js', + 'app/scripts/controllers/permissions/**/*.test.js', ], extends: ['@metamask/eslint-config-mocha'], rules: { @@ -198,7 +198,7 @@ module.exports = { 'app/scripts/migrations/*.test.js', 'app/scripts/platforms/*.test.js', 'app/scripts/controllers/network/**/*.test.js', - 'app/scripts/controllers/permissions/*.test.js', + 'app/scripts/controllers/permissions/**/*.test.js', ], extends: ['@metamask/eslint-config-jest'], rules: { diff --git a/.mocharc.js b/.mocharc.js index f0e374691..e3904c2b1 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -6,7 +6,7 @@ module.exports = { './app/scripts/migrations/*.test.js', './app/scripts/platforms/*.test.js', './app/scripts/controllers/network/**/*.test.js', - './app/scripts/controllers/permissions/*.test.js', + './app/scripts/controllers/permissions/**/*.test.js', ], recursive: true, require: ['test/env.js', 'test/setup.js'], diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 0c5a3e68d..ba5ef0b53 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -102,6 +102,85 @@ const state = { swapsFeatureIsLive: false, swapsQuoteRefreshTime: 60000, }, + "snapStates": {}, + "snaps": { + "local:http://localhost:8080/": { + "enabled": true, + "id": "local:http://localhost:8080/", + "initialPermissions": { + "snap_confirm": {} + }, + "manifest": { + "description": "An example MetaMask Snap.", + "initialPermissions": { + "snap_confirm": {} + }, + "manifestVersion": "0.1", + "proposedName": "MetaMask Example Snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/snaps-skunkworks.git" + }, + "source": { + "location": { + "npm": { + "filePath": "dist/bundle.js", + "iconPath": "images/icon.svg", + "packageName": "@metamask/example-snap", + "registry": "https://registry.npmjs.org/" + } + }, + "shasum": "3lEt0yUu080DwV78neROaAAIQWXukSkMnP4OBhOhBnE=" + }, + "version": "0.6.0" + }, + "permissionName": "wallet_snap_local:http://localhost:8080/", + "sourceCode": "(...)", + "status": "stopped", + "svgIcon": "...", + "version": "0.6.0" + }, + "Filecoin Snap": { + "enabled": true, + "id": "npm:http://localhost:8080/", + "initialPermissions": { + "snap_confirm": {}, + "eth_accounts": {}, + "snap_manageState": {}, + }, + "manifest": { + "description": "This swap provides developers everywhere access to an entirely new data storage paradigm, even letting your programs store data autonomously. Learn more.", + "initialPermissions": { + "snap_confirm": {}, + "eth_accounts": {}, + "snap_manageState": {}, + }, + "manifestVersion": "0.1", + "proposedName": "Filecoin Snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/snaps-skunkworks.git" + }, + "source": { + "location": { + "npm": { + "filePath": "dist/bundle.js", + "iconPath": "images/icon.svg", + "packageName": "@metamask/example-snap", + "registry": "https://registry.npmjs.org/" + } + }, + "shasum": "3lEt0yUu080DwV78neROaAAIQWXukSkMnP4OBhOhBnE=" + }, + "version": "0.6.0" + }, + "permissionName": "wallet_snap_npm:http://localhost:8080/", + "sourceCode": "(...)", + "status": "stopped", + "svgIcon": "...", + "version": "0.6.0" + }, + }, accountArray: [ { name: 'This is a Really Long Account Name', @@ -1030,6 +1109,17 @@ const state = { }, }, }, + "local:http://localhost:8080/": { + permissions: { + 'snap_confirm': { + invoker: "local:http://localhost:8080/", + parentCapability: 'snap_confirm', + id: 'a7342F4b-beae-4525-a36c-c0635fd03359', + date: 1620710693178, + caveats: [] + }, + }, + }, }, permissionActivityLog: [ { @@ -1172,20 +1262,6 @@ const state = { }, }, }, - subjectMetadata: { - 'https://metamask.github.io': { - name: 'E2E Test Dapp', - origin: 'https://metamask.github.io', - iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', - subjectType: 'website', - }, - 'https://app.uniswap.org': { - name: 'Uniswap', - origin: 'https://app.uniswap.org', - iconUrl: './UNI.png', - subjectType: 'website', - }, - }, threeBoxSyncingAllowed: false, showRestorePrompt: true, threeBoxLastUpdated: 0, @@ -1212,6 +1288,31 @@ const state = { ensResolutionsByAddress: {}, pendingApprovals: {}, pendingApprovalCount: 0, + subjectMetadata: { + "http://localhost:8080": { + extensionId: null, + iconUrl: null, + name: "Hello, Snaps!", + origin: "http://localhost:8080", + subjectType: "website" + }, + "https://metamask.github.io": { + extensionId: null, + iconUrl: null, + name: "Snaps Iframe Execution Environment", + origin: "https://metamask.github.io", + subjectType: "website" + }, + "local:http://localhost:8080/": { + extensionId: null, + iconUrl: null, + name: "MetaMask Example Snap", + origin: "local:http://localhost:8080/", + subjectType: "snap", + svgIcon: "...", + version: "0.6.0" + } + } }, appState: { shouldClose: false, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 355700dc1..1edbaa7a5 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1015,10 +1015,6 @@ "ethGasPriceFetchWarning": { "message": "Der Gaspreis, der sich aus der Gaseinschätzung ergibt, ist derzeit nicht verfügbar." }, - "eth_accounts": { - "message": "Siehe Adresse, Kontostand, Aktivität und Einleitung von Transaktionen", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Öffentliche Ethereum-Adresse" }, @@ -2076,6 +2072,10 @@ "permissionRequest": { "message": "Berechtigungsanfrage" }, + "permission_ethereumAccounts": { + "message": "Siehe Adresse, Kontostand, Aktivität und Einleitung von Transaktionen", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Berechtigungen" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 0a3fe1284..34a8b4443 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Το εφεδρικό τέλος συναλλαγής που παρέχεται ως η κύρια υπηρεσία εκτίμησης τελών συναλλαγής, δεν είναι διαθέσιμο αυτή τη στιγμή." }, - "eth_accounts": { - "message": "Βλέπε διεύθυνση, υπόλοιπο λογαριασμού, δραστηριότητα και έναρξη συναλλαγών", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Δημόσια Διεύθυνση Ethereum" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "Αίτημα άδειας" }, + "permission_ethereumAccounts": { + "message": "Βλέπε διεύθυνση, υπόλοιπο λογαριασμού, δραστηριότητα και έναρξη συναλλαγών", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Άδειες" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c2204bddc..a94462cde 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -248,6 +248,9 @@ "approve": { "message": "Approve spend limit" }, + "approveAndInstall": { + "message": "Approve & Install" + }, "approveButtonText": { "message": "Approve" }, @@ -261,6 +264,12 @@ "approvedAmountWithColon": { "message": "Approved amount:" }, + "areYouDeveloper": { + "message": "Are you a developer?" + }, + "areYouSure": { + "message": "Are you sure?" + }, "asset": { "message": "Asset" }, @@ -539,6 +548,10 @@ "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..." }, @@ -1077,10 +1090,6 @@ "ethGasPriceFetchWarning": { "message": "Backup gas price is provided as the main gas estimation service is unavailable right now." }, - "eth_accounts": { - "message": "See address, account balance, activity and suggest transactions to approve", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Ethereum Public Address" }, @@ -1093,6 +1102,9 @@ "etherscanViewOn": { "message": "View on Etherscan" }, + "expandExperience": { + "message": "Expand your web3 experience" + }, "expandView": { "message": "Expand view" }, @@ -1673,6 +1685,9 @@ "malformedData": { "message": "Malformed data" }, + "manageSnaps": { + "message": "Manage your installed Snaps" + }, "max": { "message": "Max" }, @@ -1954,6 +1969,9 @@ "noNFTs": { "message": "No NFTs yet" }, + "noSnaps": { + "message": "No Snaps installed" + }, "noThanks": { "message": "No Thanks" }, @@ -2155,6 +2173,9 @@ "message": "Open MetaMask in full screen to connect your ledger via WebHID.", "description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid." }, + "openSourceCode": { + "message": "Check the source code" + }, "optional": { "message": "Optional" }, @@ -2211,6 +2232,37 @@ "permissionRequest": { "message": "Permission request" }, + "permissionRequestCapitalized": { + "message": "Permission Request" + }, + "permission_accessNetwork": { + "message": "Access the Internet.", + "description": "The description of the `endowment:network-access` permission." + }, + "permission_accessSnap": { + "message": "Connect to the $1 Snap.", + "description": "The description for the `wallet_snap_*` permission. $1 is the name of the Snap." + }, + "permission_customConfirmation": { + "message": "Display a confirmation in MetaMask.", + "description": "The description for the `snap_confirm` permission" + }, + "permission_ethereumAccounts": { + "message": "See address, account balance, activity and suggest transactions to approve", + "description": "The description for the `eth_accounts` permission" + }, + "permission_manageBip44Keys": { + "message": "Control your \"$1\" accounts and assets.", + "description": "The description for the `snap_getBip44Entropy_*` permission. $1 is the name of a protocol, e.g. 'Filecoin'." + }, + "permission_manageState": { + "message": "Store and manage its data on your device.", + "description": "The description for the `snap_manageState` permission" + }, + "permission_unknown": { + "message": "Unknown permission: $1", + "description": "$1 is the name of a requested permission that is not recognized." + }, "permissions": { "message": "Permissions" }, @@ -2346,6 +2398,12 @@ "removeNFT": { "message": "Remove NFT" }, + "removeSnap": { + "message": "Remove Snap" + }, + "removeSnapDescription": { + "message": "This action will delete the snap, its data and revoke your given permissions." + }, "replace": { "message": "replace" }, @@ -2661,6 +2719,39 @@ "slow": { "message": "Slow" }, + "snapAccess": { + "message": "$1 snap has access to:", + "description": "$1 represents the name of the snap" + }, + "snapError": { + "message": "Snap Error: '$1'. Error Code: '$2'", + "description": "This is shown when a snap encounters an error. $1 is the error message from the snap, and $2 is the error code." + }, + "snapInstall": { + "message": "Install Snap" + }, + "snapInstallWarningCheck": { + "message": "To confirm you understand, check all." + }, + "snapInstallWarningKeyAccess": { + "message": "You are granting key access to the snap \"$1\". This is irrevocable and grants \"$1\" control of your accounts and assets. Make sure you trust \"$1\" before proceeding.", + "description": "The parameter is the name of the snap" + }, + "snapRequestsPermission": { + "message": "This snap is requesting the following permissions:" + }, + "snaps": { + "message": "Snaps" + }, + "snapsSettingsDescription": { + "message": "Manage your Snaps" + }, + "snapsStatus": { + "message": "Snap status is dependent on activity." + }, + "snapsToggle": { + "message": "A snap will only run if it is enabled" + }, "somethingWentWrong": { "message": "Oops! Something went wrong." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index e529f90c6..be06dc662 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -665,10 +665,6 @@ "ethGasPriceFetchWarning": { "message": "Se muestra el precio del gas de respaldo, ya que el servicio para calcular el precio del gas principal no se encuentra disponible en este momento." }, - "eth_accounts": { - "message": "Ver las direcciones de las cuentas permitidas (requerido)", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Dirección pública de Ethereum" }, @@ -1296,6 +1292,10 @@ "pending": { "message": "Pendiente" }, + "permission_ethereumAccounts": { + "message": "Ver las direcciones de las cuentas permitidas (requerido)", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Permisos" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 5fa8bf07e..b0f0ce47c 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1035,10 +1035,6 @@ "ethGasPriceFetchWarning": { "message": "Se muestra el precio del gas de respaldo, ya que el servicio para calcular el precio del gas principal no se encuentra disponible en este momento." }, - "eth_accounts": { - "message": "Ver dirección, saldo de cuenta, actividad e iniciar transacciones", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Dirección pública de Ethereum" }, @@ -2125,6 +2121,10 @@ "permissionRequest": { "message": "Solicitud de permiso" }, + "permission_ethereumAccounts": { + "message": "Ver dirección, saldo de cuenta, actividad e iniciar transacciones", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Permisos" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index c43e7175b..ba7210bad 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Le prix de carburant de sauvegarde est fourni, car le service principal d’estimation du carburant est momentanément indisponible." }, - "eth_accounts": { - "message": "Consultez l’adresse, le solde du compte et l’activité, et lancez des transactions", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Adresse publique d’Ethereum" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "Demande d’autorisation" }, + "permission_ethereumAccounts": { + "message": "Consultez l’adresse, le solde du compte et l’activité, et lancez des transactions", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Autorisations" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9ad9040a0..ad121bc46 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "बैकअप गैस की कीमत प्रदान की जाती है क्योंकि मुख्य गैस अनुमान सर्विस अभी उपलब्ध नहीं है।" }, - "eth_accounts": { - "message": "पता, खाते की शेषराशि, गतिविधि देखें और लेन-देन शुरू करें", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Ethereum सार्वजनिक पता" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "अनुमति अनुरोध" }, + "permission_ethereumAccounts": { + "message": "पता, खाते की शेषराशि, गतिविधि देखें और लेन-देन शुरू करें", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "अनुमतियाँ" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 2a0ce20e1..9eefa0111 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Biaya gas cadangan diberikan karena layanan estimasi gas utama saat ini tidak tersedia." }, - "eth_accounts": { - "message": "Lihat alamat, saldo akun, aktivitas, dan mulai transaksi", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Alamat Publik Ethereum" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "Permohonan izin" }, + "permission_ethereumAccounts": { + "message": "Lihat alamat, saldo akun, aktivitas, dan mulai transaksi", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Izin" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 26c11736c..420339ca7 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -565,10 +565,6 @@ "estimatedProcessingTimes": { "message": "Tempi di Elaborazione Stimati" }, - "eth_accounts": { - "message": "Accesso agli indirizzi dei tuoi account autorizzati (richiesto)", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Indirizzo pubblico Ethereum " }, @@ -1054,6 +1050,10 @@ "pending": { "message": "in corso" }, + "permission_ethereumAccounts": { + "message": "Accesso agli indirizzi dei tuoi account autorizzati (richiesto)", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Permessi" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 8b0813cf2..f7a2439f2 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "現在メインのガスの見積もりサービスが利用できないため、バックアップのガス代が提供されています。" }, - "eth_accounts": { - "message": "アドレス、アカウント残高、アクティビティを表示してトランザクションを開始", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "パブリックイーサリアムアドレス" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "許可のリクエスト" }, + "permission_ethereumAccounts": { + "message": "アドレス、アカウント残高、アクティビティを表示してトランザクションを開始", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "許可" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f91a173ce..08bfea464 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "현재 주요 가스 견적 서비스를 사용할 수 없으므로 백업 가스 가격을 제공합니다." }, - "eth_accounts": { - "message": "허용되는 계정의 주소 보기(필수)", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "이더리움 공개 주소" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "승인 요청" }, + "permission_ethereumAccounts": { + "message": "허용되는 계정의 주소 보기(필수)", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "권한" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index b93496dc1..43a567687 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -668,10 +668,6 @@ "ethGasPriceFetchWarning": { "message": "Ibinibigay ang backup na presyo ng gas dahil hindi available ang pangunahing serbisyo sa pagtatantya ng gas sa ngayon." }, - "eth_accounts": { - "message": "Tingnan ang mga address ng iyong mga pinapayagang account (kinakailangan)", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Pampublikong Address ng Ethereum" }, @@ -1321,6 +1317,10 @@ "pending": { "message": "Nakabinbin" }, + "permission_ethereumAccounts": { + "message": "Tingnan ang mga address ng iyong mga pinapayagang account (kinakailangan)", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Mga Pahintulot" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 5cac8f4b4..e26c0e3a1 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1019,10 +1019,6 @@ "ethGasPriceFetchWarning": { "message": "O preço de backup do gás é fornecido porque a estimativa de gás principal está indisponível no momento." }, - "eth_accounts": { - "message": "Ver endereço, saldo da conta, atividade e iniciar transações", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Endereço público do Ethereum" }, @@ -2109,6 +2105,10 @@ "permissionRequest": { "message": "Solicitação de permissão" }, + "permission_ethereumAccounts": { + "message": "Ver endereço, saldo da conta, atividade e iniciar transações", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Permissões" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index bd3759f14..6b555588d 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Указана резервная цена газа, поскольку основной сервис определения цены газа сейчас недоступен." }, - "eth_accounts": { - "message": "См. адрес, баланс счета, активность и инициируйте транзакции", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Открытый адрес Ethereum" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "Запрос разрешения" }, + "permission_ethereumAccounts": { + "message": "См. адрес, баланс счета, активность и инициируйте транзакции", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Разрешения" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 472a04f59..fe2961c58 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Ang backup gas price ay inilalaan dahil ang pangunahing pagtantiya ng presyo ng gas ay hindi available sa ngayon." }, - "eth_accounts": { - "message": "Tingnan ang mga address, balanse ng account, aktibidad at simulan ang iyong mga transaksyon", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Pampublikong Address ng Ethereum" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "Kahilingan sa pahintulot" }, + "permission_ethereumAccounts": { + "message": "Tingnan ang mga address, balanse ng account, aktibidad at simulan ang iyong mga transaksyon", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Mga Pahintulot" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 47a85b823..762f280eb 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Ana gaz tahmini hizmeti olarak sunulan yedek gaz fiyatı şu anda kullanılamıyor." }, - "eth_accounts": { - "message": "Adrese, hesap bakiyesine, aktiviteye bakın ve işlemleri başlatın", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Ethereum Genel Adresi" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "İzin talebi" }, + "permission_ethereumAccounts": { + "message": "Adrese, hesap bakiyesine, aktiviteye bakın ve işlemleri başlatın", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "İzinler" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index c8f43f33b..2c480fafa 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "Giá gas dự phòng được cung cấp vì dịch vụ ước tính giá gas chính hiện không hoạt động." }, - "eth_accounts": { - "message": "Xem địa chỉ, số dư tài khoản, hoạt động và bắt đầu giao dịch", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "Địa chỉ công khai trên Ethereum" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "Yêu cầu quyền" }, + "permission_ethereumAccounts": { + "message": "Xem địa chỉ, số dư tài khoản, hoạt động và bắt đầu giao dịch", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "Quyền" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index f141784c0..939fec72d 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1005,10 +1005,6 @@ "ethGasPriceFetchWarning": { "message": "由于目前主要的燃料估算服务不可用,因此提供了备用燃料价格。" }, - "eth_accounts": { - "message": "查看您允许的账户的地址(必填)", - "description": "The description for the `eth_accounts` permission" - }, "ethereumPublicAddress": { "message": "以太坊 Ethereum 公开地址" }, @@ -2079,6 +2075,10 @@ "permissionRequest": { "message": "权限请求" }, + "permission_ethereumAccounts": { + "message": "查看您允许的账户的地址(必填)", + "description": "The description for the `eth_accounts` permission" + }, "permissions": { "message": "权限" }, diff --git a/app/scripts/controllers/permissions/flask/snap-permissions.js b/app/scripts/controllers/permissions/flask/snap-permissions.js new file mode 100644 index 000000000..bea6ba7a3 --- /dev/null +++ b/app/scripts/controllers/permissions/flask/snap-permissions.js @@ -0,0 +1,34 @@ +import { + restrictedMethodPermissionBuilders, + selectHooks, +} from '@metamask/rpc-methods'; +import { endowmentPermissionBuilders } from '@metamask/snap-controllers'; + +/** + * @returns {Record>} All endowment permission + * specifications. + */ +export const buildSnapEndowmentSpecifications = () => + Object.values(endowmentPermissionBuilders).reduce( + (allSpecifications, { targetKey, specificationBuilder }) => { + allSpecifications[targetKey] = specificationBuilder(); + return allSpecifications; + }, + {}, + ); + +/** + * @param {Record} hooks - The hooks for the Snap + * restricted method implementations. + */ +export function buildSnapRestrictedMethodSpecifications(hooks) { + return Object.values(restrictedMethodPermissionBuilders).reduce( + (specifications, { targetKey, specificationBuilder, methodHooks }) => { + specifications[targetKey] = specificationBuilder({ + methodHooks: selectHooks(hooks, methodHooks), + }); + return specifications; + }, + {}, + ); +} diff --git a/app/scripts/controllers/permissions/flask/snap-permissions.test.js b/app/scripts/controllers/permissions/flask/snap-permissions.test.js new file mode 100644 index 000000000..c1f5dcce6 --- /dev/null +++ b/app/scripts/controllers/permissions/flask/snap-permissions.test.js @@ -0,0 +1,46 @@ +import { + EndowmentPermissions, + RestrictedMethods, +} from '../../../../../shared/constants/permissions'; +import { + buildSnapEndowmentSpecifications, + buildSnapRestrictedMethodSpecifications, +} from './snap-permissions'; + +describe('buildSnapRestrictedMethodSpecifications', () => { + it('creates valid permission specification objects', () => { + const hooks = { + addSnap: () => undefined, + clearSnapState: () => undefined, + getMnemonic: () => undefined, + getSnap: () => undefined, + getSnapRpcHandler: () => undefined, + getSnapState: () => undefined, + showConfirmation: () => undefined, + updateSnapState: () => undefined, + }; + + const specifications = buildSnapRestrictedMethodSpecifications(hooks); + + const allRestrictedMethods = Object.keys(RestrictedMethods); + Object.keys(specifications).forEach((permissionKey) => + expect(allRestrictedMethods).toContain(permissionKey), + ); + + Object.values(specifications).forEach((specification) => { + expect(specification).toMatchObject({ + targetKey: expect.stringMatching(/^(snap_|wallet_)/u), + methodImplementation: expect.any(Function), + allowedCaveats: null, + }); + }); + }); +}); + +describe('buildSnapEndowmentSpecifications', () => { + it('creates valid permission specification objects', () => { + expect( + Object.keys(buildSnapEndowmentSpecifications()).sort(), + ).toStrictEqual(Object.keys(EndowmentPermissions).sort()); + }); +}); diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 419d03993..c091882ad 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -4,3 +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'; +///: END:ONLY_INCLUDE_IN diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 435079ba9..201810e54 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -1,4 +1,7 @@ -import { constructPermission } from '@metamask/snap-controllers'; +import { + constructPermission, + PermissionType, +} from '@metamask/snap-controllers'; import { CaveatTypes, RestrictedMethods, @@ -90,6 +93,7 @@ export const getPermissionSpecifications = ({ }) => { return { [PermissionKeys.eth_accounts]: { + permissionType: PermissionType.RestrictedMethod, targetKey: PermissionKeys.eth_accounts, allowedCaveats: [CaveatTypes.restrictReturnedAccounts], diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index 55028dbdd..74587ed73 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -1,3 +1,6 @@ +///: BEGIN:ONLY_INCLUDE_IN(flask) +import { handlers as permittedSnapMethods } from '@metamask/rpc-methods/dist/permitted'; +///: END:ONLY_INCLUDE_IN import { flatten } from 'lodash'; import { permissionRpcMethods } from '@metamask/snap-controllers'; import { selectHooks } from '@metamask/rpc-methods'; @@ -30,7 +33,7 @@ const expectedHookNames = Array.from( * controllers. * @returns {(req: Object, res: Object, next: Function, end: Function) => void} */ -export default function createMethodMiddleware(hooks) { +export function createMethodMiddleware(hooks) { // Fail immediately if we forgot to provide any expected hooks. const missingHookNames = expectedHookNames.filter( (hookName) => !Object.hasOwnProperty.call(hooks, hookName), @@ -60,6 +63,7 @@ export default function createMethodMiddleware(hooks) { selectHooks(hooks, hookNames), ); } catch (error) { + console.error(error); return end(error); } } @@ -67,3 +71,40 @@ export default function createMethodMiddleware(hooks) { return next(); }; } + +///: BEGIN:ONLY_INCLUDE_IN(flask) +const snapHandlerMap = permittedSnapMethods.reduce((map, handler) => { + for (const methodName of handler.methodNames) { + map.set(methodName, handler); + } + return map; +}, new Map()); + +export function createSnapMethodMiddleware(isSnap, hooks) { + return async function methodMiddleware(req, res, next, end) { + const handler = snapHandlerMap.get(req.method); + if (handler) { + if (/^snap_/iu.test(req.method) && !isSnap) { + return end(ethErrors.rpc.methodNotFound()); + } + + const { implementation, hookNames } = handler; + try { + // Implementations may or may not be async, so we must await them. + return await implementation( + req, + res, + next, + end, + selectHooks(hooks, hookNames), + ); + } catch (error) { + console.error(error); + return end(error); + } + } + + return next(); + }; +} +///: END:ONLY_INCLUDE_IN diff --git a/app/scripts/lib/rpc-method-middleware/index.js b/app/scripts/lib/rpc-method-middleware/index.js index b0f813570..adf68c359 100644 --- a/app/scripts/lib/rpc-method-middleware/index.js +++ b/app/scripts/lib/rpc-method-middleware/index.js @@ -1 +1 @@ -export { default } from './createMethodMiddleware'; +export * from './createMethodMiddleware'; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 966be90e3..93381ed63 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -38,7 +38,13 @@ import { import { PermissionController, SubjectMetadataController, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SnapController, + ///: END:ONLY_INCLUDE_IN } from '@metamask/snap-controllers'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import { IframeExecutionService } from '@metamask/iframe-execution-environment-service'; +///: END:ONLY_INCLUDE_IN import { TRANSACTION_STATUSES, @@ -57,11 +63,17 @@ import { import { CaveatTypes, RestrictedMethods, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + EndowmentPermissions, + ///: END:ONLY_INCLUDE_IN } from '../../shared/constants/permissions'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { MILLISECOND } from '../../shared/constants/time'; import { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + MESSAGE_TYPE, + ///: END:ONLY_INCLUDE_IN POLLING_TOKEN_ENVIRONMENT_TYPES, SUBJECT_TYPES, } from '../../shared/constants/app'; @@ -73,7 +85,12 @@ import { isEqualCaseInsensitive } from '../../ui/helpers/utils/util'; import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; -import createMethodMiddleware from './lib/rpc-method-middleware'; +import { + createMethodMiddleware, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + createSnapMethodMiddleware, + ///: END:ONLY_INCLUDE_IN +} from './lib/rpc-method-middleware'; import createOriginMiddleware from './lib/createOriginMiddleware'; import createTabIdMiddleware from './lib/createTabIdMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; @@ -107,9 +124,13 @@ import { getPermissionBackgroundApiMethods, getPermissionSpecifications, getPermittedAccountsByOrigin, - PermissionLogController, NOTIFICATION_NAMES, + PermissionLogController, unrestrictedMethods, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + buildSnapEndowmentSpecifications, + buildSnapRestrictedMethodSpecifications, + ///: END:ONLY_INCLUDE_IN } from './controllers/permissions'; export const METAMASK_CONTROLLER_EVENTS = { @@ -500,36 +521,41 @@ export default class MetamaskController extends EventEmitter { }), state: initState.PermissionController, caveatSpecifications: getCaveatSpecifications({ getIdentities }), - permissionSpecifications: getPermissionSpecifications({ - getIdentities, - getAllAccounts: this.keyringController.getAccounts.bind( - this.keyringController, - ), - captureKeyringTypesWithMissingIdentities: ( - identities = {}, - accounts = [], - ) => { - const accountsMissingIdentities = accounts.filter( - (address) => !identities[address], - ); - const keyringTypesWithMissingIdentities = accountsMissingIdentities.map( - (address) => - this.keyringController.getKeyringForAccount(address)?.type, - ); + permissionSpecifications: { + ...getPermissionSpecifications({ + getIdentities, + getAllAccounts: this.keyringController.getAccounts.bind( + this.keyringController, + ), + captureKeyringTypesWithMissingIdentities: ( + identities = {}, + accounts = [], + ) => { + const accountsMissingIdentities = accounts.filter( + (address) => !identities[address], + ); + const keyringTypesWithMissingIdentities = accountsMissingIdentities.map( + (address) => + this.keyringController.getKeyringForAccount(address)?.type, + ); - const identitiesCount = Object.keys(identities || {}).length; + const identitiesCount = Object.keys(identities || {}).length; - const accountTrackerCount = Object.keys( - this.accountTracker.store.getState().accounts || {}, - ).length; + const accountTrackerCount = Object.keys( + this.accountTracker.store.getState().accounts || {}, + ).length; - captureException( - new Error( - `Attempt to get permission specifications failed because their were ${accounts.length} accounts, but ${identitiesCount} identities, and the ${keyringTypesWithMissingIdentities} keyrings included accounts with missing identities. Meanwhile, there are ${accountTrackerCount} accounts in the account tracker.`, - ), - ); - }, - }), + captureException( + new Error( + `Attempt to get permission specifications failed because their were ${accounts.length} accounts, but ${identitiesCount} identities, and the ${keyringTypesWithMissingIdentities} keyrings included accounts with missing identities. Meanwhile, there are ${accountTrackerCount} accounts in the account tracker.`, + ), + ); + }, + }), + ///: BEGIN:ONLY_INCLUDE_IN(flask) + ...this.getSnapPermissionSpecifications(), + ///: END:ONLY_INCLUDE_IN + }, unrestrictedMethods, }); @@ -547,6 +573,53 @@ export default class MetamaskController extends EventEmitter { subjectCacheLimit: 100, }); + ///: BEGIN:ONLY_INCLUDE_IN(flask) + this.workerController = new IframeExecutionService({ + onError: this.onExecutionEnvironmentError.bind(this), + iframeUrl: new URL( + 'https://metamask.github.io/iframe-execution-environment/0.3.1', + ), + messenger: this.controllerMessenger.getRestricted({ + name: 'ExecutionService', + }), + setupSnapProvider: this.setupSnapProvider.bind(this), + }); + + const snapControllerMessenger = this.controllerMessenger.getRestricted({ + name: 'SnapController', + allowedEvents: [ + 'ExecutionService:unhandledError', + 'ExecutionService:unresponsive', + ], + allowedActions: [ + `${this.permissionController.name}:getEndowments`, + `${this.permissionController.name}:getPermissions`, + `${this.permissionController.name}:hasPermission`, + `${this.permissionController.name}:requestPermissions`, + `${this.permissionController.name}:revokeAllPermissions`, + ], + }); + + this.snapController = new SnapController({ + endowmentPermissionNames: Object.values(EndowmentPermissions), + terminateAllSnaps: this.workerController.terminateAllSnaps.bind( + this.workerController, + ), + terminateSnap: this.workerController.terminateSnap.bind( + this.workerController, + ), + executeSnap: this.workerController.executeSnap.bind( + this.workerController, + ), + getRpcMessageHandler: this.workerController.getRpcMessageHandler.bind( + this.workerController, + ), + closeAllConnections: this.removeAllConnections.bind(this), + state: initState.SnapController, + messenger: snapControllerMessenger, + }); + ///: END:ONLY_INCLUDE_IN + this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, tokensController: this.tokensController, @@ -799,6 +872,9 @@ export default class MetamaskController extends EventEmitter { TokenListController: this.tokenListController, TokensController: this.tokensController, CollectiblesController: this.collectiblesController, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SnapController: this.snapController, + ///: END:ONLY_INCLUDE_IN }); this.memStore = new ComposableObservableStore({ @@ -835,6 +911,9 @@ export default class MetamaskController extends EventEmitter { TokenListController: this.tokenListController, TokensController: this.tokensController, CollectiblesController: this.collectiblesController, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SnapController: this.snapController, + ///: END:ONLY_INCLUDE_IN }, controllerMessenger: this.controllerMessenger, }); @@ -866,6 +945,58 @@ export default class MetamaskController extends EventEmitter { this.publicConfigStore = this.createPublicConfigStore(); } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + /** + * Constructor helper for getting Snap permission specifications. + */ + getSnapPermissionSpecifications() { + return { + ...buildSnapEndowmentSpecifications(), + ...buildSnapRestrictedMethodSpecifications({ + addSnap: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:add', + ), + clearSnapState: (fromSubject) => + this.controllerMessenger( + 'SnapController:updateSnap', + fromSubject, + {}, + ), + getMnemonic: this.getPrimaryKeyringMnemonic.bind(this), + getSnap: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:get', + ), + getSnapRpcHandler: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:getRpcMessageHandler', + ), + getSnapState: async (...args) => { + // TODO:flask Just return the action result directly in the next + // @metamask/snap-controllers update. + return ( + (await this.controllerMessenger.call( + 'SnapController:getSnapState', + ...args, + )) ?? null + ); + }, + showConfirmation: (origin, confirmationData) => + this.approvalController.addAndShowApprovalRequest({ + origin, + type: MESSAGE_TYPE.SNAP_CONFIRM, + requestData: confirmationData, + }), + updateSnapState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:updateSnapState', + ), + }), + }; + } + ///: END:ONLY_INCLUDE_IN + /** * Sets up BaseController V2 event subscriptions. Currently, this includes * the subscriptions necessary to notify permission subjects of account @@ -929,6 +1060,39 @@ export default class MetamaskController extends EventEmitter { }, getPermittedAccountsByOrigin, ); + + ///: BEGIN:ONLY_INCLUDE_IN(flask) + // Record Snap metadata whenever a Snap is added to state. + this.controllerMessenger.subscribe( + `${this.snapController.name}:snapAdded`, + (snapId, snap, svgIcon = null) => { + const { + manifest: { proposedName }, + version, + } = snap; + this.subjectMetadataController.addSubjectMetadata({ + subjectType: SUBJECT_TYPES.SNAP, + name: proposedName, + origin: snapId, + version, + svgIcon, + }); + }, + ); + + this.controllerMessenger.subscribe( + `${this.snapController.name}:snapInstalled`, + (snapId) => { + this.metaMetricsController.trackEvent({ + event: 'Snap Installed', + category: 'Snaps', + properties: { + snap_id: snapId, + }, + }); + }, + ); + ///: END:ONLY_INCLUDE_IN } /** @@ -1400,6 +1564,16 @@ export default class MetamaskController extends EventEmitter { ), ...getPermissionBackgroundApiMethods(permissionController), + ///: BEGIN:ONLY_INCLUDE_IN(flask) + // snaps + removeSnapError: this.snapController.removeSnapError.bind( + this.snapController, + ), + disableSnap: this.snapController.disableSnap.bind(this.snapController), + enableSnap: this.snapController.enableSnap.bind(this.snapController), + removeSnap: this.removeSnap.bind(this), + ///: END:ONLY_INCLUDE_IN + // swaps fetchAndSetQuotes: swapsController.fetchAndSetQuotes.bind( swapsController, @@ -1826,6 +2000,17 @@ export default class MetamaskController extends EventEmitter { this.preferencesController.setSelectedAddress(address); } + /** + * Gets the mnemonic of the user's primary keyring. + */ + getPrimaryKeyringMnemonic() { + const keyring = this.keyringController.getKeyringsByType('HD Key Tree')[0]; + if (!keyring.mnemonic) { + throw new Error('Primary keyring mnemonic unavailable.'); + } + return keyring.mnemonic; + } + // // Hardware // @@ -2201,6 +2386,32 @@ export default class MetamaskController extends EventEmitter { return await promise; } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + /** + * Gets an "app key" corresponding to an Ethereum address. An app key is more + * or less an addrdess hashed together with some string, in this case a + * subject identifier / origin. + * + * @todo Figure out a way to derive app keys that doesn't depend on the user's + * Ethereum addresses. + * @param {string} subject - The identifier of the subject whose app key to + * retrieve. + * @param {string} [requestedAccount] - The account whose app key to retrieve. + * The first account in the keyring will be used by default. + */ + async getAppKeyForSubject(subject, requestedAccount) { + let account; + + if (requestedAccount) { + account = requestedAccount; + } else { + account = (await this.keyringController.getAccounts())[0]; + } + + return this.keyringController.exportAppKeyForAddress(account, subject); + } + ///: END:ONLY_INCLUDE_IN + /** * Signifies user intent to complete an eth_sign method. * @@ -2671,6 +2882,13 @@ export default class MetamaskController extends EventEmitter { * @property {string} - The URL of the page or frame hosting the script that sent the message. */ + /** + * A Snap sender object. + * + * @typedef {Object} SnapSender + * @property {string} snapId - The ID of the snap. + */ + /** * Used to create a multiplexed stream for connecting to an untrusted context * like a Dapp or other extension. @@ -2682,6 +2900,7 @@ export default class MetamaskController extends EventEmitter { */ setupUntrustedCommunication({ connectionStream, sender, subjectType }) { const { usePhishDetect } = this.preferencesController.store.getState(); + let _subjectType; if (subjectType) { _subjectType = subjectType; @@ -2794,14 +3013,20 @@ export default class MetamaskController extends EventEmitter { * A method for serving our ethereum provider over a given stream. * * @param {*} outStream - The stream to provide over. - * @param {MessageSender} sender - The sender of the messages on this stream + * @param {MessageSender | SnapSender} sender - The sender of the messages on this stream * @param {string} subjectType - The type of the sender, i.e. subject. */ setupProviderConnection(outStream, sender, subjectType) { let origin; if (subjectType === SUBJECT_TYPES.INTERNAL) { origin = 'metamask'; - } else { + } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + else if (subjectType === SUBJECT_TYPES.SNAP) { + origin = sender.snapId; + } + ///: END:ONLY_INCLUDE_IN + else { origin = new URL(sender.url).origin; } @@ -2820,9 +3045,9 @@ export default class MetamaskController extends EventEmitter { const engine = this.setupProviderEngine({ origin, - tabId, sender, subjectType, + tabId, }); // setup connection @@ -2844,6 +3069,33 @@ export default class MetamaskController extends EventEmitter { }); } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + /** + * For snaps running in workers. + * + * @param snapId + * @param error + */ + onExecutionEnvironmentError(snapId, error) { + this.snapController.stopPlugin(snapId); + this.snapController.addSnapError(error); + } + + /** + * For snaps running in workers. + * + * @param snapId + * @param connectionStream + */ + setupSnapProvider(snapId, connectionStream) { + this.setupUntrustedCommunication({ + connectionStream, + sender: { snapId }, + subjectType: SUBJECT_TYPES.SNAP, + }); + } + ///: END:ONLY_INCLUDE_IN + /** * A method for creating a provider that is safely restricted for the requesting subject. * @@ -2872,13 +3124,16 @@ export default class MetamaskController extends EventEmitter { // append origin to each request engine.push(createOriginMiddleware({ origin })); + // append tabId to each request if it exists if (tabId) { engine.push(createTabIdMiddleware({ tabId })); } + // logging engine.push(createLoggerMiddleware({ origin })); engine.push(this.permissionLogController.createMiddleware()); + // onboarding if (subjectType === SUBJECT_TYPES.WEBSITE) { engine.push( @@ -2888,6 +3143,8 @@ export default class MetamaskController extends EventEmitter { }), ); } + + // Unrestricted/permissionless RPC method implementations engine.push( createMethodMiddleware({ origin, @@ -2975,6 +3232,34 @@ export default class MetamaskController extends EventEmitter { ), }), ); + + ///: BEGIN:ONLY_INCLUDE_IN(flask) + engine.push( + createSnapMethodMiddleware(subjectType === SUBJECT_TYPES.SNAP, { + getAppKey: this.getAppKeyForSubject.bind(this, origin), + getSnaps: this.snapController.getPermittedSnaps.bind( + this.snapController, + origin, + ), + requestPermissions: async (requestedPermissions) => { + const [ + approvedPermissions, + ] = await this.permissionController.requestPermissions( + { origin }, + requestedPermissions, + ); + + return Object.values(approvedPermissions); + }, + getAccounts: this.getPermittedAccounts.bind(this, origin), + installSnaps: this.snapController.installSnaps.bind( + this.snapController, + origin, + ), + }), + ); + ///: END:ONLY_INCLUDE_IN + // filter and subscription polyfills engine.push(filterMiddleware); engine.push(subscriptionManager.middleware); @@ -2986,6 +3271,7 @@ export default class MetamaskController extends EventEmitter { }), ); } + // forward to metamask primary provider engine.push(providerAsMiddleware(provider)); return engine; @@ -3060,6 +3346,24 @@ export default class MetamaskController extends EventEmitter { } } + /** + * Closes all connections for the given origin, and removes the references + * to them. + * Ignores unknown origins. + * + * @param {string} origin - The origin string. + */ + removeAllConnections(origin) { + const connections = this.connections[origin]; + if (!connections) { + return; + } + + Object.keys(connections).forEach((id) => { + this.removeConnection(origin, id); + }); + } + /** * Causes the RPC engines associated with the connections to the given origin * to emit a notification event with the given payload. @@ -3513,4 +3817,23 @@ export default class MetamaskController extends EventEmitter { } return this.keyringController.setLocked(); } + + ///: BEGIN:ONLY_INCLUDE_IN(flask) + // SNAPS + /** + * Removes the specified snap, and all of its associated permissions. + * If we didn't revoke the permission to access the snap from all subjects, + * they could just reinstall without any confirmation. + * + * TODO: This should be implemented in `SnapController.removeSnap` via a controller action. + * + * @param {{ id: string, permissionName: string }} snap - The wrapper object of the snap to remove. + */ + removeSnap(snap) { + this.snapController.removeSnap(snap.id); + this.permissionController.revokePermissionForAllSubjects( + snap.permissionName, + ); + } + ///: END:ONLY_INCLUDE_IN } diff --git a/development/build/transforms/README.md b/development/build/transforms/README.md index 4cbba5e5b..de1e8d0c5 100644 --- a/development/build/transforms/README.md +++ b/development/build/transforms/README.md @@ -29,8 +29,8 @@ this.store.updateStructure({ ..., GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, - ///: BEGIN:ONLY_INCLUDE_IN(beta) - PluginController: this.pluginController, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SnapController: this.snapController, ///: END:ONLY_INCLUDE_IN }); ``` diff --git a/jest.config.js b/jest.config.js index f7bcdf51f..e366a29fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ module.exports = { collectCoverageFrom: [ - '/app/scripts/controllers/permissions/*.js', + '/app/scripts/controllers/permissions/**/*.js', '/shared/**/*.js', '/ui/**/*.js', ], @@ -14,7 +14,7 @@ module.exports = { lines: 43, statements: 43, }, - './app/scripts/controllers/permissions/*.js': { + './app/scripts/controllers/permissions/**/*.js': { branches: 100, functions: 100, lines: 100, @@ -33,7 +33,7 @@ module.exports = { '/app/scripts/migrations/*.test.js', '/app/scripts/platforms/*.test.js', 'app/scripts/controllers/network/**/*.test.js', - '/app/scripts/controllers/permissions/*.test.js', + '/app/scripts/controllers/permissions/**/*.test.js', ], testTimeout: 2500, transform: { diff --git a/lavamoat/browserify/beta/policy-override.json b/lavamoat/browserify/beta/policy-override.json index 71fd98f61..a3d850f83 100644 --- a/lavamoat/browserify/beta/policy-override.json +++ b/lavamoat/browserify/beta/policy-override.json @@ -10,6 +10,11 @@ "@babel/runtime": true } }, + "keccak": { + "packages": { + "readable-stream": true + } + }, "node-fetch": { "globals": { "fetch": true diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ad02b495f..b4a93c2e5 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -700,11 +700,11 @@ }, "@metamask/snap-controllers": { "globals": { + "URL": true, "Worker": true, "clearTimeout": true, "console.error": true, "console.log": true, - "console.warn": true, "fetch": true, "setTimeout": true }, @@ -715,14 +715,22 @@ "@metamask/post-message-stream": true, "@metamask/safe-event-emitter": true, "@metamask/snap-workers": true, + "ajv": true, + "buffer": true, + "concat-stream": true, + "crypto-browserify": true, "deep-freeze-strict": true, "eth-rpc-errors": true, "fast-deep-equal": true, + "gunzip-maybe": true, "immer": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, "nanoid": true, - "pump": true + "pump": true, + "readable-web-to-node-stream": true, + "semver": true, + "tar-stream": true } }, "@ngraveio/bc-ur": { @@ -973,6 +981,11 @@ "define": true } }, + "ajv": { + "packages": { + "fast-deep-equal": true + } + }, "analytics-node": { "globals": { "clearTimeout": true, @@ -1158,6 +1171,7 @@ "bl": { "packages": { "buffer": true, + "inherits": true, "readable-stream": true, "util": true } @@ -1270,6 +1284,16 @@ "btoa": true } }, + "browserify-zlib": { + "packages": { + "assert": true, + "buffer": true, + "pako": true, + "process": true, + "readable-stream": true, + "util": true + } + }, "bs58": { "packages": { "base-x": true @@ -1296,6 +1320,11 @@ "ieee754": true } }, + "buffer-from": { + "packages": { + "buffer": true + } + }, "buffer-split": { "packages": { "buffer-indexof": true @@ -1426,6 +1455,13 @@ "util": true } }, + "concat-stream": { + "packages": { + "buffer": true, + "inherits": true, + "readable-stream": true + } + }, "constant-case": { "packages": { "snake-case": true, @@ -1707,6 +1743,16 @@ "stream-browserify": true } }, + "duplexify": { + "packages": { + "buffer": true, + "end-of-stream": true, + "inherits": true, + "process": true, + "readable-stream": true, + "stream-shift": true + } + }, "elliptic": { "packages": { "bn.js": true, @@ -2212,6 +2258,11 @@ "postMessage": true } }, + "fs-constants": { + "packages": { + "constants-browserify": true + } + }, "fsm-event": { "packages": { "assert": true, @@ -2280,6 +2331,16 @@ "superagent": true } }, + "gunzip-maybe": { + "packages": { + "browserify-zlib": true, + "is-deflate": true, + "is-gzip": true, + "peek-stream": true, + "pumpify": true, + "through2": true + } + }, "hamt-sharding": { "packages": { "is-buffer": true, @@ -3930,6 +3991,14 @@ "sha.js": true } }, + "peek-stream": { + "packages": { + "buffer": true, + "buffer-from": true, + "duplexify": true, + "through2": true + } + }, "peer-book": { "packages": { "bs58": true, @@ -4152,6 +4221,13 @@ "process": true } }, + "pumpify": { + "packages": { + "duplexify": true, + "inherits": true, + "pump": true + } + }, "punycode": { "globals": { "define": true @@ -4465,6 +4541,11 @@ "util-deprecate": true } }, + "readable-web-to-node-stream": { + "packages": { + "readable-stream": true + } + }, "receptacle": { "globals": { "clearTimeout": true, @@ -4786,6 +4867,19 @@ "upper-case": true } }, + "tar-stream": { + "packages": { + "bl": true, + "buffer": true, + "end-of-stream": true, + "fs-constants": true, + "inherits": true, + "process": true, + "readable-stream": true, + "string_decoder": true, + "util": true + } + }, "textarea-caret": { "globals": { "document.body.appendChild": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ad02b495f..d5a9a9310 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -617,6 +617,25 @@ "URL": true } }, + "@metamask/iframe-execution-environment-service": { + "globals": { + "clearTimeout": true, + "console.log": true, + "document.body.appendChild": true, + "document.createElement": true, + "document.getElementById": true, + "setTimeout": true + }, + "packages": { + "@metamask/post-message-stream": true, + "@metamask/snap-controllers": true, + "@metamask/snap-workers": true, + "json-rpc-engine": true, + "json-rpc-middleware-stream": true, + "nanoid": true, + "pump": true + } + }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -700,11 +719,11 @@ }, "@metamask/snap-controllers": { "globals": { + "URL": true, "Worker": true, "clearTimeout": true, "console.error": true, "console.log": true, - "console.warn": true, "fetch": true, "setTimeout": true }, @@ -715,14 +734,22 @@ "@metamask/post-message-stream": true, "@metamask/safe-event-emitter": true, "@metamask/snap-workers": true, + "ajv": true, + "buffer": true, + "concat-stream": true, + "crypto-browserify": true, "deep-freeze-strict": true, "eth-rpc-errors": true, "fast-deep-equal": true, + "gunzip-maybe": true, "immer": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, "nanoid": true, - "pump": true + "pump": true, + "readable-web-to-node-stream": true, + "semver": true, + "tar-stream": true } }, "@ngraveio/bc-ur": { @@ -973,6 +1000,11 @@ "define": true } }, + "ajv": { + "packages": { + "fast-deep-equal": true + } + }, "analytics-node": { "globals": { "clearTimeout": true, @@ -1158,6 +1190,7 @@ "bl": { "packages": { "buffer": true, + "inherits": true, "readable-stream": true, "util": true } @@ -1270,6 +1303,16 @@ "btoa": true } }, + "browserify-zlib": { + "packages": { + "assert": true, + "buffer": true, + "pako": true, + "process": true, + "readable-stream": true, + "util": true + } + }, "bs58": { "packages": { "base-x": true @@ -1296,6 +1339,11 @@ "ieee754": true } }, + "buffer-from": { + "packages": { + "buffer": true + } + }, "buffer-split": { "packages": { "buffer-indexof": true @@ -1426,6 +1474,13 @@ "util": true } }, + "concat-stream": { + "packages": { + "buffer": true, + "inherits": true, + "readable-stream": true + } + }, "constant-case": { "packages": { "snake-case": true, @@ -1707,6 +1762,16 @@ "stream-browserify": true } }, + "duplexify": { + "packages": { + "buffer": true, + "end-of-stream": true, + "inherits": true, + "process": true, + "readable-stream": true, + "stream-shift": true + } + }, "elliptic": { "packages": { "bn.js": true, @@ -2212,6 +2277,11 @@ "postMessage": true } }, + "fs-constants": { + "packages": { + "constants-browserify": true + } + }, "fsm-event": { "packages": { "assert": true, @@ -2280,6 +2350,16 @@ "superagent": true } }, + "gunzip-maybe": { + "packages": { + "browserify-zlib": true, + "is-deflate": true, + "is-gzip": true, + "peek-stream": true, + "pumpify": true, + "through2": true + } + }, "hamt-sharding": { "packages": { "is-buffer": true, @@ -3930,6 +4010,14 @@ "sha.js": true } }, + "peek-stream": { + "packages": { + "buffer": true, + "buffer-from": true, + "duplexify": true, + "through2": true + } + }, "peer-book": { "packages": { "bs58": true, @@ -4152,6 +4240,13 @@ "process": true } }, + "pumpify": { + "packages": { + "duplexify": true, + "inherits": true, + "pump": true + } + }, "punycode": { "globals": { "define": true @@ -4465,6 +4560,11 @@ "util-deprecate": true } }, + "readable-web-to-node-stream": { + "packages": { + "readable-stream": true + } + }, "receptacle": { "globals": { "clearTimeout": true, @@ -4786,6 +4886,19 @@ "upper-case": true } }, + "tar-stream": { + "packages": { + "bl": true, + "buffer": true, + "end-of-stream": true, + "fs-constants": true, + "inherits": true, + "process": true, + "readable-stream": true, + "string_decoder": true, + "util": true + } + }, "textarea-caret": { "globals": { "document.body.appendChild": true, diff --git a/lavamoat/browserify/main/policy-override.json b/lavamoat/browserify/main/policy-override.json index 71fd98f61..a3d850f83 100644 --- a/lavamoat/browserify/main/policy-override.json +++ b/lavamoat/browserify/main/policy-override.json @@ -10,6 +10,11 @@ "@babel/runtime": true } }, + "keccak": { + "packages": { + "readable-stream": true + } + }, "node-fetch": { "globals": { "fetch": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ad02b495f..b4a93c2e5 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -700,11 +700,11 @@ }, "@metamask/snap-controllers": { "globals": { + "URL": true, "Worker": true, "clearTimeout": true, "console.error": true, "console.log": true, - "console.warn": true, "fetch": true, "setTimeout": true }, @@ -715,14 +715,22 @@ "@metamask/post-message-stream": true, "@metamask/safe-event-emitter": true, "@metamask/snap-workers": true, + "ajv": true, + "buffer": true, + "concat-stream": true, + "crypto-browserify": true, "deep-freeze-strict": true, "eth-rpc-errors": true, "fast-deep-equal": true, + "gunzip-maybe": true, "immer": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, "nanoid": true, - "pump": true + "pump": true, + "readable-web-to-node-stream": true, + "semver": true, + "tar-stream": true } }, "@ngraveio/bc-ur": { @@ -973,6 +981,11 @@ "define": true } }, + "ajv": { + "packages": { + "fast-deep-equal": true + } + }, "analytics-node": { "globals": { "clearTimeout": true, @@ -1158,6 +1171,7 @@ "bl": { "packages": { "buffer": true, + "inherits": true, "readable-stream": true, "util": true } @@ -1270,6 +1284,16 @@ "btoa": true } }, + "browserify-zlib": { + "packages": { + "assert": true, + "buffer": true, + "pako": true, + "process": true, + "readable-stream": true, + "util": true + } + }, "bs58": { "packages": { "base-x": true @@ -1296,6 +1320,11 @@ "ieee754": true } }, + "buffer-from": { + "packages": { + "buffer": true + } + }, "buffer-split": { "packages": { "buffer-indexof": true @@ -1426,6 +1455,13 @@ "util": true } }, + "concat-stream": { + "packages": { + "buffer": true, + "inherits": true, + "readable-stream": true + } + }, "constant-case": { "packages": { "snake-case": true, @@ -1707,6 +1743,16 @@ "stream-browserify": true } }, + "duplexify": { + "packages": { + "buffer": true, + "end-of-stream": true, + "inherits": true, + "process": true, + "readable-stream": true, + "stream-shift": true + } + }, "elliptic": { "packages": { "bn.js": true, @@ -2212,6 +2258,11 @@ "postMessage": true } }, + "fs-constants": { + "packages": { + "constants-browserify": true + } + }, "fsm-event": { "packages": { "assert": true, @@ -2280,6 +2331,16 @@ "superagent": true } }, + "gunzip-maybe": { + "packages": { + "browserify-zlib": true, + "is-deflate": true, + "is-gzip": true, + "peek-stream": true, + "pumpify": true, + "through2": true + } + }, "hamt-sharding": { "packages": { "is-buffer": true, @@ -3930,6 +3991,14 @@ "sha.js": true } }, + "peek-stream": { + "packages": { + "buffer": true, + "buffer-from": true, + "duplexify": true, + "through2": true + } + }, "peer-book": { "packages": { "bs58": true, @@ -4152,6 +4221,13 @@ "process": true } }, + "pumpify": { + "packages": { + "duplexify": true, + "inherits": true, + "pump": true + } + }, "punycode": { "globals": { "define": true @@ -4465,6 +4541,11 @@ "util-deprecate": true } }, + "readable-web-to-node-stream": { + "packages": { + "readable-stream": true + } + }, "receptacle": { "globals": { "clearTimeout": true, @@ -4786,6 +4867,19 @@ "upper-case": true } }, + "tar-stream": { + "packages": { + "bl": true, + "buffer": true, + "end-of-stream": true, + "fs-constants": true, + "inherits": true, + "process": true, + "readable-stream": true, + "string_decoder": true, + "util": true + } + }, "textarea-caret": { "globals": { "document.body.appendChild": true, diff --git a/package.json b/package.json index a3c969984..b7e74c57e 100644 --- a/package.json +++ b/package.json @@ -113,13 +113,15 @@ "@metamask/eth-ledger-bridge-keyring": "^0.10.0", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/etherscan-link": "^2.1.0", + "@metamask/iframe-execution-environment-service": "^0.9.0", "@metamask/jazzicon": "^2.0.0", "@metamask/logo": "^3.1.1", "@metamask/obs-store": "^5.0.0", "@metamask/post-message-stream": "^4.0.0", "@metamask/providers": "^8.1.1", - "@metamask/rpc-methods": "^0.5.0", - "@metamask/snap-controllers": "^0.4.0", + "@metamask/rpc-methods": "^0.9.0", + "@metamask/slip44": "^2.0.0", + "@metamask/snap-controllers": "^0.9.0", "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.6.2", diff --git a/shared/constants/app.js b/shared/constants/app.js index 928ba95ce..6c099860d 100644 --- a/shared/constants/app.js +++ b/shared/constants/app.js @@ -1,3 +1,5 @@ +import { RestrictedMethods } from './permissions'; + /** * A string representing the type of environment the application is currently running in * popup - When the user click's the icon in their browser's extension bar; the default view @@ -31,7 +33,7 @@ export const PLATFORM_OPERA = 'Opera'; export const MESSAGE_TYPE = { ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain', - ETH_ACCOUNTS: 'eth_accounts', + ETH_ACCOUNTS: RestrictedMethods.eth_accounts, ETH_DECRYPT: 'eth_decrypt', ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey', ETH_REQUEST_ACCOUNTS: 'eth_requestAccounts', @@ -44,6 +46,9 @@ export const MESSAGE_TYPE = { SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain', WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET_LEGACY: 'metamask_watchAsset', + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SNAP_CONFIRM: RestrictedMethods.snap_confirm, + ///: END:ONLY_INCLUDE_IN }; /** @@ -55,6 +60,9 @@ export const SUBJECT_TYPES = { INTERNAL: 'internal', UNKNOWN: 'unknown', WEBSITE: 'website', + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SNAP: 'snap', + ///: END:ONLY_INCLUDE_IN }; export const POLLING_TOKEN_ENVIRONMENT_TYPES = { diff --git a/shared/constants/permissions.js b/shared/constants/permissions.js index 546945a4f..ddfb12e23 100644 --- a/shared/constants/permissions.js +++ b/shared/constants/permissions.js @@ -4,4 +4,21 @@ export const CaveatTypes = Object.freeze({ export const RestrictedMethods = Object.freeze({ eth_accounts: 'eth_accounts', + ///: BEGIN:ONLY_INCLUDE_IN(flask) + snap_confirm: 'snap_confirm', + snap_manageState: 'snap_manageState', + 'snap_getBip44Entropy_*': 'snap_getBip44Entropy_*', + 'wallet_snap_*': 'wallet_snap_*', + ///: END:ONLY_INCLUDE_IN }); + +///: BEGIN:ONLY_INCLUDE_IN(flask) +export const PermissionNamespaces = Object.freeze({ + snap_getBip44Entropy_: 'snap_getBip44Entropy_*', + wallet_snap_: 'wallet_snap_*', +}); + +export const EndowmentPermissions = Object.freeze({ + 'endowment:network-access': 'endowment:network-access', +}); +///: END:ONLY_INCLUDE_IN diff --git a/shared/constants/permissions.test.js b/shared/constants/permissions.test.js new file mode 100644 index 000000000..2c3b408f0 --- /dev/null +++ b/shared/constants/permissions.test.js @@ -0,0 +1,22 @@ +import { endowmentPermissionBuilders } from '@metamask/snap-controllers'; +import { restrictedMethodPermissionBuilders } from '@metamask/rpc-methods'; +import { EndowmentPermissions, RestrictedMethods } from './permissions'; + +describe('EndowmentPermissions', () => { + it('has the expected permission keys', () => { + expect(Object.keys(EndowmentPermissions).sort()).toStrictEqual( + Object.keys(endowmentPermissionBuilders).sort(), + ); + }); +}); + +describe('RestrictedMethods', () => { + it('has the expected permission keys', () => { + expect(Object.keys(RestrictedMethods).sort()).toStrictEqual( + [ + 'eth_accounts', + ...Object.keys(restrictedMethodPermissionBuilders), + ].sort(), + ); + }); +}); diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 69bdcf240..c11ff6027 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -27,8 +27,11 @@ @import 'edit-gas-fee-popover/edit-gas-item/index'; @import 'edit-gas-fee-popover/network-statistics/index'; @import 'edit-gas-fee-popover/network-statistics/status-slider/index'; -@import 'flask/snaps-authorship-pill/index'; @import 'edit-gas-fee-popover/edit-gas-tooltip/index'; +@import 'flask/experimental-area/index'; +@import 'flask/snap-install-warning/index'; +@import 'flask/snap-settings-card/index'; +@import 'flask/snaps-authorship-pill/index'; @import 'gas-customization/gas-modal-page-container/index'; @import 'gas-customization/gas-price-button-group/index'; @import 'gas-customization/index'; @@ -51,7 +54,6 @@ @import 'selected-account/index'; @import 'signature-request/index'; @import 'signature-request-original/index'; -@import 'flask/snap-settings-card/index'; @import 'tab-bar/index'; @import 'token-cell/token-cell'; @import 'token-list-display/token-list-display'; @@ -68,7 +70,6 @@ @import 'wallet-overview/index'; @import 'whats-new-popup/index'; @import 'loading-network-screen/index'; -@import 'flask/experimental-area/index'; @import 'transaction-decoding/index'; @import 'advanced-gas-fee-popover/index'; @import 'advanced-gas-fee-popover/advanced-gas-fee-gas-limit/index'; diff --git a/ui/components/app/flask/snap-install-warning/index.js b/ui/components/app/flask/snap-install-warning/index.js new file mode 100644 index 000000000..172d7db7f --- /dev/null +++ b/ui/components/app/flask/snap-install-warning/index.js @@ -0,0 +1 @@ +export { default } from './snap-install-warning'; diff --git a/ui/components/app/flask/snap-install-warning/index.scss b/ui/components/app/flask/snap-install-warning/index.scss new file mode 100644 index 000000000..2f9830668 --- /dev/null +++ b/ui/components/app/flask/snap-install-warning/index.scss @@ -0,0 +1,30 @@ +.snap-install-warning { + .checkbox-label { + @include H7; + + display: flex; + align-items: flex-start; + gap: 0 16px; + } + + &__content { + padding: 0 16px 24px; + } + + &__footer { + display: flex; + flex-flow: row; + justify-content: center; + flex: 0 0 auto; + width: 100%; + max-height: 40px; + } + + &__footer-button { + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + } +} diff --git a/ui/components/app/flask/snap-install-warning/snap-install-warning.js b/ui/components/app/flask/snap-install-warning/snap-install-warning.js new file mode 100644 index 000000000..1b90ec38d --- /dev/null +++ b/ui/components/app/flask/snap-install-warning/snap-install-warning.js @@ -0,0 +1,79 @@ +import React, { useCallback, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import CheckBox from '../../../ui/check-box/check-box.component'; +import Typography from '../../../ui/typography/typography'; +import { TYPOGRAPHY } from '../../../../helpers/constants/design-system'; +import Popover from '../../../ui/popover'; +import Button from '../../../ui/button'; + +export default function SnapInstallWarning({ onCancel, onSubmit, snapName }) { + const t = useI18nContext(); + const [isConfirmed, setIsConfirmed] = useState(false); + + const onCheckboxClicked = useCallback( + () => setIsConfirmed((confirmedState) => !confirmedState), + [], + ); + + const SnapInstallWarningFooter = () => { + return ( +
+ + +
+ ); + }; + + return ( + } + > +
+ + {t('snapInstallWarningCheck')} + +
+ + +
+
+
+ ); +} + +SnapInstallWarning.propTypes = { + /** + * onCancel handler + */ + onCancel: PropTypes.func, + /** + * onSubmit handler + */ + onSubmit: PropTypes.func, + /** + * Name of snap + */ + snapName: PropTypes.string, +}; diff --git a/ui/components/app/flask/snap-settings-card/snap-settings-card.js b/ui/components/app/flask/snap-settings-card/snap-settings-card.js index 09debc698..95afbc810 100644 --- a/ui/components/app/flask/snap-settings-card/snap-settings-card.js +++ b/ui/components/app/flask/snap-settings-card/snap-settings-card.js @@ -13,12 +13,14 @@ import ToggleButton from '../../../ui/toggle-button'; import Chip from '../../../ui/chip'; import ColorIndicator from '../../../ui/color-indicator'; import Button from '../../../ui/button'; +import Tooltip from '../../../ui/tooltip'; import { COLORS, TYPOGRAPHY, FONT_WEIGHT, ALIGN_ITEMS, + JUSTIFY_CONTENT, DISPLAY, TEXT_ALIGN, } from '../../../../helpers/constants/design-system'; @@ -41,7 +43,7 @@ const SnapSettingsCard = ({ name, description, icon, - dateAdded, + dateAdded = '', version, url, onToggle, @@ -87,12 +89,14 @@ const SnapSettingsCard = ({ {name} - + + + @@ -124,24 +129,26 @@ const SnapSettingsCard = ({ {t('flaskSnapSettingsCardButtonCta')} - - - - } - label={status} - labelProps={{ - color: COLORS.UI4, - margin: [0, 1], - }} - backgroundColor={COLORS.UI1} - className="snap-settings-card__chip" - {...chipProps} - /> + + + + + } + label={status} + labelProps={{ + color: COLORS.UI4, + margin: [0, 1], + }} + backgroundColor={COLORS.UI1} + className="snap-settings-card__chip" + {...chipProps} + /> + diff --git a/ui/components/app/modals/index.scss b/ui/components/app/modals/index.scss index 29d0822bf..0ca2954e6 100644 --- a/ui/components/app/modals/index.scss +++ b/ui/components/app/modals/index.scss @@ -44,4 +44,3 @@ outline: none !important; } } - diff --git a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index a48c22857..ffbbca22d 100644 --- a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -71,11 +71,14 @@ export default class PermissionPageContainerContent extends PureComponent { subjectMetadata, selectedIdentities, allIdentitiesSelected, + selectedPermissions, } = this.props; const { t } = this.context; if (subjectMetadata.extensionId) { return t('externalExtension', [subjectMetadata.extensionId]); + } else if (!selectedPermissions.eth_accounts) { + return t('permissionRequestCapitalized'); } else if (allIdentitiesSelected) { return t('connectToAll', [ this.renderAccountTooltip(t('connectToAllAccounts')), diff --git a/ui/components/app/permissions-connect-header/index.scss b/ui/components/app/permissions-connect-header/index.scss index 1a6bcc6f4..2e2ff2463 100644 --- a/ui/components/app/permissions-connect-header/index.scss +++ b/ui/components/app/permissions-connect-header/index.scss @@ -1,8 +1,5 @@ .permissions-connect-header { - display: flex; flex: 0; - flex-direction: column; - justify-content: center; width: 92%; &__icon { diff --git a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js index 70ec3ce25..33dc5f3b8 100644 --- a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js +++ b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js @@ -1,6 +1,14 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SiteOrigin from '../../ui/site-origin/site-origin'; +import SiteOrigin from '../../ui/site-origin'; +import Box from '../../ui/box'; +import { + FLEX_DIRECTION, + JUSTIFY_CONTENT, +} from '../../../helpers/constants/design-system'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import SnapsAuthorshipPill from '../flask/snaps-authorship-pill'; +///: END:ONLY_INCLUDE_IN export default class PermissionsConnectHeader extends Component { static propTypes = { @@ -8,13 +16,18 @@ export default class PermissionsConnectHeader extends Component { iconName: PropTypes.string.isRequired, siteOrigin: PropTypes.string.isRequired, headerTitle: PropTypes.node, + boxProps: PropTypes.shape({ ...Box.propTypes }), headerText: PropTypes.string, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + npmPackageName: PropTypes.string, + ///: END:ONLY_INCLUDE_IN }; static defaultProps = { iconUrl: null, headerTitle: '', headerText: '', + boxProps: {}, }; renderHeaderIcon() { @@ -28,13 +41,38 @@ export default class PermissionsConnectHeader extends Component { } render() { - const { headerTitle, headerText } = this.props; + const { + boxProps, + headerTitle, + headerText, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + npmPackageName, + ///: END:ONLY_INCLUDE_IN + } = this.props; + ///: BEGIN:ONLY_INCLUDE_IN(flask) + const npmPackageUrl = `https://www.npmjs.com/package/${npmPackageName}`; + ///: END:ONLY_INCLUDE_IN return ( -
+ {this.renderHeaderIcon()}
{headerTitle}
+ { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + npmPackageName ? ( + + ) : null + ///: END:ONLY_INCLUDE_IN + }
{headerText}
-
+
); } } diff --git a/ui/components/app/permissions-connect-permission-list/index.scss b/ui/components/app/permissions-connect-permission-list/index.scss index 510911395..adbe224b7 100644 --- a/ui/components/app/permissions-connect-permission-list/index.scss +++ b/ui/components/app/permissions-connect-permission-list/index.scss @@ -2,7 +2,6 @@ .permission { @include H6; - width: 100%; padding-bottom: 16px; border-bottom: 1px solid var(--Grey-100); display: flex; diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js index 95ed4e912..a0d412368 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js @@ -1,33 +1,129 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; +import { + RestrictedMethods, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + EndowmentPermissions, + PermissionNamespaces, + ///: END:ONLY_INCLUDE_IN +} from '../../../../shared/constants/permissions'; import { useI18nContext } from '../../../hooks/useI18nContext'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import { coinTypeToProtocolName } from '../../../helpers/utils/util'; +///: END:ONLY_INCLUDE_IN + +const UNKNOWN_PERMISSION = Symbol('unknown'); + +/** + * @typedef {Object} PermissionLabelObject + * @property {string} label - The text label. + * @property {string} leftIcon - The left icon. + * @property {string} rightIcon - The right icon. + */ + +/** + * Gets the permission list label dictionary key for the specified permission + * name. + * + * @param {string} permissionName - The name of the permission whose key to + * retrieve. + * @param {Record} permissionDictionary - The + * dictionary object mapping permission keys to label objects. + */ +function getPermissionKey(permissionName, permissionDictionary) { + if (Object.hasOwnProperty.call(permissionDictionary, permissionName)) { + return permissionName; + } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + for (const namespace of Object.keys(PermissionNamespaces)) { + if (permissionName.startsWith(namespace)) { + return PermissionNamespaces[namespace]; + } + } + ///: END:ONLY_INCLUDE_IN + + return UNKNOWN_PERMISSION; +} export default function PermissionsConnectPermissionList({ permissions }) { const t = useI18nContext(); - const PERMISSION_TYPES = useMemo(() => { + const PERMISSION_LIST_VALUES = useMemo(() => { return { - eth_accounts: { + [RestrictedMethods.eth_accounts]: { leftIcon: 'fas fa-eye', - label: t('eth_accounts'), + label: t('permission_ethereumAccounts'), + rightIcon: null, + }, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + [RestrictedMethods.snap_confirm]: { + leftIcon: 'fas fa-user-check', + label: t('permission_customConfirmation'), + rightIcon: null, + }, + [RestrictedMethods['snap_getBip44Entropy_*']]: (permissionName) => { + const coinType = permissionName.split('_').slice(-1); + return { + leftIcon: 'fas fa-door-open', + label: t('permission_manageBip44Keys', [ + coinTypeToProtocolName(coinType) || + `${coinType} (Unrecognized protocol)`, + ]), + rightIcon: null, + }; + }, + [RestrictedMethods.snap_manageState]: { + leftIcon: 'fas fa-download', + label: t('permission_manageState'), + rightIcon: null, + }, + [RestrictedMethods['wallet_snap_*']]: (permissionName) => { + const snapId = permissionName.split('_').slice(-1); + return { + leftIcon: 'fas fa-bolt', + label: t('permission_accessSnap', [snapId]), + rightIcon: null, + }; + }, + [EndowmentPermissions['endowment:network-access']]: { + leftIcon: 'fas fa-wifi', + label: t('permission_accessNetwork'), rightIcon: null, }, + ///: END:ONLY_INCLUDE_IN + [UNKNOWN_PERMISSION]: (permissionName) => { + return { + leftIcon: 'fas fa-times-circle', + label: t('permission_unknown', [permissionName ?? 'undefined']), + rightIcon: null, + }; + }, }; }, [t]); return (
- {Object.keys(permissions).map((permission) => ( -
- - {PERMISSION_TYPES[permission].label} - -
- ))} + {Object.keys(permissions).map((permission) => { + const listValue = + PERMISSION_LIST_VALUES[ + getPermissionKey(permission, PERMISSION_LIST_VALUES) + ]; + + const { label, leftIcon, rightIcon } = + typeof listValue === 'function' ? listValue(permission) : listValue; + + return ( +
+ + {label} + {rightIcon && } +
+ ); + })}
); } PermissionsConnectPermissionList.propTypes = { - permissions: PropTypes.objectOf(PropTypes.bool).isRequired, + permissions: PropTypes.object.isRequired, }; diff --git a/ui/helpers/constants/design-system.js b/ui/helpers/constants/design-system.js index 893e332da..89b0a3801 100644 --- a/ui/helpers/constants/design-system.js +++ b/ui/helpers/constants/design-system.js @@ -113,7 +113,7 @@ export const DISPLAY = { LIST_ITEM: 'list-item', }; -const FRACTIONS = { +export const FRACTIONS = { HALF: '1/2', ONE_THIRD: '1/3', TWO_THIRDS: '2/3', diff --git a/ui/helpers/constants/routes.js b/ui/helpers/constants/routes.js index 1de62a7ff..1969ff49b 100644 --- a/ui/helpers/constants/routes.js +++ b/ui/helpers/constants/routes.js @@ -12,6 +12,8 @@ const ALERTS_ROUTE = '/settings/alerts'; const NETWORKS_ROUTE = '/settings/networks'; const NETWORKS_FORM_ROUTE = '/settings/networks/form'; const ADD_NETWORK_ROUTE = '/settings/networks/add-network'; +const SNAPS_LIST_ROUTE = '/settings/snaps-list'; +const SNAPS_VIEW_ROUTE = '/settings/snaps-view'; const CONTACT_LIST_ROUTE = '/settings/contact-list'; const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact'; const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact'; @@ -28,6 +30,9 @@ const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; const SEND_ROUTE = '/send'; const CONNECT_ROUTE = '/connect'; const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +const CONNECT_SNAP_INSTALL_ROUTE = '/snap-install'; +///: END:ONLY_INCLUDE_IN const CONNECTED_ROUTE = '/connected'; const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'; const SWAPS_ROUTE = '/swaps'; @@ -202,6 +207,8 @@ export { SECURITY_ROUTE, GENERAL_ROUTE, ABOUT_US_ROUTE, + SNAPS_LIST_ROUTE, + SNAPS_VIEW_ROUTE, CONTACT_LIST_ROUTE, CONTACT_EDIT_ROUTE, CONTACT_ADD_ROUTE, @@ -213,6 +220,9 @@ export { INITIALIZE_SEED_PHRASE_INTRO_ROUTE, CONNECT_ROUTE, CONNECT_CONFIRM_PERMISSIONS_ROUTE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + CONNECT_SNAP_INSTALL_ROUTE, + ///: END:ONLY_INCLUDE_IN CONNECTED_ROUTE, CONNECTED_ACCOUNTS_ROUTE, PATH_NAME_MAP, diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index ae2568fbb..9cb8a6ed3 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -4,6 +4,7 @@ import BigNumber from 'bignumber.js'; import * as ethUtil from 'ethereumjs-util'; import { DateTime } from 'luxon'; import { util } from '@metamask/controllers'; +import slip44 from '@metamask/slip44'; import { addHexPrefix } from '../../../app/scripts/lib/util'; import { GOERLI_CHAIN_ID, @@ -580,3 +581,18 @@ export function roundToDecimalPlacesRemovingExtraZeroes( .dec(toBigNumber.dec(numberish).toFixed(numberOfDecimalPlaces)) .toNumber(); } + +/** + * Gets the name of the SLIP-44 protocol corresponding to the specified + * `coin_type`. + * + * @param {string | number} coinType - The SLIP-44 `coin_type` value whose name + * to retrieve. + * @returns {string | undefined} The name of the protocol if found. + */ +export function coinTypeToProtocolName(coinType) { + if (String(coinType) === '1') { + return 'Test Networks'; + } + return slip44[coinType]?.name || undefined; +} diff --git a/ui/hooks/useOriginMetadata.js b/ui/hooks/useOriginMetadata.js index e3365316e..1c4152aba 100644 --- a/ui/hooks/useOriginMetadata.js +++ b/ui/hooks/useOriginMetadata.js @@ -1,5 +1,5 @@ import { useSelector } from 'react-redux'; -import { getSubjectMetadata } from '../selectors'; +import { getTargetSubjectMetadata } from '../selectors'; import { SUBJECT_TYPES } from '../../shared/constants/app'; /** @@ -19,24 +19,35 @@ import { SUBJECT_TYPES } from '../../shared/constants/app'; * current origin */ export function useOriginMetadata(origin) { - const subjectMetadata = useSelector(getSubjectMetadata); + const targetSubjectMetadata = useSelector((state) => + getTargetSubjectMetadata(state, origin), + ); + if (!origin) { return null; } - const url = new URL(origin); - const minimumOriginMetadata = { - host: url.host, - hostname: url.hostname, - origin, - subjectType: SUBJECT_TYPES.UNKNOWN, - }; + let minimumOriginMetadata = null; + try { + const url = new URL(origin); + minimumOriginMetadata = { + host: url.host, + hostname: url.hostname, + origin, + subjectType: SUBJECT_TYPES.UNKNOWN, + }; + } catch (_) { + // do nothing + } - if (subjectMetadata?.[origin]) { + if (targetSubjectMetadata && minimumOriginMetadata) { return { ...minimumOriginMetadata, - ...subjectMetadata[origin], + ...targetSubjectMetadata, }; + } else if (targetSubjectMetadata) { + return targetSubjectMetadata; } + return minimumOriginMetadata; } diff --git a/ui/pages/confirmation/confirmation.scss b/ui/pages/confirmation/confirmation.scss index 7635c87c5..482f1c210 100644 --- a/ui/pages/confirmation/confirmation.scss +++ b/ui/pages/confirmation/confirmation.scss @@ -1,4 +1,5 @@ @import 'components/confirmation-footer/confirmation-footer'; +@import 'templates/flask/snap-confirm/index'; .confirmation-page { width: 100%; @@ -20,7 +21,6 @@ &__content { grid-area: content; - padding: 16px 16px 0; min-width: 0; & > :last-child { diff --git a/ui/pages/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmation/templates/add-ethereum-chain.js index a56565692..2a16852dd 100644 --- a/ui/pages/confirmation/templates/add-ethereum-chain.js +++ b/ui/pages/confirmation/templates/add-ethereum-chain.js @@ -206,6 +206,7 @@ function getValues(pendingApproval, t, actions) { pendingApproval.id, ethErrors.provider.userRejectedRequest(), ), + networkDisplay: true, }; } diff --git a/ui/pages/confirmation/templates/flask/snap-confirm/index.scss b/ui/pages/confirmation/templates/flask/snap-confirm/index.scss new file mode 100644 index 000000000..7fed11cd3 --- /dev/null +++ b/ui/pages/confirmation/templates/flask/snap-confirm/index.scss @@ -0,0 +1,10 @@ +.snap-confirm { + padding: 16px 32px; + border-top: 1px solid var(--Grey-100); + border-bottom: 1px solid var(--Grey-100); + margin: 24px 0 16px 0; + + .text { + font-family: monospace; + } +} diff --git a/ui/pages/confirmation/templates/flask/snap-confirm/snap-confirm.js b/ui/pages/confirmation/templates/flask/snap-confirm/snap-confirm.js new file mode 100644 index 000000000..4a3a2268c --- /dev/null +++ b/ui/pages/confirmation/templates/flask/snap-confirm/snap-confirm.js @@ -0,0 +1,105 @@ +import { + RESIZE, + TYPOGRAPHY, +} from '../../../../../helpers/constants/design-system'; + +function getValues(pendingApproval, t, actions) { + const { prompt, description, textAreaContent } = pendingApproval.requestData; + + return { + content: [ + { + element: 'Typography', + key: 'title', + children: prompt, + props: { + variant: TYPOGRAPHY.H3, + align: 'center', + fontWeight: 'bold', + boxProps: { + margin: [0, 0, 4], + }, + }, + }, + ...(description + ? [ + { + element: 'Typography', + key: 'subtitle', + children: description, + props: { + variant: TYPOGRAPHY.H6, + align: 'center', + boxProps: { + margin: [0, 0, 4], + }, + }, + }, + ] + : []), + ...(textAreaContent + ? [ + { + element: 'div', + key: 'text-area', + children: { + element: 'TextArea', + props: { + // TODO(ritave): Terrible hard-coded height hack. Fixing this to adjust automatically to current window height would + // mean allowing template compoments to change global css, and since the intended use of the template + // renderer was to allow users to build their own UIs, this would be a big no-no. + height: '238px', + value: textAreaContent, + readOnly: true, + resize: RESIZE.VERTICAL, + scrollable: true, + className: 'text', + }, + }, + props: { + className: 'snap-confirm', + }, + }, + ] + : []), + { + element: 'Typography', + key: 'only-interact-with-entities-you-trust', + children: [ + { + element: 'span', + key: 'only-connect-trust', + children: `${t('onlyConnectTrust')} `, + }, + { + element: 'a', + children: t('learnMore'), + key: 'learnMore-a-href', + props: { + href: + 'https://metamask.zendesk.com/hc/en-us/articles/4405506066331-User-guide-Dapps', + target: '__blank', + }, + }, + ], + props: { + variant: TYPOGRAPHY.H7, + align: 'center', + boxProps: { + margin: 0, + }, + }, + }, + ], + approvalText: t('approveButtonText'), + cancelText: t('reject'), + onApprove: () => actions.resolvePendingApproval(pendingApproval.id, true), + onCancel: () => actions.resolvePendingApproval(pendingApproval.id, false), + }; +} + +const snapConfirm = { + getValues, +}; + +export default snapConfirm; diff --git a/ui/pages/confirmation/templates/index.js b/ui/pages/confirmation/templates/index.js index 2bbf54b1e..5b7aa6f78 100644 --- a/ui/pages/confirmation/templates/index.js +++ b/ui/pages/confirmation/templates/index.js @@ -6,10 +6,16 @@ import { } from '../../../store/actions'; import addEthereumChain from './add-ethereum-chain'; import switchEthereumChain from './switch-ethereum-chain'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import snapConfirm from './flask/snap-confirm/snap-confirm'; +///: END:ONLY_INCLUDE_IN const APPROVAL_TEMPLATES = { [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN]: addEthereumChain, [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN]: switchEthereumChain, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + [MESSAGE_TYPE.SNAP_CONFIRM]: snapConfirm, + ///: END:ONLY_INCLUDE_IN }; export const TEMPLATED_CONFIRMATION_MESSAGE_TYPES = Object.keys( diff --git a/ui/pages/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmation/templates/switch-ethereum-chain.js index 4eee4a69f..cd28f21b0 100644 --- a/ui/pages/confirmation/templates/switch-ethereum-chain.js +++ b/ui/pages/confirmation/templates/switch-ethereum-chain.js @@ -85,6 +85,7 @@ function getValues(pendingApproval, t, actions) { pendingApproval.id, ethErrors.provider.userRejectedRequest(), ), + networkDisplay: true, }; } diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index e2b080834..004312a85 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -21,7 +21,13 @@ import WhatsNewPopup from '../../components/app/whats-new-popup'; import RecoveryPhraseReminder from '../../components/app/recovery-phrase-reminder'; import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; import Typography from '../../components/ui/typography/typography'; -import { TYPOGRAPHY, FONT_WEIGHT } from '../../helpers/constants/design-system'; +import { + TYPOGRAPHY, + FONT_WEIGHT, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + COLORS, + ///: END:ONLY_INCLUDE_IN +} from '../../helpers/constants/design-system'; import { ASSET_ROUTE, @@ -105,6 +111,11 @@ export default class Home extends PureComponent { showWhatsNewPopup: PropTypes.bool.isRequired, hideWhatsNewPopup: PropTypes.func.isRequired, notificationsToShow: PropTypes.bool.isRequired, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + errorsToShow: PropTypes.object.isRequired, + shouldShowErrors: PropTypes.bool.isRequired, + removeSnapError: PropTypes.func.isRequired, + ///: END:ONLY_INCLUDE_IN showRecoveryPhraseReminder: PropTypes.bool.isRequired, setRecoveryPhraseReminderHasBeenShown: PropTypes.func.isRequired, setRecoveryPhraseReminderLastShown: PropTypes.func.isRequired, @@ -242,6 +253,11 @@ export default class Home extends PureComponent { setWeb3ShimUsageAlertDismissed, originOfCurrentTab, disableWeb3ShimUsageAlert, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + removeSnapError, + errorsToShow, + shouldShowErrors, + ///: END:ONLY_INCLUDE_IN infuraBlocked, newNetworkAdded, setNewNetworkAdded, @@ -250,6 +266,43 @@ export default class Home extends PureComponent { } = this.props; return ( + { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + shouldShowErrors + ? Object.entries(errorsToShow).map(([errorId, error]) => { + return ( + + + {t('somethingWentWrong')} + + + {t('snapError', [error.message, error.code])} + + + } + onIgnore={async () => { + await removeSnapError(errorId); + }} + ignoreText="Dismiss" + key="home-error-message" + /> + ); + }) + : null + ///: END:ONLY_INCLUDE_IN + } {newCollectibleAddedMessage ? ( { pendingConfirmations, infuraBlocked: getInfuraBlocked(state), notificationsToShow: getSortedNotificationsToShow(state).length > 0, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + errorsToShow: metamask.snapErrors, + shouldShowErrors: Object.entries(metamask.snapErrors || []).length > 0, + ///: END:ONLY_INCLUDE_IN showWhatsNewPopup: getShowWhatsNewPopup(state), showRecoveryPhraseReminder: getShowRecoveryPhraseReminder(state), seedPhraseBackedUp, @@ -140,6 +147,9 @@ const mapDispatchToProps = (dispatch) => ({ } }); }, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + removeSnapError: async (id) => await removeSnapError(id), + ///: END:ONLY_INCLUDE_IN restoreFromThreeBox: (address) => dispatch(restoreFromThreeBox(address)), setShowRestorePromptToFalse: () => dispatch(setShowRestorePromptToFalse()), setConnectedStatusPopoverHasBeenShown: () => diff --git a/ui/pages/home/index.scss b/ui/pages/home/index.scss index f2322d2cb..2625059c6 100644 --- a/ui/pages/home/index.scss +++ b/ui/pages/home/index.scss @@ -152,4 +152,8 @@ background: none; margin-left: 20px; } + + &__error-message { + left: 8px; + } } diff --git a/ui/pages/permissions-connect/flask/snap-install/index.js b/ui/pages/permissions-connect/flask/snap-install/index.js new file mode 100644 index 000000000..563ca9572 --- /dev/null +++ b/ui/pages/permissions-connect/flask/snap-install/index.js @@ -0,0 +1 @@ +export { default } from './snap-install'; diff --git a/ui/pages/permissions-connect/flask/snap-install/index.scss b/ui/pages/permissions-connect/flask/snap-install/index.scss new file mode 100644 index 000000000..5d692bb06 --- /dev/null +++ b/ui/pages/permissions-connect/flask/snap-install/index.scss @@ -0,0 +1,28 @@ +.snap-install { + box-shadow: none; + + .permissions-connect-permission-list { + padding: 0 24px; + + .permission { + padding: 8px 0; + } + } + + .source-code { + @include H7; + + display: flex; + color: var(--ui-4); + + .link { + color: var(--primary-blue); + margin-inline-start: 4px; + cursor: pointer; + } + } + + .page-container__footer { + width: 100%; + } +} diff --git a/ui/pages/permissions-connect/flask/snap-install/snap-install.js b/ui/pages/permissions-connect/flask/snap-install/snap-install.js new file mode 100644 index 000000000..950f8d317 --- /dev/null +++ b/ui/pages/permissions-connect/flask/snap-install/snap-install.js @@ -0,0 +1,153 @@ +import PropTypes from 'prop-types'; +import React, { useCallback, useMemo, useState } from 'react'; +import { PageContainerFooter } from '../../../../components/ui/page-container'; +import PermissionsConnectPermissionList from '../../../../components/app/permissions-connect-permission-list'; +import PermissionsConnectFooter from '../../../../components/app/permissions-connect-footer'; +import PermissionConnectHeader from '../../../../components/app/permissions-connect-header'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import SnapInstallWarning from '../../../../components/app/flask/snap-install-warning'; +import Box from '../../../../components/ui/box/box'; +import { + ALIGN_ITEMS, + BLOCK_SIZES, + BORDER_STYLE, + FLEX_DIRECTION, + JUSTIFY_CONTENT, + TYPOGRAPHY, +} from '../../../../helpers/constants/design-system'; +import Typography from '../../../../components/ui/typography'; + +export default function SnapInstall({ + request, + approveSnapInstall, + rejectSnapInstall, + targetSubjectMetadata, +}) { + const t = useI18nContext(); + + const [isShowingWarning, setIsShowingWarning] = useState(false); + + const onCancel = useCallback(() => rejectSnapInstall(request.metadata.id), [ + request, + rejectSnapInstall, + ]); + + const onSubmit = useCallback(() => approveSnapInstall(request), [ + request, + approveSnapInstall, + ]); + + const npmId = useMemo(() => { + if (!targetSubjectMetadata.origin.startsWith('npm:')) { + return undefined; + } + return targetSubjectMetadata.origin.substring(4); + }, [targetSubjectMetadata]); + + const shouldShowWarning = useMemo( + () => + Boolean( + request.permissions && + Object.keys(request.permissions).find((v) => + v.startsWith('snap_getBip44Entropy_'), + ), + ), + [request.permissions], + ); + + return ( + + + + + + {t('snapRequestsPermission')} + + + + + {targetSubjectMetadata.sourceCode ? ( + <> +
+
{t('areYouDeveloper')}
+
+ global.platform.openTab({ + url: targetSubjectMetadata.sourceCode, + }) + } + > + {t('openSourceCode')} +
+
+ + + + + ) : ( + + + + )} + + setIsShowingWarning(true) : onSubmit + } + submitText={t('approveAndInstall')} + /> +
+ {isShowingWarning && ( + setIsShowingWarning(false)} + onSubmit={onSubmit} + snapName={targetSubjectMetadata.name} + /> + )} +
+ ); +} + +SnapInstall.propTypes = { + request: PropTypes.object.isRequired, + approveSnapInstall: PropTypes.func.isRequired, + rejectSnapInstall: PropTypes.func.isRequired, + targetSubjectMetadata: PropTypes.shape({ + iconUrl: PropTypes.string, + name: PropTypes.string, + origin: PropTypes.string.isRequired, + sourceCode: PropTypes.string, + }).isRequired, +}; diff --git a/ui/pages/permissions-connect/index.scss b/ui/pages/permissions-connect/index.scss index 9876fb73f..5921bee95 100644 --- a/ui/pages/permissions-connect/index.scss +++ b/ui/pages/permissions-connect/index.scss @@ -1,4 +1,5 @@ @import 'choose-account/index'; +@import 'flask/snap-install/index'; @import 'redirect/index'; .permissions-connect { diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 2278af0c4..e6e85d8b0 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -8,6 +8,9 @@ import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; import PermissionPageContainer from '../../components/app/permission-page-container'; import ChooseAccount from './choose-account'; import PermissionsRedirect from './redirect'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import SnapInstall from './flask/snap-install'; +///: END:ONLY_INCLUDE_IN const APPROVE_TIMEOUT = MILLISECOND * 1200; @@ -30,6 +33,11 @@ export default class PermissionConnect extends Component { history: PropTypes.object.isRequired, connectPath: PropTypes.string.isRequired, confirmPermissionPath: PropTypes.string.isRequired, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + snapInstallPath: PropTypes.string.isRequired, + isSnap: PropTypes.bool.isRequired, + ///: END:ONLY_INCLUDE_IN + totalPages: PropTypes.string.isRequired, page: PropTypes.string.isRequired, targetSubjectMetadata: PropTypes.shape({ extensionId: PropTypes.string, @@ -38,6 +46,7 @@ export default class PermissionConnect extends Component { origin: PropTypes.string.isRequired, subjectType: PropTypes.string, }), + isRequestingAccounts: PropTypes.bool.isRequired, }; static defaultProps = { @@ -77,10 +86,17 @@ export default class PermissionConnect extends Component { componentDidMount() { const { + connectPath, + confirmPermissionPath, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + snapInstallPath, + isSnap, + ///: END:ONLY_INCLUDE_IN getCurrentWindowTab, getRequestAccountTabIds, permissionsRequest, history, + isRequestingAccounts, } = this.props; getCurrentWindowTab(); getRequestAccountTabIds(); @@ -94,6 +110,18 @@ export default class PermissionConnect extends Component { if (environmentType === ENVIRONMENT_TYPE_NOTIFICATION) { window.addEventListener('beforeunload', this.beforeUnload); } + + if (history.location.pathname === connectPath && !isRequestingAccounts) { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + if (isSnap) { + history.push(snapInstallPath); + } else { + ///: END:ONLY_INCLUDE_IN + history.push(confirmPermissionPath); + ///: BEGIN:ONLY_INCLUDE_IN(flask) + } + ///: END:ONLY_INCLUDE_IN + } } static getDerivedStateFromProps(props, state) { @@ -165,11 +193,11 @@ export default class PermissionConnect extends Component { renderTopBar() { const { redirecting } = this.state; - const { page } = this.props; + const { page, isRequestingAccounts, totalPages } = this.props; const { t } = this.context; return redirecting ? null : (
- {page === '2' ? ( + {page === '2' && isRequestingAccounts ? (
this.goBack()} @@ -178,9 +206,11 @@ export default class PermissionConnect extends Component { {t('back')}
) : null} -
- {t('xOfY', [page, '2'])} -
+ {isRequestingAccounts ? ( +
+ {t('xOfY', [page, totalPages])} +
+ ) : null}
); } @@ -197,6 +227,9 @@ export default class PermissionConnect extends Component { permissionsRequestId, connectPath, confirmPermissionPath, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + snapInstallPath, + ///: END:ONLY_INCLUDE_IN } = this.props; const { selectedAccountAddresses, @@ -257,6 +290,29 @@ export default class PermissionConnect extends Component { /> )} /> + { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + } + ( + { + approvePermissionsRequest(...args); + this.redirect(true); + }} + rejectSnapInstall={(requestId) => + this.cancelPermissionsRequest(requestId) + } + targetSubjectMetadata={targetSubjectMetadata} + /> + )} + /> + { + ///: END:ONLY_INCLUDE_IN + } )} diff --git a/ui/pages/permissions-connect/permissions-connect.container.js b/ui/pages/permissions-connect/permissions-connect.container.js index 6c8464700..60a7dcfd5 100644 --- a/ui/pages/permissions-connect/permissions-connect.container.js +++ b/ui/pages/permissions-connect/permissions-connect.container.js @@ -1,15 +1,15 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { - getPermissionsRequests, getAccountsWithLabels, getLastConnectedInfo, - getSubjectMetadata, + getPermissionsRequests, getSelectedAddress, + getTargetSubjectMetadata, } from '../../selectors'; import { getNativeCurrency } from '../../ducks/metamask/metamask'; -import { formatDate } from '../../helpers/utils/util'; +import { formatDate, getURLHostName } from '../../helpers/utils/util'; import { approvePermissionsRequest, rejectPermissionsRequest, @@ -20,6 +20,9 @@ import { import { CONNECT_ROUTE, CONNECT_CONFIRM_PERMISSIONS_ROUTE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + CONNECT_SNAP_INSTALL_ROUTE, + ///: END:ONLY_INCLUDE_IN } from '../../helpers/constants/routes'; import { SUBJECT_TYPES } from '../../../shared/constants/app'; import PermissionApproval from './permissions-connect.component'; @@ -38,27 +41,25 @@ const mapStateToProps = (state, ownProps) => { (req) => req.metadata.id === permissionsRequestId, ); + const isRequestingAccounts = Boolean( + permissionsRequest?.permissions.eth_accounts, + ); + const { metadata = {} } = permissionsRequest || {}; const { origin } = metadata; const nativeCurrency = getNativeCurrency(state); - const subjectMetadata = getSubjectMetadata(state); - - let targetSubjectMetadata = null; - if (origin) { - if (subjectMetadata[origin]) { - targetSubjectMetadata = subjectMetadata[origin]; - } else { - const targetUrl = new URL(origin); - targetSubjectMetadata = { - name: targetUrl.hostname, - origin, - iconUrl: null, - extensionId: null, - subjectType: SUBJECT_TYPES.UNKNOWN, - }; - } - } + const targetSubjectMetadata = getTargetSubjectMetadata(state, origin) ?? { + name: getURLHostName(origin) || origin, + origin, + iconUrl: null, + extensionId: null, + subjectType: SUBJECT_TYPES.UNKNOWN, + }; + + ///: BEGIN:ONLY_INCLUDE_IN(flask) + const isSnap = targetSubjectMetadata.subjectType === SUBJECT_TYPES.SNAP; + ///: END:ONLY_INCLUDE_IN const accountsWithLabels = getAccountsWithLabels(state); @@ -74,17 +75,35 @@ const mapStateToProps = (state, ownProps) => { const connectPath = `${CONNECT_ROUTE}/${permissionsRequestId}`; const confirmPermissionPath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_CONFIRM_PERMISSIONS_ROUTE}`; + ///: BEGIN:ONLY_INCLUDE_IN(flask) + const snapInstallPath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_SNAP_INSTALL_ROUTE}`; + ///: END:ONLY_INCLUDE_IN + + let totalPages = 1 + isRequestingAccounts; + ///: BEGIN:ONLY_INCLUDE_IN(flask) + totalPages += isSnap; + ///: END:ONLY_INCLUDE_IN + totalPages = totalPages.toString(); let page = ''; if (pathname === connectPath) { page = '1'; } else if (pathname === confirmPermissionPath) { - page = '2'; + page = isRequestingAccounts ? '2' : '1'; + ///: BEGIN:ONLY_INCLUDE_IN(flask) + } else if (pathname === snapInstallPath) { + page = isRequestingAccounts ? '3' : '2'; + ///: END:ONLY_INCLUDE_IN } else { throw new Error('Incorrect path for permissions-connect component'); } return { + isRequestingAccounts, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + isSnap, + snapInstallPath, + ///: END:ONLY_INCLUDE_IN permissionsRequest, permissionsRequestId, accounts: accountsWithLabels, @@ -96,6 +115,7 @@ const mapStateToProps = (state, ownProps) => { lastConnectedInfo, connectPath, confirmPermissionPath, + totalPages, page, targetSubjectMetadata, }; diff --git a/ui/pages/settings/flask/snaps-list-tab/index.js b/ui/pages/settings/flask/snaps-list-tab/index.js new file mode 100644 index 000000000..70e07b686 --- /dev/null +++ b/ui/pages/settings/flask/snaps-list-tab/index.js @@ -0,0 +1 @@ +export { default } from './snap-list-tab'; diff --git a/ui/pages/settings/flask/snaps-list-tab/index.scss b/ui/pages/settings/flask/snaps-list-tab/index.scss new file mode 100644 index 000000000..d3bb0e2af --- /dev/null +++ b/ui/pages/settings/flask/snaps-list-tab/index.scss @@ -0,0 +1,21 @@ +.snap-list-tab { + width: 100%; + height: 100%; + + &__wrapper { + width: auto; + } + + &__body { + padding: 12px 18px; + + @media screen and (min-width: $break-large) { + padding: 12px; + } + } + + .snap-settings-card { + margin: 8px 0; + max-width: 344px; + } +} diff --git a/ui/pages/settings/flask/snaps-list-tab/snap-list-tab.js b/ui/pages/settings/flask/snaps-list-tab/snap-list-tab.js new file mode 100644 index 000000000..7f5736504 --- /dev/null +++ b/ui/pages/settings/flask/snaps-list-tab/snap-list-tab.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import SnapSettingsCard from '../../../../components/app/flask/snap-settings-card'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import Typography from '../../../../components/ui/typography/typography'; +import { + TYPOGRAPHY, + COLORS, + FLEX_DIRECTION, + JUSTIFY_CONTENT, + ALIGN_ITEMS, +} from '../../../../helpers/constants/design-system'; +import Box from '../../../../components/ui/box'; +import { SNAPS_VIEW_ROUTE } from '../../../../helpers/constants/routes'; +import { disableSnap, enableSnap } from '../../../../store/actions'; +import { getSnaps } from '../../../../selectors'; + +const SnapListTab = () => { + const t = useI18nContext(); + const history = useHistory(); + const dispatch = useDispatch(); + const snaps = useSelector(getSnaps); + const onClick = (snap) => { + const route = `${SNAPS_VIEW_ROUTE}/${window.btoa( + unescape(encodeURIComponent(snap.id)), + )}`; + history.push(route); + }; + const onToggle = (snap) => { + if (snap.enabled) { + dispatch(disableSnap(snap.id)); + } else { + dispatch(enableSnap(snap.id)); + } + }; + + return ( +
+ {Object.entries(snaps).length ? ( +
+ + + {t('expandExperience')} + + + {t('manageSnaps')} + + +
+ {Object.entries(snaps).map(([key, snap]) => { + return ( + { + onToggle(snap); + }} + description={snap.manifest.description} + url={snap.id} + name={snap.manifest.proposedName} + status={snap.status} + version={snap.version} + onClick={() => { + onClick(snap); + }} + /> + ); + })} +
+
+ ) : ( + + + {t('noSnaps')} + + + )} +
+ ); +}; + +export default SnapListTab; diff --git a/ui/pages/settings/flask/snaps-list-tab/snap-list-tab.stories.js b/ui/pages/settings/flask/snaps-list-tab/snap-list-tab.stories.js new file mode 100644 index 000000000..452b7754b --- /dev/null +++ b/ui/pages/settings/flask/snaps-list-tab/snap-list-tab.stories.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../../store/store'; +import testData from '../../../../../.storybook/test-data'; +import SnapListTab from './snap-list-tab'; + +// Using Test Data For Redux +const store = configureStore(testData); + +export default { + title: 'Pages/Settings/SnapListTab', + id: __filename, + decorators: [(story) => {story()}], + argTypes: { + onToggle: { + action: 'onToggle', + }, + onRemove: { + action: 'onRemove', + }, + }, +}; +export const DefaultStory = (args) => { + const state = store.getState(); + const [viewingSnap, setViewingSnap] = useState(); + const [snap, setSnap] = useState(); + + return ( +
+ { + setSnap(s); + setViewingSnap(true); + }} + /> +
+ ); +}; +const state = store.getState(); +DefaultStory.args = { + snaps: state.metamask.snaps, + viewingSnap: false, +}; +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/settings/flask/view-snap/index.js b/ui/pages/settings/flask/view-snap/index.js new file mode 100644 index 000000000..520826cbb --- /dev/null +++ b/ui/pages/settings/flask/view-snap/index.js @@ -0,0 +1 @@ +export { default } from './view-snap'; diff --git a/ui/pages/settings/flask/view-snap/index.scss b/ui/pages/settings/flask/view-snap/index.scss new file mode 100644 index 000000000..d0efae6f7 --- /dev/null +++ b/ui/pages/settings/flask/view-snap/index.scss @@ -0,0 +1,114 @@ +.view-snap { + padding: 12px 18px; + + @media screen and (min-width: $break-large) { + padding: 12px; + } + + &__subheader { + @include H4; + + padding: 16px 4px; + border-bottom: 1px solid var(--alto); + margin-right: 24px; + height: 72px; + align-items: center; + display: flex; + flex-flow: row nowrap; + + @media screen and (max-width: $break-small) { + margin-right: 0; + padding: 0 0 16px; + flex-direction: column; + align-items: center; + gap: 8px; + height: max-content; + } + } + + &__title { + @media screen and (max-width: $break-small) { + padding-bottom: 16px; + } + } + + &__pill-toggle-container { + align-items: center; + display: flex; + flex-grow: 1; + + @media screen and (max-width: $break-small) { + width: 100%; + justify-content: space-between; + } + } + + &__pill-container { + @media screen and (max-width: $break-small) { + padding-left: 0; + display: inline-block; + } + } + + &__toggle-container { + @media screen and (max-width: $break-small) { + padding-left: 0; + display: inline-block; + } + } + + &__content-container { + @media screen and (max-width: $break-small) { + width: 100%; + } + } + + &__section { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + border-bottom: 1px solid var(--Grey-100); + padding-bottom: 16px; + margin-bottom: 16px; + + @media screen and (max-width: $break-small) { + height: initial; + padding: 5px 0 16px; + } + + .connected-sites-list__content-row { + border-top: none; + border-bottom: 1px solid var(--ui-2); + + &:last-child { + border-bottom: none; + } + } + + &:last-child { + margin-bottom: 0; + border-bottom: none; + } + } + + &__permission-list { + padding-bottom: 0; + + .permission { + padding-top: 16px; + + &:last-child { + border-bottom: none; + } + } + } + + &__remove-button { + max-width: 175px; + + @media screen and (max-width: $break-small) { + align-self: center; + } + } +} diff --git a/ui/pages/settings/flask/view-snap/view-snap.js b/ui/pages/settings/flask/view-snap/view-snap.js new file mode 100644 index 000000000..14c2b050e --- /dev/null +++ b/ui/pages/settings/flask/view-snap/view-snap.js @@ -0,0 +1,171 @@ +import React, { useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import Button from '../../../../components/ui/button'; +import Typography from '../../../../components/ui/typography'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + TYPOGRAPHY, + COLORS, + TEXT_ALIGN, + FRACTIONS, +} from '../../../../helpers/constants/design-system'; +import SnapsAuthorshipPill from '../../../../components/app/flask/snaps-authorship-pill'; +import Box from '../../../../components/ui/box'; +import ToggleButton from '../../../../components/ui/toggle-button'; +import PermissionsConnectPermissionList from '../../../../components/app/permissions-connect-permission-list/permissions-connect-permission-list'; +import ConnectedSitesList from '../../../../components/app/connected-sites-list'; +import Tooltip from '../../../../components/ui/tooltip'; +import { SNAPS_LIST_ROUTE } from '../../../../helpers/constants/routes'; +import { + disableSnap, + enableSnap, + removeSnap, + removePermissionsFor, +} from '../../../../store/actions'; +import { getSnaps, getSubjectsWithPermission } from '../../../../selectors'; + +function ViewSnap() { + const t = useI18nContext(); + const history = useHistory(); + const location = useLocation(); + const { pathname } = location; + const pathNameTail = pathname.match(/[^/]+$/u)[0]; + const snaps = useSelector(getSnaps); + const snap = Object.entries(snaps) + .map(([_, snapState]) => snapState) + .find((snapState) => { + const decoded = decodeURIComponent(escape(window.atob(pathNameTail))); + return snapState.id === decoded; + }); + + useEffect(() => { + if (!snap) { + history.push(SNAPS_LIST_ROUTE); + } + }, [history, snap]); + + const authorshipPillUrl = `https://npmjs.com/package/${snap?.manifest.source.location.npm.packageName}`; + const connectedSubjects = useSelector((state) => + getSubjectsWithPermission(state, snap?.permissionName), + ); + const dispatch = useDispatch(); + const onDisconnect = (connectedOrigin, snapPermissionName) => { + dispatch( + removePermissionsFor({ + [connectedOrigin]: [snapPermissionName], + }), + ); + }; + const onToggle = () => { + if (snap.enabled) { + dispatch(disableSnap(snap.id)); + } else { + dispatch(enableSnap(snap.id)); + } + }; + + if (!snap) { + return null; + } + return ( +
+
+
+ + {snap.manifest.proposedName} + + + + + + + + + + + +
+ +
+ + {snap.manifest.description} + +
+
+ {t('permissions')} + + {t('snapAccess', [snap.manifest.proposedName])} + + + + +
+
+ + + {t('connectedSites')} + + + {t('connectedSnapSites', [snap.manifest.proposedName])} + + { + onDisconnect(origin, snap.permissionName); + }} + /> + +
+
+ {t('removeSnap')} + + {t('removeSnapDescription')} + + +
+
+
+
+ ); +} + +export default React.memo(ViewSnap); diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index f257c79af..d5864e2a8 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -3,6 +3,8 @@ @import 'networks-tab/index'; @import 'settings-tab/index'; @import 'contact-list-tab/index'; +@import 'flask/snaps-list-tab/index'; +@import 'flask/view-snap/index'; .settings-page { position: relative; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index fd94271ba..7d6f37458 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -11,6 +11,10 @@ import { ABOUT_US_ROUTE, SETTINGS_ROUTE, NETWORKS_ROUTE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SNAPS_VIEW_ROUTE, + SNAPS_LIST_ROUTE, + ///: END:ONLY_INCLUDE_IN CONTACT_LIST_ROUTE, CONTACT_ADD_ROUTE, CONTACT_EDIT_ROUTE, @@ -26,6 +30,10 @@ import InfoTab from './info-tab'; import SecurityTab from './security-tab'; import ContactListTab from './contact-list-tab'; import ExperimentalTab from './experimental-tab'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import SnapListTab from './flask/snaps-list-tab'; +import ViewSnap from './flask/view-snap'; +///: END:ONLY_INCLUDE_IN class SettingsPage extends PureComponent { static propTypes = { @@ -35,6 +43,7 @@ class SettingsPage extends PureComponent { history: PropTypes.object, isAddressEntryPage: PropTypes.bool, isPopup: PropTypes.bool, + isSnapViewPage: PropTypes.bool, pathnameI18nKey: PropTypes.string, initialBreadCrumbRoute: PropTypes.string, breadCrumbTextKey: PropTypes.string, @@ -74,8 +83,8 @@ class SettingsPage extends PureComponent { currentPath, mostRecentOverviewPage, addNewNetwork, + isSnapViewPage, } = this.props; - return (
- {this.renderSubHeader()} + {isSnapViewPage ? null : this.renderSubHeader()} {this.renderContent()}
@@ -116,11 +125,16 @@ class SettingsPage extends PureComponent { renderTitle() { const { t } = this.context; - const { isPopup, pathnameI18nKey, addressName } = this.props; - + const { + isPopup, + pathnameI18nKey, + addressName, + isSnapViewPage, + } = this.props; let titleText; - - if (isPopup && addressName) { + if (isSnapViewPage) { + titleText = t('snaps'); + } else if (isPopup && addressName) { titleText = t('details'); } else if (pathnameI18nKey && isPopup) { titleText = t(pathnameI18nKey); @@ -152,7 +166,7 @@ class SettingsPage extends PureComponent { } else if (initialBreadCrumbKey) { subheaderText = t(initialBreadCrumbKey); } else { - subheaderText = t(pathnameI18nKey || 'contacts'); + subheaderText = t(pathnameI18nKey || 'general'); } return ( @@ -207,6 +221,18 @@ class SettingsPage extends PureComponent { content: t('contacts'), key: CONTACT_LIST_ROUTE, }, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + { + icon: ( + {t('snapsSettingsDescription')} + ), + content: t('snaps'), + key: SNAPS_LIST_ROUTE, + }, + ///: END:ONLY_INCLUDE_IN { icon: , content: t('securityAndPrivacy'), @@ -280,6 +306,16 @@ class SettingsPage extends PureComponent { path={`${CONTACT_VIEW_ROUTE}/:id`} component={ContactListTab} /> + { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + + ///: END:ONLY_INCLUDE_IN + } + { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + + ///: END:ONLY_INCLUDE_IN + } ( { } = state; const pathNameTail = pathname.match(/[^/]+$/u)[0]; - const isAddressEntryPage = pathNameTail.includes('0x'); + const isSnapViewPage = Boolean(pathname.match(SNAPS_VIEW_ROUTE)); const isAddContactPage = Boolean(pathname.match(CONTACT_ADD_ROUTE)); const isEditContactPage = Boolean(pathname.match(CONTACT_EDIT_ROUTE)); const isNetworksFormPage = @@ -71,6 +75,8 @@ const mapStateToProps = (state, ownProps) => { backRoute = CONTACT_LIST_ROUTE; } else if (isNetworksFormPage) { backRoute = NETWORKS_ROUTE; + } else if (isSnapViewPage) { + backRoute = SNAPS_LIST_ROUTE; } let initialBreadCrumbRoute; @@ -96,6 +102,7 @@ const mapStateToProps = (state, ownProps) => { mostRecentOverviewPage: getMostRecentOverviewPage(state), addNewNetwork, conversionDate, + isSnapViewPage, }; }; diff --git a/ui/pages/settings/settings.stories.js b/ui/pages/settings/settings.stories.js index 0fcfbb87e..19aa64469 100644 --- a/ui/pages/settings/settings.stories.js +++ b/ui/pages/settings/settings.stories.js @@ -15,6 +15,8 @@ import { NETWORKS_ROUTE, SECURITY_ROUTE, SETTINGS_ROUTE, + SNAPS_LIST_ROUTE, + SNAPS_VIEW_ROUTE, } from '../../helpers/constants/routes'; import SettingsPage from './settings.component'; @@ -38,6 +40,8 @@ const ROUTES_TO_I18N_KEYS = { [CONTACT_ADD_ROUTE]: 'newContact', [CONTACT_EDIT_ROUTE]: 'editContact', [CONTACT_LIST_ROUTE]: 'contacts', + [SNAPS_LIST_ROUTE]: 'snaps', + [SNAPS_VIEW_ROUTE]: 'snaps', [CONTACT_VIEW_ROUTE]: 'viewContact', [NETWORKS_ROUTE]: 'networks', [NETWORKS_FORM_ROUTE]: 'networks', @@ -54,9 +58,9 @@ const Settings = ({ history }) => { location.pathname === '/iframe.html' ? '/settings/general' : location.pathname; - const pathnameI18nKey = ROUTES_TO_I18N_KEYS[pathname]; - const backRoute = SETTINGS_ROUTE; + const isSnapViewPage = Boolean(pathname.match(SNAPS_VIEW_ROUTE)); + const backRoute = isSnapViewPage ? SNAPS_LIST_ROUTE : SETTINGS_ROUTE; return (
@@ -66,6 +70,7 @@ const Settings = ({ history }) => { history={history} pathnameI18nKey={pathnameI18nKey} backRoute={backRoute} + isSnapViewPage={isSnapViewPage} />
); diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index 09d845d5c..be0543ef5 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -4,6 +4,7 @@ import { getOriginOfCurrentTab, getSelectedAddress, getSubjectMetadata, + getTargetSubjectMetadata, } from '.'; // selectors @@ -96,6 +97,27 @@ export function getConnectedSubjectsForSelectedAddress(state) { return connectedSubjects; } +export function getSubjectsWithPermission(state, permissionName) { + const subjects = getPermissionSubjects(state); + + const connectedSubjects = []; + + Object.entries(subjects).forEach(([origin, { permissions }]) => { + if (permissions[permissionName]) { + const { extensionId, name, iconUrl } = + getTargetSubjectMetadata(state, origin) || {}; + + connectedSubjects.push({ + extensionId, + origin, + name, + iconUrl, + }); + } + }); + return connectedSubjects; +} + /** * Returns an object mapping addresses to objects mapping origins to connected * subject info. Subject info objects have the following properties: diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index f61daadd2..c287422e9 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1,4 +1,7 @@ import { createSelector } from 'reselect'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import { memoize } from 'lodash'; +///: END:ONLY_INCLUDE_IN import { addHexPrefix } from '../../app/scripts/lib/util'; import { MAINNET_CHAIN_ID, @@ -16,13 +19,20 @@ import { TRANSPORT_STATES, } from '../../shared/constants/hardware-wallets'; +import { + MESSAGE_TYPE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + SUBJECT_TYPES, + ///: END:ONLY_INCLUDE_IN +} from '../../shared/constants/app'; + +import { TRUNCATED_NAME_CHAR_LIMIT } from '../../shared/constants/labels'; + import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, ALLOWED_SWAPS_CHAIN_IDS, } from '../../shared/constants/swaps'; -import { TRUNCATED_NAME_CHAR_LIMIT } from '../../shared/constants/labels'; - import { shortenAddress, getAccountByAddress, @@ -50,7 +60,6 @@ import { getLedgerWebHidConnectedStatus, getLedgerTransportStatus, } from '../ducks/app/app'; -import { MESSAGE_TYPE } from '../../shared/constants/app'; /** * One of the only remaining valid uses of selecting the network subkey of the @@ -526,6 +535,31 @@ export function getSubjectMetadata(state) { return state.metamask.subjectMetadata; } +///: BEGIN:ONLY_INCLUDE_IN(flask) +/** + * @param {string} svgString - The raw SVG string to make embeddable. + * @returns {string} The embeddable SVG string. + */ +const getEmbeddableSvg = memoize( + (svgString) => `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`, +); +///: END:ONLY_INCLUDE_IN + +export function getTargetSubjectMetadata(state, origin) { + const metadata = getSubjectMetadata(state)[origin]; + + ///: BEGIN:ONLY_INCLUDE_IN(flask) + if (metadata?.subjectType === SUBJECT_TYPES.SNAP) { + const { svgIcon, ...remainingMetadata } = metadata; + return { + ...remainingMetadata, + iconUrl: svgIcon ? getEmbeddableSvg(svgIcon) : null, + }; + } + ///: END:ONLY_INCLUDE_IN + return metadata; +} + export function getRpcPrefsForCurrentProvider(state) { const { frequentRpcListDetail, provider } = state.metamask; const selectRpcInfo = frequentRpcListDetail.find( @@ -650,6 +684,12 @@ export function getShowWhatsNewPopup(state) { return state.appState.showWhatsNewPopup; } +///: BEGIN:ONLY_INCLUDE_IN(flask) +export function getSnaps(state) { + return state.metamask.snaps; +} +///: END:ONLY_INCLUDE_IN + /** * Get an object of notification IDs and if they are allowed or not. * diff --git a/ui/store/actions.js b/ui/store/actions.js index 0899f0dbc..71ba543e3 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -807,6 +807,33 @@ export function txError(err) { }; } +///: BEGIN:ONLY_INCLUDE_IN(flask) +export function disableSnap(snapId) { + return async (dispatch) => { + await promisifiedBackground.disableSnap(snapId); + await forceUpdateMetamaskState(dispatch); + }; +} + +export function enableSnap(snapId) { + return async (dispatch) => { + await promisifiedBackground.enableSnap(snapId); + await forceUpdateMetamaskState(dispatch); + }; +} + +export function removeSnap(snap) { + return async (dispatch) => { + await promisifiedBackground.removeSnap(snap); + await forceUpdateMetamaskState(dispatch); + }; +} + +export async function removeSnapError(msgData) { + return promisifiedBackground.removeSnapError(msgData); +} +///: END:ONLY_INCLUDE_IN + export function cancelMsg(msgData) { return async (dispatch) => { dispatch(showLoadingIndication()); diff --git a/yarn.lock b/yarn.lock index d98e508cc..035f99887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2586,49 +2586,12 @@ semver "^7.3.5" yargs "^17.0.1" -"@metamask/contract-metadata@^1.29.0", "@metamask/contract-metadata@^1.31.0": +"@metamask/contract-metadata@^1.31.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.31.0.tgz#9e3e46de7a955ea1ca61f7db20d9a17b5e91d3d0" integrity sha512-4FBJkg/vDiYp/thIiZknxrJ0lfsj2eWIPenwlNZmoqOhoL4VqhK5eKWxi+EuGMvv9taP+QBRk6Key7wC1uL78A== -"@metamask/controllers@^17.0.0": - version "17.0.0" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-17.0.0.tgz#7ef00b4f7583d8075115e8a2f074d7b66646bbe8" - integrity sha512-myPlAk8SpNm5SwHHKGgm2XDLP4bxNR2UsKoQlYtV7bJq3l8FV1agSFwHBwDhg61/52Xvqdqy+1YDVdV3kOwPgg== - dependencies: - "@ethereumjs/common" "^2.3.1" - "@ethereumjs/tx" "^3.2.1" - "@metamask/contract-metadata" "^1.29.0" - "@types/uuid" "^8.3.0" - abort-controller "^3.0.0" - async-mutex "^0.2.6" - babel-runtime "^6.26.0" - eth-ens-namehash "^2.0.8" - eth-json-rpc-infura "^5.1.0" - eth-keyring-controller "^6.2.1" - eth-method-registry "1.1.0" - eth-phishing-detect "^1.1.14" - eth-query "^2.1.2" - eth-rpc-errors "^4.0.0" - eth-sig-util "^3.0.0" - ethereumjs-util "^7.0.10" - ethereumjs-wallet "^1.0.1" - ethers "^5.4.1" - ethjs-unit "^0.1.6" - ethjs-util "^0.1.6" - human-standard-collectible-abi "^1.0.2" - human-standard-token-abi "^2.0.0" - immer "^9.0.6" - isomorphic-fetch "^3.0.0" - jsonschema "^1.2.4" - nanoid "^3.1.12" - punycode "^2.1.1" - single-call-balance-checker-abi "^1.0.0" - uuid "^8.3.2" - web3 "^0.20.7" - web3-provider-engine "^16.0.3" - -"@metamask/controllers@^25.0.0": +"@metamask/controllers@^25.0.0", "@metamask/controllers@^25.1.0": version "25.1.0" resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-25.1.0.tgz#2efee24a9a2b03ab2a2b0422c8f250931c269560" integrity sha512-syndn2lIhtlACzaqjDrw23dJzw8pZ6en4Cr35C7B9RRS87EhahUqkPP73moAzLtvbyqtBlAUO1HHrqV3lw4E5g== @@ -2717,6 +2680,24 @@ resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.1.0.tgz#13829d8244bbf19ea658c0b20d21a77b67de0bdd" integrity sha512-Hggj4y0QIjDzKGTXzarhEPIQyFSB2bi2y6YLJNwaT4JmP30UB5Cj6gqoY0M4pj3QT57fzp0BUuGp7F/AUe28tw== +"@metamask/iframe-execution-environment-service@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@metamask/iframe-execution-environment-service/-/iframe-execution-environment-service-0.9.0.tgz#721e15ee4651741a599940dbcfa524cc55eaaa47" + integrity sha512-a240sg83sX1dxfBDdRd0uoujaN4V9VtHKELMcTMgpYCI0uE83//Q01a7L8MiBtLhzr8o4D/xXRUIDR0Y9NKc3Q== + dependencies: + "@metamask/controllers" "^25.1.0" + "@metamask/object-multiplex" "^1.2.0" + "@metamask/post-message-stream" "^4.0.0" + "@metamask/snap-controllers" "^0.9.0" + "@metamask/snap-types" "^0.9.0" + "@metamask/snap-workers" "^0.9.0" + eth-rpc-errors "^4.0.3" + json-rpc-engine "^6.1.0" + json-rpc-middleware-stream "^3.0.0" + nanoid "^3.1.31" + pump "^3.0.0" + stream "^0.0.2" + "@metamask/jazzicon@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/jazzicon/-/jazzicon-2.0.0.tgz#5615528e91c0fc5c9d79202d1f0954a7922525a0" @@ -2747,10 +2728,10 @@ resolved "https://registry.yarnpkg.com/@metamask/metamask-eth-abis/-/metamask-eth-abis-2.1.0.tgz#316c2e72373506f1a0120b76e432760a27eb6806" integrity sha512-T8LBEB0PQo0N1tZQKZ2K8BGmv+IDLcXkzt8Pn7x0YnwZD6YpCIvKqYM3iy2fJ6wFXeCvRKqpn4K6EqwnkSJAbQ== -"@metamask/object-multiplex@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-1.1.0.tgz#6b1507c4d10caafd2ea82dd2a5360b91631e036e" - integrity sha512-ImDw5+NdO5qnzmK/rpSlPmQMQm6HIC6wAHdR9nBaDK8TpeuRik5H8DCUcoNrxSeUAk1iHwchZ03lpZu6mZfrdw== +"@metamask/object-multiplex@^1.1.0", "@metamask/object-multiplex@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-1.2.0.tgz#38fc15c142f61939391e1b9a8eed679696c7e4f4" + integrity sha512-hksV602d3NWE2Q30Mf2Np1WfVKaGqfJRy9vpHAmelbaD0OkDt06/0KQkRR6UVYdMbTbkuEu8xN5JDUU80inGwQ== dependencies: end-of-stream "^1.4.4" once "^1.4.0" @@ -2765,15 +2746,6 @@ readable-stream "^2.2.2" through2 "^2.0.3" -"@metamask/obs-store@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-6.0.2.tgz#1fbc458cc617557a4557f9ab58e6676c474df2b1" - integrity sha512-MjnP+xNZGBx46YZrR8ZYPb+ScPfxJUbs09MTByuQKxMsf7Lxz17oBTI5ZMkOZOTSBBxhknKdjJg+nAM8mMopwg== - dependencies: - "@metamask/safe-event-emitter" "^2.0.0" - readable-stream "^2.2.2" - through2 "^2.0.3" - "@metamask/obs-store@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-7.0.0.tgz#6cae5f28306bb3e83a381bc9ae22682316095bd3" @@ -2807,13 +2779,13 @@ pump "^3.0.0" webextension-polyfill-ts "^0.25.0" -"@metamask/rpc-methods@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@metamask/rpc-methods/-/rpc-methods-0.5.0.tgz#3c0073d80e68eceb8b9fa19bea0b2daef8638a42" - integrity sha512-OFGd4T20dYTYxdB8WK0xa6FXRaNmJR5mAS7Wp7+6n8rqKljKJ0jDyfpGia1YKI6gKsB7Xdn5efnWxviuF/XQXQ== +"@metamask/rpc-methods@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@metamask/rpc-methods/-/rpc-methods-0.9.0.tgz#eb55cc39d2ea9a663211e8d805bdf566af70c764" + integrity sha512-wii0TMuRscet8+x3tqfAcEmY0TrMFzOnD3QFpFVUy3fznv4b/EzDD/XLQToafd2yUaDjUrrS9FHwU9omqzPxcg== dependencies: "@metamask/key-tree" "^3.0.1" - "@metamask/snap-controllers" "^0.5.0" + "@metamask/snap-controllers" "^0.9.0" eth-rpc-errors "^4.0.2" "@metamask/safe-event-emitter@^2.0.0": @@ -2821,57 +2793,49 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q== -"@metamask/snap-controllers@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@metamask/snap-controllers/-/snap-controllers-0.4.0.tgz#20647f061e20263f462347c2bf0d59f7aedd6898" - integrity sha512-pZ9S72Y9u2KBrMdzFauz6t4LIxJBqcT6uzxstxdY8y0qroeeTJum6Z0L9HFkVSsTLP3JMyVSDD6FwRsHIwXewg== - dependencies: - "@metamask/controllers" "^17.0.0" - "@metamask/object-multiplex" "^1.1.0" - "@metamask/obs-store" "^6.0.2" - "@metamask/post-message-stream" "4.0.0" - "@metamask/safe-event-emitter" "^2.0.0" - "@metamask/snap-workers" "^0.4.0" - "@types/deep-freeze-strict" "^1.1.0" - deep-freeze-strict "^1.1.1" - eth-rpc-errors "^4.0.2" - fast-deep-equal "^3.1.3" - immer "^9.0.6" - json-rpc-engine "^6.1.0" - json-rpc-middleware-stream "^3.0.0" - nanoid "^3.1.28" - pump "^3.0.0" +"@metamask/slip44@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-2.0.0.tgz#1b646a1418af341d5ea979c28015a817ff23af33" + integrity sha512-eRomm783ti/1b/TlNnlTCUkYRuTaMYkeTAG0z2rt/WyT8UzxY+8+v/kbl9vk5qhDHeclzBrd9gbqLnLU1kh+Ow== -"@metamask/snap-controllers@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@metamask/snap-controllers/-/snap-controllers-0.5.0.tgz#a77563ea7bea0ba7c6fd73059f9ecbf86be16fd4" - integrity sha512-eKuKQh17LrHfkpk8T5J87jg4TTmnoG65JpPHgpV+GLyohF4CMEtyK6uJYoPcm5c3z7IArcYbuucc1bqLbz9JoA== +"@metamask/snap-controllers@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@metamask/snap-controllers/-/snap-controllers-0.9.0.tgz#e0006fc9991e995dd86dff792106990aae2aeda0" + integrity sha512-os3fEai0w4ctpyy6ExlthY8tnww98Vm+RVwOZgrCKDY5dAXqlSXpyWc1uOfkQyiPhUEJtdznJTWzaWzNIO9MfQ== dependencies: - "@metamask/controllers" "^17.0.0" + "@metamask/controllers" "^25.1.0" "@metamask/object-multiplex" "^1.1.0" "@metamask/obs-store" "^7.0.0" "@metamask/post-message-stream" "4.0.0" "@metamask/safe-event-emitter" "^2.0.0" - "@metamask/snap-workers" "^0.5.0" + "@metamask/snap-workers" "^0.9.0" "@types/deep-freeze-strict" "^1.1.0" + ajv "^8.8.2" + concat-stream "^2.0.0" deep-freeze-strict "^1.1.1" eth-rpc-errors "^4.0.2" fast-deep-equal "^3.1.3" + gunzip-maybe "^1.4.2" immer "^9.0.6" json-rpc-engine "^6.1.0" json-rpc-middleware-stream "^3.0.0" - nanoid "^3.1.28" + nanoid "^3.1.31" pump "^3.0.0" + readable-web-to-node-stream "^3.0.2" + semver "^7.3.5" + tar-stream "^2.2.0" -"@metamask/snap-workers@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@metamask/snap-workers/-/snap-workers-0.4.0.tgz#ba561eb15a7b7e7b353738ad5635a68c03cf64b0" - integrity sha512-usPEnwRXIwaDc06f8Jis4/CxXzmZJpPOLucOMqkxGAAz3hepA/T5fbfus12sibo5h6QsG0VTqBQ5AqKFlTr0zQ== +"@metamask/snap-types@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@metamask/snap-types/-/snap-types-0.9.0.tgz#aa164111be1b5c53fbaaf03c1bccbdbd0741daa4" + integrity sha512-pK4tvurUhcKMEkTD0XvQze5HCbtrgmpFWDztBekNIMJTXDrnYIEw4Dxn+LwCX7WJ0DN/03brQSEzmIrYbcBw7Q== + dependencies: + "@metamask/controllers" "^25.1.0" -"@metamask/snap-workers@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@metamask/snap-workers/-/snap-workers-0.5.0.tgz#9f1b8243f64819e40d66e659d580b6da59cb8015" - integrity sha512-sR30/nmkndPeLox282BdTNnU3g6Mo5Gt8rdr6PUSyfosbwrYtrbZcXFqR+ozK/gNhJ3de7hpjLXKNkVSF8OjOQ== +"@metamask/snap-workers@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@metamask/snap-workers/-/snap-workers-0.9.0.tgz#215407b632fef4723dd75af7accf1f02a6a46916" + integrity sha512-+4YY5CQ7OPFPWh4QF5e4COgc0aWL6Df7Oc8/y//Sabp1rmXWI429OzCOlBi+NGJfQ1K7ORBMlRtOwYB9ZmWyLA== "@metamask/test-dapp@^5.0.0": version "5.0.0" @@ -5303,10 +5267,10 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.0.2.tgz#1396e27f208ed56dd5638ab5a251edeb1c91d402" - integrity sha512-V0HGxJd0PiDF0ecHYIesTOqfd1gJguwQUOYfMfAWnRsWQEXfc5ifbUFhD3Wjc+O+y7VAqL+g07prq9gHQ/JOZQ== +ajv@^8.0.1, ajv@^8.8.2: + version "8.8.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb" + integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -6914,6 +6878,13 @@ browserify-unibabel@^3.0.0: resolved "https://registry.yarnpkg.com/browserify-unibabel/-/browserify-unibabel-3.0.0.tgz#5a6b8f0f704ce388d3927df47337e25830f71dda" integrity sha1-WmuPD3BM44jTkn30czfiWDD3Hdo= +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + integrity sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0= + dependencies: + pako "~0.2.0" + browserify-zlib@^0.2.0, browserify-zlib@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" @@ -9839,10 +9810,10 @@ duplexer@^0.1.1, duplexer@~0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= -duplexify@^3.1.2, duplexify@^3.4.2: - version "3.5.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd" - integrity sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ== +duplexify@^3.1.2, duplexify@^3.4.2, duplexify@^3.5.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== dependencies: end-of-stream "^1.0.0" inherits "^2.0.1" @@ -9921,6 +9892,11 @@ elliptic@6.5.3, elliptic@6.5.4, elliptic@=3.0.3, elliptic@^6.0.0, elliptic@^6.4. minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emitter-component@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6" + integrity sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY= + emittery@0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.0.tgz#bb373c660a9d421bb44706ec4967ed50c02a8026" @@ -11417,7 +11393,7 @@ ethjs-util@0.1.3: is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" -ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: +ethjs-util@0.1.6, ethjs-util@^0.1.3: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -13433,6 +13409,18 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" +gunzip-maybe@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" + integrity sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw== + dependencies: + browserify-zlib "^0.1.4" + is-deflate "^1.0.0" + is-gzip "^1.0.0" + peek-stream "^1.1.0" + pumpify "^1.3.3" + through2 "^2.0.3" + gzip-size@5.1.1, gzip-size@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -15019,6 +15007,11 @@ is-decimal@^1.0.0, is-decimal@^1.0.2: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== +is-deflate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14" + integrity sha1-yGKQHDwWH7CdrHzcfnhPgOmPLxQ= + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -15147,6 +15140,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-gzip@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" + integrity sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM= + is-hex-prefixed@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" @@ -19105,7 +19103,7 @@ nanoid@^2.0.0, nanoid@^2.1.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== -nanoid@^3.1.12, nanoid@^3.1.23, nanoid@^3.1.28, nanoid@^3.1.31: +nanoid@^3.1.23, nanoid@^3.1.31: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== @@ -20444,6 +20442,11 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU= + pako@~1.0.2, pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" @@ -20816,6 +20819,15 @@ pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9: safe-buffer "^5.0.1" sha.js "^2.4.8" +peek-stream@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67" + integrity sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA== + dependencies: + buffer-from "^1.0.0" + duplexify "^3.5.0" + through2 "^2.0.3" + peer-book@^0.9.1, peer-book@~0.9.0: version "0.9.1" resolved "https://registry.yarnpkg.com/peer-book/-/peer-book-0.9.1.tgz#42dffd7b1faf263bd6abe2907a26f7411f4dbf34" @@ -22811,6 +22823,13 @@ readable-stream@~1.0.15: isarray "0.0.1" string_decoder "~0.10.x" +readable-web-to-node-stream@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" + integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== + dependencies: + readable-stream "^3.6.0" + readdir-scoped-modules@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" @@ -24937,6 +24956,13 @@ stream-to-pull-stream@^1.7.2, stream-to-pull-stream@^1.7.3: looper "^3.0.0" pull-stream "^3.2.3" +stream@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef" + integrity sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8= + dependencies: + emitter-component "^1.1.1" + streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -25522,7 +25548,7 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" -tar-stream@^2.0.0, tar-stream@^2.0.1, tar-stream@^2.1.4: +tar-stream@^2.0.0, tar-stream@^2.0.1, tar-stream@^2.1.4, tar-stream@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -26751,7 +26777,7 @@ util.promisify@1.0.0: define-properties "^1.1.2" object.getownpropertydescriptors "^2.0.3" -util@0.10.3, util@~0.10.1: +util@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= @@ -26777,6 +26803,13 @@ util@^0.12.0, util@^0.12.3, util@~0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" +util@~0.10.1: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + utila@^0.4.0, utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"