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
feature/default_network_editable
Erik Marks 3 years ago committed by GitHub
parent 2b5b787ca9
commit 35ac762e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .eslintrc.js
  2. 2
      .mocharc.js
  3. 129
      .storybook/test-data.js
  4. 8
      app/_locales/de/messages.json
  5. 8
      app/_locales/el/messages.json
  6. 99
      app/_locales/en/messages.json
  7. 8
      app/_locales/es/messages.json
  8. 8
      app/_locales/es_419/messages.json
  9. 8
      app/_locales/fr/messages.json
  10. 8
      app/_locales/hi/messages.json
  11. 8
      app/_locales/id/messages.json
  12. 8
      app/_locales/it/messages.json
  13. 8
      app/_locales/ja/messages.json
  14. 8
      app/_locales/ko/messages.json
  15. 8
      app/_locales/ph/messages.json
  16. 8
      app/_locales/pt_BR/messages.json
  17. 8
      app/_locales/ru/messages.json
  18. 8
      app/_locales/tl/messages.json
  19. 8
      app/_locales/tr/messages.json
  20. 8
      app/_locales/vi/messages.json
  21. 8
      app/_locales/zh_CN/messages.json
  22. 34
      app/scripts/controllers/permissions/flask/snap-permissions.js
  23. 46
      app/scripts/controllers/permissions/flask/snap-permissions.test.js
  24. 3
      app/scripts/controllers/permissions/index.js
  25. 6
      app/scripts/controllers/permissions/specifications.js
  26. 43
      app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
  27. 2
      app/scripts/lib/rpc-method-middleware/index.js
  28. 387
      app/scripts/metamask-controller.js
  29. 4
      development/build/transforms/README.md
  30. 6
      jest.config.js
  31. 5
      lavamoat/browserify/beta/policy-override.json
  32. 98
      lavamoat/browserify/beta/policy.json
  33. 117
      lavamoat/browserify/flask/policy.json
  34. 5
      lavamoat/browserify/main/policy-override.json
  35. 98
      lavamoat/browserify/main/policy.json
  36. 6
      package.json
  37. 10
      shared/constants/app.js
  38. 17
      shared/constants/permissions.js
  39. 22
      shared/constants/permissions.test.js
  40. 7
      ui/components/app/app-components.scss
  41. 1
      ui/components/app/flask/snap-install-warning/index.js
  42. 30
      ui/components/app/flask/snap-install-warning/index.scss
  43. 79
      ui/components/app/flask/snap-install-warning/snap-install-warning.js
  44. 57
      ui/components/app/flask/snap-settings-card/snap-settings-card.js
  45. 1
      ui/components/app/modals/index.scss
  46. 3
      ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js
  47. 3
      ui/components/app/permissions-connect-header/index.scss
  48. 46
      ui/components/app/permissions-connect-header/permissions-connect-header.component.js
  49. 1
      ui/components/app/permissions-connect-permission-list/index.scss
  50. 118
      ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js
  51. 2
      ui/helpers/constants/design-system.js
  52. 10
      ui/helpers/constants/routes.js
  53. 16
      ui/helpers/utils/util.js
  54. 33
      ui/hooks/useOriginMetadata.js
  55. 2
      ui/pages/confirmation/confirmation.scss
  56. 1
      ui/pages/confirmation/templates/add-ethereum-chain.js
  57. 10
      ui/pages/confirmation/templates/flask/snap-confirm/index.scss
  58. 105
      ui/pages/confirmation/templates/flask/snap-confirm/snap-confirm.js
  59. 6
      ui/pages/confirmation/templates/index.js
  60. 1
      ui/pages/confirmation/templates/switch-ethereum-chain.js
  61. 55
      ui/pages/home/home.component.js
  62. 10
      ui/pages/home/home.container.js
  63. 4
      ui/pages/home/index.scss
  64. 1
      ui/pages/permissions-connect/flask/snap-install/index.js
  65. 28
      ui/pages/permissions-connect/flask/snap-install/index.scss
  66. 153
      ui/pages/permissions-connect/flask/snap-install/snap-install.js
  67. 1
      ui/pages/permissions-connect/index.scss
  68. 66
      ui/pages/permissions-connect/permissions-connect.component.js
  69. 62
      ui/pages/permissions-connect/permissions-connect.container.js
  70. 1
      ui/pages/settings/flask/snaps-list-tab/index.js
  71. 21
      ui/pages/settings/flask/snaps-list-tab/index.scss
  72. 94
      ui/pages/settings/flask/snaps-list-tab/snap-list-tab.js
  73. 50
      ui/pages/settings/flask/snaps-list-tab/snap-list-tab.stories.js
  74. 1
      ui/pages/settings/flask/view-snap/index.js
  75. 114
      ui/pages/settings/flask/view-snap/index.scss
  76. 171
      ui/pages/settings/flask/view-snap/view-snap.js
  77. 2
      ui/pages/settings/index.scss
  78. 50
      ui/pages/settings/settings.component.js
  79. 9
      ui/pages/settings/settings.container.js
  80. 9
      ui/pages/settings/settings.stories.js
  81. 22
      ui/selectors/permissions.js
  82. 46
      ui/selectors/selectors.js
  83. 27
      ui/store/actions.js
  84. 241
      yarn.lock

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

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

@ -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": "<svg>...</svg>",
"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": "<svg>...</svg>",
"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: "<svg>...</svg>",
version: "0.6.0"
}
}
},
appState: {
shouldClose: false,

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

@ -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": "Άδειες"
},

@ -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."
},

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

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

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

@ -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": "अनमति"
},

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

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

@ -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": "許可"
},

@ -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": "권한"
},

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

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

@ -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": "Разрешения"
},

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

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

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

@ -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": "权限"
},

@ -0,0 +1,34 @@
import {
restrictedMethodPermissionBuilders,
selectHooks,
} from '@metamask/rpc-methods';
import { endowmentPermissionBuilders } from '@metamask/snap-controllers';
/**
* @returns {Record<string, Record<string, unknown>>} All endowment permission
* specifications.
*/
export const buildSnapEndowmentSpecifications = () =>
Object.values(endowmentPermissionBuilders).reduce(
(allSpecifications, { targetKey, specificationBuilder }) => {
allSpecifications[targetKey] = specificationBuilder();
return allSpecifications;
},
{},
);
/**
* @param {Record<string, Function>} 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;
},
{},
);
}

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

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

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

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

@ -1 +1 @@
export { default } from './createMethodMiddleware';
export * from './createMethodMiddleware';

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

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

@ -1,6 +1,6 @@
module.exports = {
collectCoverageFrom: [
'<rootDir>/app/scripts/controllers/permissions/*.js',
'<rootDir>/app/scripts/controllers/permissions/**/*.js',
'<rootDir>/shared/**/*.js',
'<rootDir>/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 = {
'<rootDir>/app/scripts/migrations/*.test.js',
'<rootDir>/app/scripts/platforms/*.test.js',
'<rootDir>app/scripts/controllers/network/**/*.test.js',
'<rootDir>/app/scripts/controllers/permissions/*.test.js',
'<rootDir>/app/scripts/controllers/permissions/**/*.test.js',
],
testTimeout: 2500,
transform: {

@ -10,6 +10,11 @@
"@babel/runtime": true
}
},
"keccak": {
"packages": {
"readable-stream": true
}
},
"node-fetch": {
"globals": {
"fetch": true

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

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

@ -10,6 +10,11 @@
"@babel/runtime": true
}
},
"keccak": {
"packages": {
"readable-stream": true
}
},
"node-fetch": {
"globals": {
"fetch": true

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

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

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

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

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

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

@ -0,0 +1 @@
export { default } from './snap-install-warning';

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

@ -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 (
<div className="snap-install-warning__footer">
<Button
className="snap-install-warning__footer-button"
type="default"
onClick={onCancel}
>
{t('cancel')}
</Button>
<Button
className="snap-install-warning__footer-button"
type="primary"
disabled={!isConfirmed}
onClick={onSubmit}
>
{t('confirm')}
</Button>
</div>
);
};
return (
<Popover
className="snap-install-warning"
title={t('areYouSure')}
footer={<SnapInstallWarningFooter />}
>
<div className="snap-install-warning__content">
<Typography variant={TYPOGRAPHY.H6} boxProps={{ paddingBottom: 4 }}>
{t('snapInstallWarningCheck')}
</Typography>
<div className="checkbox-label">
<CheckBox
checked={isConfirmed}
id="warning-accept"
onClick={onCheckboxClicked}
/>
<label htmlFor="warning-accept">
{t('snapInstallWarningKeyAccess', [snapName])}
</label>
</div>
</div>
</Popover>
);
}
SnapInstallWarning.propTypes = {
/**
* onCancel handler
*/
onCancel: PropTypes.func,
/**
* onSubmit handler
*/
onSubmit: PropTypes.func,
/**
* Name of snap
*/
snapName: PropTypes.string,
};

@ -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}
</Typography>
<Box paddingLeft={4} className="snap-settings-card__toggle-container">
<ToggleButton
value={isEnabled}
onToggle={onToggle}
className="snap-settings-card__toggle-container__toggle-button"
{...toggleButtonProps}
/>
<Tooltip interactive position="bottom" html={t('snapsToggle')}>
<ToggleButton
value={isEnabled}
onToggle={onToggle}
className="snap-settings-card__toggle-container__toggle-button"
{...toggleButtonProps}
/>
</Tooltip>
</Box>
</Box>
<Typography
@ -113,6 +117,7 @@ const SnapSettingsCard = ({
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
marginBottom={4}
>
<Box>
@ -124,24 +129,26 @@ const SnapSettingsCard = ({
{t('flaskSnapSettingsCardButtonCta')}
</Button>
</Box>
<Chip
leftIcon={
<Box paddingLeft={1}>
<ColorIndicator
color={STATUS_COLORS[status]}
type={ColorIndicator.TYPES.FILLED}
/>
</Box>
}
label={status}
labelProps={{
color: COLORS.UI4,
margin: [0, 1],
}}
backgroundColor={COLORS.UI1}
className="snap-settings-card__chip"
{...chipProps}
/>
<Tooltip interactive position="bottom" html={t('snapsStatus')}>
<Chip
leftIcon={
<Box paddingLeft={1}>
<ColorIndicator
color={STATUS_COLORS[status]}
type={ColorIndicator.TYPES.FILLED}
/>
</Box>
}
label={status}
labelProps={{
color: COLORS.UI4,
margin: [0, 1],
}}
backgroundColor={COLORS.UI1}
className="snap-settings-card__chip"
{...chipProps}
/>
</Tooltip>
</Box>
</Box>
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}>

@ -44,4 +44,3 @@
outline: none !important;
}
}

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

@ -1,8 +1,5 @@
.permissions-connect-header {
display: flex;
flex: 0;
flex-direction: column;
justify-content: center;
width: 92%;
&__icon {

@ -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 (
<div className="permissions-connect-header">
<Box
className="permissions-connect-header"
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JUSTIFY_CONTENT.CENTER}
{...boxProps}
>
{this.renderHeaderIcon()}
<div className="permissions-connect-header__title">{headerTitle}</div>
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
npmPackageName ? (
<SnapsAuthorshipPill
packageName={npmPackageName}
url={npmPackageUrl}
/>
) : null
///: END:ONLY_INCLUDE_IN
}
<div className="permissions-connect-header__subtitle">{headerText}</div>
</div>
</Box>
);
}
}

@ -2,7 +2,6 @@
.permission {
@include H6;
width: 100%;
padding-bottom: 16px;
border-bottom: 1px solid var(--Grey-100);
display: flex;

@ -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<string, PermissionLabelObject>} 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 (
<div className="permissions-connect-permission-list">
{Object.keys(permissions).map((permission) => (
<div className="permission" key={PERMISSION_TYPES[permission].label}>
<i className={PERMISSION_TYPES[permission].leftIcon} />
{PERMISSION_TYPES[permission].label}
<i className={PERMISSION_TYPES[permission].rightIcon} />
</div>
))}
{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 (
<div className="permission" key={permission}>
<i className={leftIcon} />
{label}
{rightIcon && <i className={rightIcon} />}
</div>
);
})}
</div>
);
}
PermissionsConnectPermissionList.propTypes = {
permissions: PropTypes.objectOf(PropTypes.bool).isRequired,
permissions: PropTypes.object.isRequired,
};

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

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

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

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

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

@ -206,6 +206,7 @@ function getValues(pendingApproval, t, actions) {
pendingApproval.id,
ethErrors.provider.userRejectedRequest(),
),
networkDisplay: true,
};
}

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

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

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

@ -85,6 +85,7 @@ function getValues(pendingApproval, t, actions) {
pendingApproval.id,
ethErrors.provider.userRejectedRequest(),
),
networkDisplay: true,
};
}

@ -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 (
<MultipleNotifications>
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
shouldShowErrors
? Object.entries(errorsToShow).map(([errorId, error]) => {
return (
<HomeNotification
classNames={['home__error-message']}
infoText={error.data.snapId}
descriptionText={
<>
<Typography
color={COLORS.UI1}
variant={TYPOGRAPHY.H5}
fontWeight={FONT_WEIGHT.NORMAL}
>
{t('somethingWentWrong')}
</Typography>
<Typography
color={COLORS.UI1}
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.NORMAL}
>
{t('snapError', [error.message, error.code])}
</Typography>
</>
}
onIgnore={async () => {
await removeSnapError(errorId);
}}
ignoreText="Dismiss"
key="home-error-message"
/>
);
})
: null
///: END:ONLY_INCLUDE_IN
}
{newCollectibleAddedMessage ? (
<ActionableMessage
type={newCollectibleAddedMessage === 'success' ? 'info' : 'warning'}

@ -35,6 +35,9 @@ import {
setRecoveryPhraseReminderLastShown,
setNewNetworkAdded,
setNewCollectibleAddedMessage,
///: BEGIN:ONLY_INCLUDE_IN(flask)
removeSnapError,
///: END:ONLY_INCLUDE_IN
} from '../../store/actions';
import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app';
import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask';
@ -118,6 +121,10 @@ const mapStateToProps = (state) => {
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: () =>

@ -152,4 +152,8 @@
background: none;
margin-left: 20px;
}
&__error-message {
left: 8px;
}
}

@ -0,0 +1 @@
export { default } from './snap-install';

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

@ -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 (
<Box
className="page-container snap-install"
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
height={BLOCK_SIZES.FULL}
borderStyle={BORDER_STYLE.NONE}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<Box
className="headers"
alignItems={ALIGN_ITEMS.CENTER}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<PermissionConnectHeader
icon={targetSubjectMetadata.iconUrl}
iconName={targetSubjectMetadata.name}
headerTitle={t('snapInstall')}
headerText={null} // TODO(ritave): Add header text when snaps support description
siteOrigin={targetSubjectMetadata.origin}
npmPackageName={npmId}
boxProps={{ alignItems: ALIGN_ITEMS.CENTER }}
/>
<Typography></Typography>
<Box
className="snap-requests-permission"
padding={4}
tag={TYPOGRAPHY.H7}
>
<span>{t('snapRequestsPermission')}</span>
</Box>
<PermissionsConnectPermissionList
permissions={request.permissions || {}}
/>
</Box>
<Box
className="footers"
alignItems={ALIGN_ITEMS.CENTER}
flexDirection={FLEX_DIRECTION.COLUMN}
>
{targetSubjectMetadata.sourceCode ? (
<>
<div className="source-code">
<div className="text">{t('areYouDeveloper')}</div>
<div
className="link"
onClick={() =>
global.platform.openTab({
url: targetSubjectMetadata.sourceCode,
})
}
>
{t('openSourceCode')}
</div>
</div>
<Box paddingBottom={4}>
<PermissionsConnectFooter />
</Box>
</>
) : (
<Box className="snap-install__footer--no-source-code" paddingTop={4}>
<PermissionsConnectFooter />
</Box>
)}
<PageContainerFooter
cancelButtonType="default"
onCancel={onCancel}
cancelText={t('cancel')}
onSubmit={
shouldShowWarning ? () => setIsShowingWarning(true) : onSubmit
}
submitText={t('approveAndInstall')}
/>
</Box>
{isShowingWarning && (
<SnapInstallWarning
onCancel={() => setIsShowingWarning(false)}
onSubmit={onSubmit}
snapName={targetSubjectMetadata.name}
/>
)}
</Box>
);
}
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,
};

@ -1,4 +1,5 @@
@import 'choose-account/index';
@import 'flask/snap-install/index';
@import 'redirect/index';
.permissions-connect {

@ -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 : (
<div className="permissions-connect__top-bar">
{page === '2' ? (
{page === '2' && isRequestingAccounts ? (
<div
className="permissions-connect__back"
onClick={() => this.goBack()}
@ -178,9 +206,11 @@ export default class PermissionConnect extends Component {
{t('back')}
</div>
) : null}
<div className="permissions-connect__page-count">
{t('xOfY', [page, '2'])}
</div>
{isRequestingAccounts ? (
<div className="permissions-connect__page-count">
{t('xOfY', [page, totalPages])}
</div>
) : null}
</div>
);
}
@ -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)
}
<Route
path={snapInstallPath}
exact
render={() => (
<SnapInstall
request={permissionsRequest || {}}
approveSnapInstall={(...args) => {
approvePermissionsRequest(...args);
this.redirect(true);
}}
rejectSnapInstall={(requestId) =>
this.cancelPermissionsRequest(requestId)
}
targetSubjectMetadata={targetSubjectMetadata}
/>
)}
/>
{
///: END:ONLY_INCLUDE_IN
}
</Switch>
)}
</div>

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

@ -0,0 +1 @@
export { default } from './snap-list-tab';

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

@ -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 (
<div className="snap-list-tab">
{Object.entries(snaps).length ? (
<div className="snap-list-tab__body">
<Box display="flex" flexDirection={FLEX_DIRECTION.COLUMN}>
<Typography variant={TYPOGRAPHY.H5} marginBottom={2}>
{t('expandExperience')}
</Typography>
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.UI4}
marginBottom={2}
>
{t('manageSnaps')}
</Typography>
</Box>
<div className="snap-list-tab__wrapper">
{Object.entries(snaps).map(([key, snap]) => {
return (
<SnapSettingsCard
className="snap-settings-card"
isEnabled={snap.enabled}
key={key}
onToggle={() => {
onToggle(snap);
}}
description={snap.manifest.description}
url={snap.id}
name={snap.manifest.proposedName}
status={snap.status}
version={snap.version}
onClick={() => {
onClick(snap);
}}
/>
);
})}
</div>
</div>
) : (
<Box
className="snap-list-tab__container--no-snaps"
width="full"
height="full"
justifyContent={JUSTIFY_CONTENT.CENTER}
alignItems={ALIGN_ITEMS.CENTER}
>
<Typography variant={TYPOGRAPHY.H4} color={COLORS.UI4}>
<span>{t('noSnaps')}</span>
</Typography>
</Box>
)}
</div>
);
};
export default SnapListTab;

@ -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) => <Provider store={store}>{story()}</Provider>],
argTypes: {
onToggle: {
action: 'onToggle',
},
onRemove: {
action: 'onRemove',
},
},
};
export const DefaultStory = (args) => {
const state = store.getState();
const [viewingSnap, setViewingSnap] = useState();
const [snap, setSnap] = useState();
return (
<div>
<SnapListTab
{...args}
snaps={state.metamask.snaps}
viewingSnap={viewingSnap}
currentSnap={snap}
onToggle={args.onToggle}
onRemove={args.onRemove}
onClick={(_, s) => {
setSnap(s);
setViewingSnap(true);
}}
/>
</div>
);
};
const state = store.getState();
DefaultStory.args = {
snaps: state.metamask.snaps,
viewingSnap: false,
};
DefaultStory.storyName = 'Default';

@ -0,0 +1 @@
export { default } from './view-snap';

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

@ -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 (
<div className="view-snap">
<div className="settings-page__content-row">
<div className="view-snap__subheader">
<Typography
className="view-snap__title"
variant={TYPOGRAPHY.H3}
boxProps={{ textAlign: TEXT_ALIGN.CENTER }}
>
{snap.manifest.proposedName}
</Typography>
<Box className="view-snap__pill-toggle-container">
<Box className="view-snap__pill-container" paddingLeft={2}>
<SnapsAuthorshipPill
packageName={snap.id}
url={authorshipPillUrl}
/>
</Box>
<Box
paddingLeft={4}
className="snap-settings-card__toggle-container view-snap__toggle-container"
>
<Tooltip interactive position="bottom" html={t('snapsToggle')}>
<ToggleButton
value={snap.enabled}
onToggle={onToggle}
className="snap-settings-card__toggle-container__toggle-button"
/>
</Tooltip>
</Box>
</Box>
</div>
<Box
className="view-snap__content-container"
width={FRACTIONS.SEVEN_TWELFTHS}
>
<div className="view-snap__section">
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.UI4}
boxProps={{ marginTop: 5 }}
>
{snap.manifest.description}
</Typography>
</div>
<div className="view-snap__section view-snap__permission-list">
<Typography variant={TYPOGRAPHY.H4}>{t('permissions')}</Typography>
<Typography variant={TYPOGRAPHY.H6} color={COLORS.UI4}>
{t('snapAccess', [snap.manifest.proposedName])}
</Typography>
<Box width={FRACTIONS.TEN_TWELFTHS}>
<PermissionsConnectPermissionList
permissions={snap.manifest.initialPermissions}
/>
</Box>
</div>
<div className="view-snap__section">
<Box width="11/12">
<Typography variant={TYPOGRAPHY.H4}>
{t('connectedSites')}
</Typography>
<Typography variant={TYPOGRAPHY.H6} color={COLORS.UI4}>
{t('connectedSnapSites', [snap.manifest.proposedName])}
</Typography>
<ConnectedSitesList
connectedSubjects={connectedSubjects}
onDisconnect={(origin) => {
onDisconnect(origin, snap.permissionName);
}}
/>
</Box>
</div>
<div className="view-snap__section">
<Typography variant={TYPOGRAPHY.H4}>{t('removeSnap')}</Typography>
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.UI4}
boxProps={{ paddingBottom: 3 }}
>
{t('removeSnapDescription')}
</Typography>
<Button
className="view-snap__remove-button"
type="danger"
css={{
maxWidth: '175px',
}}
onClick={async () => {
await dispatch(removeSnap(snap));
}}
>
{t('removeSnap')}
</Button>
</div>
</Box>
</div>
</div>
);
}
export default React.memo(ViewSnap);

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

@ -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 (
<div
className={classnames('main-container settings-page', {
@ -106,7 +115,7 @@ class SettingsPage extends PureComponent {
{this.renderTabs()}
</div>
<div className="settings-page__content__modules">
{this.renderSubHeader()}
{isSnapViewPage ? null : this.renderSubHeader()}
{this.renderContent()}
</div>
</div>
@ -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: (
<img
src="images/experimental-icon.svg"
alt={t('snapsSettingsDescription')}
/>
),
content: t('snaps'),
key: SNAPS_LIST_ROUTE,
},
///: END:ONLY_INCLUDE_IN
{
icon: <img src="images/security-icon.svg" alt="" />,
content: t('securityAndPrivacy'),
@ -280,6 +306,16 @@ class SettingsPage extends PureComponent {
path={`${CONTACT_VIEW_ROUTE}/:id`}
component={ContactListTab}
/>
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
<Route exact path={SNAPS_LIST_ROUTE} component={SnapListTab} />
///: END:ONLY_INCLUDE_IN
}
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
<Route exact path={`${SNAPS_VIEW_ROUTE}/:id`} component={ViewSnap} />
///: END:ONLY_INCLUDE_IN
}
<Route
render={(routeProps) => (
<SettingsTab

@ -25,6 +25,8 @@ import {
SETTINGS_ROUTE,
EXPERIMENTAL_ROUTE,
ADD_NETWORK_ROUTE,
SNAPS_LIST_ROUTE,
SNAPS_VIEW_ROUTE,
} from '../../helpers/constants/routes';
import Settings from './settings.component';
@ -36,6 +38,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',
@ -52,8 +56,8 @@ const mapStateToProps = (state, ownProps) => {
} = 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,
};
};

@ -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 (
<div style={{ height: 500 }}>
@ -66,6 +70,7 @@ const Settings = ({ history }) => {
history={history}
pathnameI18nKey={pathnameI18nKey}
backRoute={backRoute}
isSnapViewPage={isSnapViewPage}
/>
</div>
);

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

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

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

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

Loading…
Cancel
Save