Connect distinct accounts per site (#7004)

* add PermissionsController

remove provider approval controller
integrate rpc-cap
create PermissionsController
move provider approval functionality to permissions controller
add permissions approval ui, settings page
add permissions activity and history
move some functionality to metamask-inpage-provider
rename siteMetadata -> domainMetadata

add accountsChange notification to inpage provider
move functionality to inpage provider
update inpage provider
Remove 'Connections' settings page (#7369)
add hooks for exposing accounts in settings
rename unused messages in non-English locales

Add external extension id to metadata (#7396)

update inpage provider, rpc-cap
add eth_requestAccounts handling to background
prevent notifying connections if extension is locked
update inpage provider
Fix lint errors
add migration
review fixes
transaction controller review updates
removed unused messages

* Login Per Site UI (#7368)

* LoginPerSite original UI changes to keep

* First commit

* Get necessary connected tab info for redirect and icon display for permissioned sites

* Fix up designs and add missing features

* Some lint fixes

* More lint fixes

* Ensures the tx controller + tx-state-manager orders transactions in the order they are received

* Code cleanup for LoginPerSite-ui

* Update e2e tests to use new connection flow

* Fix display of connect screen and app header after login when connect request present

* Update metamask-responsive-ui.spec for new item in accounts dropdown

* Fix approve container by replacing approvedOrigins with domainMetaData

* Adds test/e2e/permissions.spec.js

* Correctly handle cancellation of a permissions request

* Redirect to home after disconnecting all sites / cancelling all permissions

* Fix display of site icons in menu

* Fix height of permissions page container

* Remove unused locale messages

* Set default values for openExternalTabs and tabIdOrigins in account-menu.container

* More code cleanup for LoginPerSite-ui

* Use extensions api to close tab in permissions-connect

* Remove unnecessary change in domIsReady() in contentscript

* Remove unnecessary private function markers and class methods (for background tab info) in metamask-controller.

* Adds getOriginOfCurrentTab selector

* Adds IconWithFallback component and substitutes for appropriate cases

* Add and utilize font mixins

* Remove unused  method in disconnect-all.container.js

* Simplify buttonSizeLarge code in page-container-footer.component.js

* Add and utilize getAccountsWithLabels selector

* Remove console.log in ui/app/store/actions.js

* Change last connected time format to yyyy-M-d

* Fix css associated with IconWithFallback change

* Ensure tracked openNonMetamaskTabsIDs are correctly set to inactive on tab changes

* Code cleanup for LoginPerSite-ui

* Use reusable function for modifying openNonMetamaskTabsIDs in background.js

* Enables automatic switching to connected account when connected domain is open

* Prevent exploit of tabIdOriginMap in background.js

* Remove unneeded code from contentscript.js

* Simplify current tab origin and window opener logic using remotePort listener tabs.queryTabs

* Design and styling fixes for LoginPerSite-ui

* Fix permissionHistory and permission logging for eth_requestAccounts and eth_accounts

* Front end changes to support display of lastConnected time in connected and permissions screens

* Fix lint errors

* Refactor structure of permissionsHistory

* Fix default values and object modifications for domain and permissionsHistory related data

* Fix connecting to new accounts from modal

* Replace retweet.svg with connect-white.svg

* Fix signature-request.spec

* Update metamask-inpage-provider version

* Fix permissions e2e tests

* Remove unneeded delay from test/e2e/signature-request.spec.js

* Add delay before attempting to retrieve network id in dapp in ethereum-on=.spec

* Use requestAccountTabIds strategy for determining tab id that opened a given window

* Improve default values for permissions requests

* Add some message descriptions to app/_locales/en/messages.json

* Code clean up in permission controller

* Stopped deep cloning object in mapObjectValues

* Bump metamask-inpage-provider version

* Add missing description in app/_locales/en/messages.json

* Return promises from queryTabs and switchToTab of extension.js

* Remove unused getAllPermissions function

* Use default props in icon-with-fallback.component.js

* Stop passing  to permissions controller

* Delete no longer used clear-approved-origins modal code

* Remove duplicate imports in ui/app/components/app/index.scss

* Use URL instead of regex in getOriginFromUrl()

* Add runtime error checking to platform, promise based extension.tab methods

* Support permission requests from external extensions

* Improve font size and colour of the domain origin on the permission confirmation screen

* Add support for toggling permissions

* Ensure getRenderablePermissionsDomains only returns domains with exposedAccount caveat permissions

* Remove unused code from LoginPerSite-ui branch

* Ensure modal closes on Enter press for new-account-modal.component.js

* Lint fix

* fixup! Login Per Site UI (#7368)

* Some code cleanup for LoginPerSite

* Adds UX for connecting to dapps via the connected sites screen (#7593)

* Adds UX for connecting to dapps via the connected sites screen

* Use openMetaMaskTabIds from background.js to determine if current active tab is MetaMask

* Delete unused permissions controller methods

* Fixes two small bugs in the LoginPerSite ui (#7595)

* Restore `providerRequest` message translations (#7600)

This message was removed, but it was replaced with a very similar
message called `likeToConnect`. The only difference is that the new
message has "MetaMask" in it. Preserving these messages without
"MetaMask" is probably better than deleting them, so these messages
have all been restored and renamed to `likeToConnect`.

* Login per site no sitemetadata fix (#7610)

* Support connected sites for which we have no site metadata.

* Change property containing subtitle info often populated by origin to a more accurate of purpose name

* Lint fix

* Improve disconnection modal messages (#7612)

* Improve disconnectAccountModalDescription and disconnectAllModalDescription messages

* Update disconnectAccountModalDescription app/_locales/en/messages.json

Co-Authored-By: Mark Stacey <markjstacey@gmail.com>

* Improve disconnectAccount modal message clarity

* Adds cancel button to the account selection screen of the permissions request flow (#7613)

* Fix eth_accounts permission language & selectability (#7614)

* fix eth_accounts language & selectability

* fix MetaMask capitalization in all messages

* Close sidebar when opening connected sites (#7611)

The 'Connected Sites' button in the accounts details now closes the
sidebar, if it is open. This was accomplished by pulling the click
handler for that button up to the wallet view component, where another
button already followed a similar pattern of closing the sidebar.

It seemed confusing to me that one handler was in the `AccountsDetails`
container component, and one was handed down from above, so I added
PropTypes to the container component.

I'm not sure that the WalletView component is the best place for this
logic, but I've put it there for now to be consistent with the add
token button.

* Reject permissions request upon tab close (#7618)

Permissions requests are now rejected when the page is closed. This
only applies to the full-screen view, as that is the view permission
requests should be handled in. The case where the user deals with the
request through a different view is handled in #7617

* Handle tab update failure (#7619)

`extension.tabs.update` can sometimes fail if the user interacts with
the tabs directly around the same time. The redirect flow has been
updated to ensure that the permissions tab is still closed in that
case. The user is on their own to find the dapp tab again in that case.

* Login per site tab popup fixes (#7617)

* Handle redirect in response to state update in permissions-connect

* Ensure origin is available to permissions-connect subcomponents during redirect

* Hide app bar whenever on redirect route

* Improvements to handling of redirects in permissions-connect

* Ensure permission request id change handling only happens when page is not null

* Lint fix

* Decouple confirm transaction screen from the selected address (#7622)

* Avoid race condtion that could prevent contextual account switching (#7623)

There was a race condition in the logic responsible for switching the
selected account based upon the active tab. It was asynchronously
querying the active tab, then assuming it had been retrieved later.

The active tab info itself was already in the redux store in another
spot, one that is guaranteed to be set before the UI renders. The
race condition was avoided by deleting the duplicate state, and using
the other active tab state.

* Only redirect back to dapp if current tab is active (#7621)

The "redirect back to dapp" behaviour can be disruptive when the
permissions connect tab is not active. The purpose of the redirect was
to maintain context between the dapp and the permissions request, but
if the user has already moved to another tab, that no longer applies.

* Fix JSX style lint errors

* Remove unused state
feature/default_network_editable
Dan Finlay 5 years ago committed by Mark Stacey
parent bb4c4189fb
commit 76b1699621
  1. 20
      app/_locales/am/messages.json
  2. 20
      app/_locales/ar/messages.json
  3. 20
      app/_locales/bg/messages.json
  4. 20
      app/_locales/bn/messages.json
  5. 22
      app/_locales/ca/messages.json
  6. 9
      app/_locales/cs/messages.json
  7. 20
      app/_locales/da/messages.json
  8. 20
      app/_locales/de/messages.json
  9. 20
      app/_locales/el/messages.json
  10. 109
      app/_locales/en/messages.json
  11. 20
      app/_locales/es/messages.json
  12. 20
      app/_locales/es_419/messages.json
  13. 24
      app/_locales/et/messages.json
  14. 20
      app/_locales/fa/messages.json
  15. 20
      app/_locales/fi/messages.json
  16. 17
      app/_locales/fil/messages.json
  17. 20
      app/_locales/fr/messages.json
  18. 3
      app/_locales/gu/messages.json
  19. 20
      app/_locales/he/messages.json
  20. 20
      app/_locales/hi/messages.json
  21. 9
      app/_locales/hn/messages.json
  22. 20
      app/_locales/hr/messages.json
  23. 9
      app/_locales/ht/messages.json
  24. 20
      app/_locales/hu/messages.json
  25. 20
      app/_locales/id/messages.json
  26. 38
      app/_locales/it/messages.json
  27. 16
      app/_locales/ja/messages.json
  28. 20
      app/_locales/kn/messages.json
  29. 20
      app/_locales/ko/messages.json
  30. 20
      app/_locales/lt/messages.json
  31. 20
      app/_locales/lv/messages.json
  32. 3
      app/_locales/ml/messages.json
  33. 3
      app/_locales/mr/messages.json
  34. 20
      app/_locales/ms/messages.json
  35. 9
      app/_locales/nl/messages.json
  36. 20
      app/_locales/no/messages.json
  37. 9
      app/_locales/ph/messages.json
  38. 20
      app/_locales/pl/messages.json
  39. 9
      app/_locales/pt/messages.json
  40. 20
      app/_locales/pt_BR/messages.json
  41. 3
      app/_locales/pt_PT/messages.json
  42. 20
      app/_locales/ro/messages.json
  43. 20
      app/_locales/ru/messages.json
  44. 20
      app/_locales/sk/messages.json
  45. 20
      app/_locales/sl/messages.json
  46. 20
      app/_locales/sr/messages.json
  47. 20
      app/_locales/sv/messages.json
  48. 20
      app/_locales/sw/messages.json
  49. 12
      app/_locales/ta/messages.json
  50. 3
      app/_locales/te/messages.json
  51. 14
      app/_locales/th/messages.json
  52. 9
      app/_locales/tr/messages.json
  53. 20
      app/_locales/uk/messages.json
  54. 9
      app/_locales/vi/messages.json
  55. 22
      app/_locales/zh_CN/messages.json
  56. 20
      app/_locales/zh_TW/messages.json
  57. 3
      app/images/broken-line.svg
  58. 3
      app/images/connect-white.svg
  59. 0
      app/images/permissions-check.svg
  60. 25
      app/scripts/background.js
  61. 96
      app/scripts/contentscript.js
  62. 377
      app/scripts/controllers/permissions/index.js
  63. 169
      app/scripts/controllers/permissions/loggerMiddleware.js
  64. 90
      app/scripts/controllers/permissions/methodMiddleware.js
  65. 49
      app/scripts/controllers/permissions/permissions-safe-methods.json
  66. 20
      app/scripts/controllers/permissions/restrictedMethods.js
  67. 14
      app/scripts/controllers/preferences.js
  68. 177
      app/scripts/controllers/provider-approval.js
  69. 83
      app/scripts/controllers/transactions/index.js
  70. 73
      app/scripts/createStandardProvider.js
  71. 132
      app/scripts/inpage.js
  72. 5
      app/scripts/lib/auto-reload.js
  73. 1
      app/scripts/lib/createOriginMiddleware.js
  74. 19
      app/scripts/lib/local-store.js
  75. 4
      app/scripts/lib/message-manager.js
  76. 4
      app/scripts/lib/personal-message-manager.js
  77. 4
      app/scripts/lib/typed-message-manager.js
  78. 29
      app/scripts/lib/util.js
  79. 282
      app/scripts/metamask-controller.js
  80. 1
      app/scripts/migrations/029.js
  81. 23
      app/scripts/migrations/040.js
  82. 54
      app/scripts/platforms/extension.js
  83. 13
      docs/porting_to_new_environment.md
  84. 8
      package.json
  85. 12
      test/e2e/contract-test/contract.js
  86. 5
      test/e2e/contract-test/index.html
  87. 19
      test/e2e/ethereum-on.spec.js
  88. 2
      test/e2e/metamask-responsive-ui.spec.js
  89. 19
      test/e2e/metamask-ui.spec.js
  90. 201
      test/e2e/permissions.spec.js
  91. 8
      test/e2e/run-all.sh
  92. 24
      test/e2e/signature-request.spec.js
  93. 3
      test/unit/app/controllers/preferences-controller-test.js
  94. 330
      test/unit/app/controllers/provider-approval-test.js
  95. 5
      test/unit/app/controllers/transactions/tx-controller-test.js
  96. 18
      ui/app/components/app/account-details/account-details.component.js
  97. 17
      ui/app/components/app/account-details/account-details.container.js
  98. 6
      ui/app/components/app/account-details/index.scss
  99. 15
      ui/app/components/app/account-menu/account-menu.component.js
  100. 9
      ui/app/components/app/account-menu/account-menu.container.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "የግላዊነት ኩነት አሁን በንቡር ነቅቷል"
},
"chartOnlyAvailableEth": {
"message": "ቻርት የሚገኘው በ Ethereum አውታረ መረቦች ላይ ብቻ ነው።"
},
"confirmClear": {
"message": "የተፈቀዱ ድረ ገጾችን ለማጥራት እንደሚፈልጉ እርግጠኛ ነዎት?"
},
"contractInteraction": {
"message": "የግንኙነት ተግባቦት"
},
"clearApprovalData": {
"message": "የግላዊነት ውሂብን አጥራ"
},
"reject": {
"message": "አይቀበሉ"
},
"providerRequest": {
"likeToConnect": {
"message": "$1ከመለያዎ ጋር ለመገናኘት ይፈልጋል"
},
"providerRequestInfo": {
"message": "ይህ ድረ ገጽ የእርስዎን መለያ ወቅታዊ አድራሻ ለማየት እየጠየቀ ነው። ምንጊዜም ግንኙነት የሚያደርጉባቸውን ድረ ገጾች የሚያምኗቸው መሆኑን ያረጋግጡ።"
},
"about": {
"message": "ስለ"
},
@ -248,9 +236,6 @@
"connect": {
"message": "ይገናኙ"
},
"connectRequest": {
"message": "የግንኙነት ጥያቄ"
},
"connectingTo": {
"message": "ከ $1ጋር መገናኘት"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "ቀደም ሲል የተወሰነ Ether ካለዎት፣ በአዲሱ ቋትዎ Ether ለማግኘት ፈጣኑ መንገድ ቀጥተኛ ተቀማጭ ነው።"
},
"dismiss": {
"message": "አሰናብት"
},
"done": {
"message": "ተጠናቅቋል"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "يتم تمكين وضع الخصوصية الآن بشكل افتراضي"
},
"chartOnlyAvailableEth": {
"message": "الرسم البياني متاح فقط على شبكات إيثيريوم."
},
"confirmClear": {
"message": "هل أنت متأكد من أنك تريد مسح المواقع المعتمدة؟"
},
"contractInteraction": {
"message": "التفاعل على العقد"
},
"clearApprovalData": {
"message": "مسح بيانات الخصوصية"
},
"reject": {
"message": "رفض"
},
"providerRequest": {
"likeToConnect": {
"message": "يرغب $1 في الاتصال بحسابك"
},
"providerRequestInfo": {
"message": "يطلب هذا الموقع حق الوصول لعرض عنوان حسابك الحالي. تأكد دائماً من ثقتك في المواقع التي تتفاعل معها."
},
"about": {
"message": "حول"
},
@ -248,9 +236,6 @@
"connect": {
"message": "اتصال"
},
"connectRequest": {
"message": "طلب اتصال"
},
"connectingTo": {
"message": "جارِ الاتصال بـ $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "إذا كان لديك بالفعل بعض الأثير، فإن أسرع طريقة للحصول على الأثير في محفظتك الجديدة عن طريق الإيداع المباشر."
},
"dismiss": {
"message": "رفض"
},
"done": {
"message": "تم"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Режимът на поверителност вече е активиран по подразбиране"
},
"chartOnlyAvailableEth": {
"message": "Диаграмата е достъпна само в мрежи на Ethereum."
},
"confirmClear": {
"message": "Сигурни ли сте, че искате да изчистите одобрените уебсайтове?"
},
"contractInteraction": {
"message": "Взаимодействие с договор"
},
"clearApprovalData": {
"message": "Изчистване на данните за поверителност"
},
"reject": {
"message": "Отхвърляне"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 би искал да се свърже с вашия акаунт"
},
"providerRequestInfo": {
"message": "Този сайт иска достъп за преглед на адреса на текущия ви акаунт. Винаги се уверявайте, че се доверявате на сайтовете, с които взаимодействате."
},
"about": {
"message": "Информация"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Свързване"
},
"connectRequest": {
"message": "Свържете заявка"
},
"connectingTo": {
"message": "Свързване с $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Ако вече имате някакъв етер, най-бързият начин да получите етер в новия си портфейл е чрез директен депозит."
},
"dismiss": {
"message": "Отхвърляне"
},
"done": {
"message": "Готово"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "গপনয়তর মড এখন ডিফলট হি সকিয় কর আছ"
},
"chartOnlyAvailableEth": {
"message": "শর Ethereum নটওয়কগিট উপলভয। "
},
"confirmClear": {
"message": "আপনিি অনিত ওয়বসইটগি পরির করর বিষয়িিত?"
},
"contractInteraction": {
"message": "কনট বপ"
},
"clearApprovalData": {
"message": "গপনয়তর ড পরির করন"
},
"reject": {
"message": "পরতন"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 আপনর অউনর সগ করতয়"
},
"providerRequestInfo": {
"message": "এই সইটটি আপনর বরতমন অউনর ঠির অর জনয অনধ জ। সবসময় নিিত হয়ন য আপনিইটর সগ করছন সিিসযয কি।"
},
"about": {
"message": "সমপর"
},
@ -248,9 +236,6 @@
"connect": {
"message": "সত করন"
},
"connectRequest": {
"message": "সর অনধ"
},
"connectingTo": {
"message": " $1 এর সগ করছ"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "আপনর ইতিমধি ইথর থকল আপনর নতন ওয় ইথর পওয়র দততম উপয় হল সরসরি জম কর।"
},
"dismiss": {
"message": "খিজ"
},
"done": {
"message": "সমপনন "
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "El mode de privacitat ara està activat per defecte"
},
"chartOnlyAvailableEth": {
"message": "Mostra només els disponibles a les xarxes Ethereum."
},
"confirmClear": {
"message": "Estàs segur que vols eliminar totes les pàgines web aprovades?"
},
"contractInteraction": {
"message": "Contractar Interacció"
},
"clearApprovalData": {
"message": "Elimina les dades de privacitat"
},
"reject": {
"message": "Rebutja"
},
"providerRequest": {
"likeToConnect": {
"message": "a $1 li agradaria connectar-se al teu compte"
},
"providerRequestInfo": {
"message": "Aquesta pàgina està demanant accès a la teva adreça"
},
"about": {
"message": "Informació"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Connecta"
},
"connectRequest": {
"message": "Sol·licitud de connexió"
},
"connectingTo": {
"message": "Connectant a $1 "
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Si ja tens una mica d'Ether, la manera més ràpida de posar Ether al teu nou moneder és per dipòsit directe."
},
"dismiss": {
"message": "Omet"
},
"done": {
"message": "Fet"
},
@ -687,7 +669,7 @@
"message": "Conectant-te a Ethereum i la web descentralitzada."
},
"metamaskSeedWords": {
"message": "Frase de recuperació de Metamask"
"message": "Frase de recuperació de MetaMask"
},
"metamaskVersion": {
"message": "Versió MetaMask"

@ -1,16 +1,7 @@
{
"confirmClear": {
"message": "Naozaj chcete vymazať schválené webové stránky?"
},
"clearApprovalData": {
"message": "Jasné údaje o schválení"
},
"reject": {
"message": "Odmítnout"
},
"providerRequestInfo": {
"message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě."
},
"account": {
"message": "Účet"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Privatlivstilstand er nu som udgangspunkt aktiveret"
},
"chartOnlyAvailableEth": {
"message": "Skema kun tilgængeligt på Ethereum-netværk."
},
"confirmClear": {
"message": "Er du sikker på, at du vil rydde godkendte hjemmesider?"
},
"contractInteraction": {
"message": "Kontraktinteraktion"
},
"clearApprovalData": {
"message": "Ryd fortrolighedsdata"
},
"reject": {
"message": "Afvis"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ønsker at forbinde til din konto"
},
"providerRequestInfo": {
"message": "Denne side anmoder om at se din nuværende kontoadresse. Sørg altid for, at du stoler på de sider du interagerer med."
},
"about": {
"message": "Om"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Få forbindelse"
},
"connectRequest": {
"message": "Tilslutningsanmodning"
},
"connectingTo": {
"message": "Forbinder til $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Hvis du allerede har Ether, er den hurtigste måde at få Ether i din nye tegnebog ved direkte indbetaling."
},
"dismiss": {
"message": "Luk"
},
"done": {
"message": "Færdig"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Der Datenschutzmodus ist jetzt standardmäßig aktiviert"
},
"chartOnlyAvailableEth": {
"message": "Die Grafik ist nur in Ethereum-Netzwerken verfügbar."
},
"confirmClear": {
"message": "Möchten Sie die genehmigten Websites wirklich löschen?"
},
"contractInteraction": {
"message": "Vertragsinteraktion"
},
"clearApprovalData": {
"message": "Genehmigungsdaten löschen"
},
"reject": {
"message": "Ablehnen"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 möchte sich mit deinem Account verbinden"
},
"providerRequestInfo": {
"message": "Diese Website fordert Zugriff auf Ihre aktuelle Kontoadresse. Stellen Sie immer sicher, dass Sie den Websites vertrauen, mit denen Sie interagieren."
},
"about": {
"message": "Über"
},
@ -236,9 +224,6 @@
"connect": {
"message": "Verbinden"
},
"connectRequest": {
"message": "Verbindungsanfrage"
},
"connectingTo": {
"message": "Verbindung mit $1 wird hergestellt"
},
@ -353,9 +338,6 @@
"directDepositEtherExplainer": {
"message": "Wenn du bereits Ether besitzt, ist die sofortige Einzahlung die schnellste Methode Ether in deine neue Wallet zu bekommen."
},
"dismiss": {
"message": "Schließen"
},
"done": {
"message": "Fertig"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Η Λειτουργία Απορρήτου είναι πλέον ενεργοποιημένη από προεπιλογή"
},
"chartOnlyAvailableEth": {
"message": "Το διάγραμμα είναι διαθέσιμο μόνο σε δίκτυα Ethereum."
},
"confirmClear": {
"message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους εγκεκριμένους ιστότοπους;"
},
"contractInteraction": {
"message": "Αλληλεπίδραση Σύμβασης"
},
"clearApprovalData": {
"message": "Εκκαθάριση Δεδομένων Απορρήτου"
},
"reject": {
"message": "Απόρριψη"
},
"providerRequest": {
"likeToConnect": {
"message": "Αίτημα σύνδεσης στον λογαριασμό σας από $1"
},
"providerRequestInfo": {
"message": "Ο ιστότοπος ζητά πρόσβαση για προβολή της τρέχουσας διεύθυνσης του λογαριασμού σας. Να σιγουρεύεστε πάντα ότι εμπιστεύεστε τους ιστότοπους με τους οποίους αλληλεπιδράτε."
},
"about": {
"message": "Σχετικά με"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Σύνδεση"
},
"connectRequest": {
"message": "Αίτημα Σύνδεσης"
},
"connectingTo": {
"message": "Σύνδεση με $1"
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Αν έχετε ήδη κάποια Ether, ο πιο γρήγορος τρόπος για να πάρετε τα Ether στο νέο σας πορτοφόλι με άμεση κατάθεση."
},
"dismiss": {
"message": "Παράβλεψη"
},
"done": {
"message": "Τέλος"
},

@ -14,47 +14,33 @@
"showIncomingTransactionsDescription": {
"message": "Select this to use Etherscan to show incoming transactions in the transactions list"
},
"cancelledConnectionWithMetaMask": {
"message": "Cancelled Connection With MetaMask"
},
"chartOnlyAvailableEth": {
"message": "Chart only available on Ethereum networks."
},
"confirmClear": {
"message": "Are you sure you want to clear approved websites?"
},
"connections": {
"message": "Connections"
},
"connectionsSettingsDescription": {
"message": "Sites allowed to read your accounts"
},
"addSite": {
"message": "Add Site"
"connectedSites": {
"message": "Connected Sites"
},
"addSiteDescription": {
"message": "Manually add a site to allow it access to your accounts, useful for older dapps"
"connectingWithMetaMask": {
"message": "Connecting With MetaMask..."
},
"connected": {
"message": "Connected"
"connectTo": {
"message": "Connect to $1",
"description": "$1 is the name/origin of a site/dapp that the user can connect to metamask"
},
"connectedDescription": {
"message": "The list of sites allowed access to your addresses"
},
"privacyModeDefault": {
"message": "Privacy Mode is now enabled by default"
"chooseAnAcount": {
"message": "Choose an account"
},
"contractInteraction": {
"message": "Contract Interaction"
},
"clearApprovalData": {
"message": "Remove all sites"
},
"reject": {
"message": "Reject"
},
"providerRequest": {
"message": "$1 would like to connect to your account"
},
"providerRequestInfo": {
"message": "This site is requesting access to view your current account address. Always make sure you trust the sites you interact with."
"redirectingBackToDapp": {
"message": "Redirecting back to dapp..."
},
"about": {
"message": "About"
@ -305,9 +291,6 @@
"connect": {
"message": "Connect"
},
"connectRequest": {
"message": "Connect Request"
},
"connectingTo": {
"message": "Connecting to $1"
},
@ -431,14 +414,36 @@
"details": {
"message": "Details"
},
"disconnectAccount": {
"message": "Disconnect account"
},
"disconnectAll": {
"message": "Disconnect All"
},
"disconnectAllModalDescription": {
"message": "Are you sure? You will be disconnected from all sites on all accounts."
},
"disconnectAccountModalDescription": {
"message": "Are you sure? Your account (\"$1\") will be disconnected from this site."
},
"disconnectAccountQuestion": {
"message": "Disconnect account?"
},
"disconnectFromThisAccount": {
"message": "Disconnect from this account?"
},
"disconnectAllAccountsQuestion": {
"message": "Disconnect all accounts?"
},
"directDepositEther": {
"message": "Directly Deposit Ether"
},
"directDepositEtherExplainer": {
"message": "If you already have some Ether, the quickest way to get Ether in your new wallet by direct deposit."
},
"dismiss": {
"message": "Dismiss"
"domainLastConnect": {
"message": "Last Connected: $1",
"description": "$1 is the date at which the user was last connected to a given domain"
},
"done": {
"message": "Done"
@ -500,6 +505,13 @@
"endOfFlowMessage10": {
"message": "All Done"
},
"extensionId": {
"message": "Extension ID: $1",
"description": "$1 is a string of random letters that are the id of another extension connecting to MetaMask"
},
"externalExtension": {
"message": "External Extension"
},
"onboardingReturnNotice": {
"message": "\"$1\" will close this tab and direct back to $2",
"description": "Return the user to the site that initiated onboarding"
@ -733,9 +745,15 @@
"max": {
"message": "Max"
},
"lastConnected": {
"message": "Last Connected"
},
"learnMore": {
"message": "Learn more"
},
"learnAboutRisks": {
"message": "Learn about the risks here."
},
"ledgerAccountRestriction": {
"message": "You need to make use your last account before you can add a new one."
},
@ -745,6 +763,10 @@
"likeToAddTokens": {
"message": "Would you like to add these tokens?"
},
"likeToConnect": {
"message": "$1 would like to connect to your MetaMask account",
"description": "$1 is the name/url of a site/dapp asking to connect to MetaMask"
},
"links": {
"message": "Links"
},
@ -876,6 +898,9 @@
"rpcUrl": {
"message": "New RPC URL"
},
"onlyConnectTrust": {
"message": "Only connect with sites you trust."
},
"optionalChainId": {
"message": "ChainID (optional)"
},
@ -1078,6 +1103,9 @@
"readyToConnect": {
"message": "Ready to Connect?"
},
"revokeInPermissions": {
"message": "* You can view and revoke permissions in MetaMask settings."
},
"rinkeby": {
"message": "Rinkeby Test Network"
},
@ -1283,6 +1311,9 @@
"storePhrase": {
"message": "Store this phrase in a password manager like 1Password."
},
"submit": {
"message": "Submit"
},
"submitted": {
"message": "Submitted"
},
@ -1331,6 +1362,14 @@
"testFaucet": {
"message": "Test Faucet"
},
"thisWillAllow": {
"message": "This will allow $1 to:",
"description": "$1 is the name or domain of a site/dapp that is requesting permissions"
},
"thisWillAllowExternalExtension": {
"message": "This will allow an external extension with id $1 to:",
"description": "$1 is a string of random letters that are the id of another extension connecting to MetaMask"
},
"thisWillCreate": {
"message": "This will create a new wallet and seed phrase"
},
@ -1343,6 +1382,10 @@
"toWithColon": {
"message": "To:"
},
"toConnectWith": {
"message": "To connect with $1",
"description": "$1 is the name or domain of a site/dapp that asking to connect with MetaMask"
},
"toETHviaShapeShift": {
"message": "$1 to ETH via ShapeShift",
"description": "system will fill in deposit type in start of message"

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Modo Privado está activado ahora por defecto"
},
"chartOnlyAvailableEth": {
"message": "Tabla solo disponible en redes Ethereum."
},
"confirmClear": {
"message": "¿Seguro que quieres borrar los sitios web aprobados?"
},
"contractInteraction": {
"message": "Interacción con contrato"
},
"clearApprovalData": {
"message": "Borrar datos de aprobación"
},
"reject": {
"message": "Rechazar"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 quisiera conectar con tu cuenta"
},
"providerRequestInfo": {
"message": "El dominio que se muestra a continuación intenta solicitar acceso a la API Ethereum para que pueda interactuar con la blockchain de Ethereum. Siempre verifique que esté en el sitio correcto antes de aprobar el acceso Ethereum."
},
"about": {
"message": "Acerca"
},
@ -205,9 +193,6 @@
"connect": {
"message": "Conectar"
},
"connectRequest": {
"message": "Petición para conectar"
},
"connectingTo": {
"message": "Conectánodse a $1"
},
@ -325,9 +310,6 @@
"directDepositEtherExplainer": {
"message": "Si posees Ether, la forma más rápida de transferirlo a tu nueva billetera es depositándolo directamente"
},
"dismiss": {
"message": "Descartar"
},
"done": {
"message": "Completo"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "El modo de privacidad está habilitado de manera predeterminada"
},
"chartOnlyAvailableEth": {
"message": "Chart está disponible únicamente en las redes de Ethereum."
},
"confirmClear": {
"message": "¿Estás seguro de que deseas borrar los sitios web aprobados?"
},
"contractInteraction": {
"message": "Interacción contractual"
},
"clearApprovalData": {
"message": "Borrar datos de privacidad"
},
"reject": {
"message": "Rechazar"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 desea conectarse a tu cuenta"
},
"providerRequestInfo": {
"message": "Este sitio está solicitando acceso para ver la dirección de tu cuenta corriente. Asegúrate siempre de que confías en los sitios con los que interactúas."
},
"about": {
"message": "Acerca de"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Conectar"
},
"connectRequest": {
"message": "Solicitud de conexión"
},
"connectingTo": {
"message": "Conexión con $1"
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Si ya tienes algunos Ethers, la forma más rápida de ingresarlos en tu nueva billetera es a través de un depósito directo."
},
"dismiss": {
"message": "Rechazar"
},
"done": {
"message": "Listo"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Privaatsusrežiim on nüüd vaikimisi lubatud"
},
"chartOnlyAvailableEth": {
"message": "Tabel on saadaval vaid Ethereumi võrkudes."
},
"confirmClear": {
"message": "Kas soovite kindlasti kinnitatud veebisaidid kustutada?"
},
"contractInteraction": {
"message": "Lepingu suhtlus"
},
"clearApprovalData": {
"message": "Tühjenda privaatsusandmed"
},
"reject": {
"message": "Lükka tagasi"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 soovib teie kontoga ühenduse luua"
},
"providerRequestInfo": {
"message": "See sait taotleb juurdepääsu teie praeguse konto aadressi vaatamiseks. Veenduge alati, et usaldate saite, millega suhtlete."
},
"about": {
"message": "Teave"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Ühendamine"
},
"connectRequest": {
"message": "Ühenduse taotlus"
},
"connectingTo": {
"message": "Ühenduse loomine $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Kui teil on juba veidi eetrit, on kiirem viis eetri rahakotti saamiseks otsene sissemakse."
},
"dismiss": {
"message": "Loobu"
},
"done": {
"message": "Valmis"
},
@ -420,7 +402,7 @@
"message": "Kui teil on küsimusi või näete midagi kahtlast, kirjutage meile support@metamask.io."
},
"endOfFlowMessage8": {
"message": "Metamask ei saa teie seemnefraasi taastada. Lisateave."
"message": "MetaMask ei saa teie seemnefraasi taastada. Lisateave."
},
"endOfFlowMessage9": {
"message": "Lisateave."
@ -1180,7 +1162,7 @@
"message": "Saate sünkroonida oma kontod ja teabe oma mobiiliseadmega. Avage MetaMaski mobiilirakendus, avage \"Settings\" (Seaded) ja puudutage valikut \"Sync from Browser Extension\" (Sünkroonimine lehitseja laiendusest)"
},
"syncWithMobileDescNewUsers": {
"message": "Järgige Metamaski mobiilirakenduse esmakordsel avamisel telefonis esitatud samme."
"message": "Järgige MetaMaski mobiilirakenduse esmakordsel avamisel telefonis esitatud samme."
},
"syncWithMobileScanThisCode": {
"message": "Skanneerige see kood MetaMaski mobiilirakendusega"

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "وضعیت محرمیت حالا بصورت خودکار فعال است"
},
"chartOnlyAvailableEth": {
"message": "تنها قابل دسترس را در شبکه های ایتریوم جدول بندی نمایید"
},
"confirmClear": {
"message": "آیا مطمئن هستید تا وب سایت های تصدیق شده را حذف کنید؟"
},
"contractInteraction": {
"message": "تعامل قرارداد"
},
"clearApprovalData": {
"message": "حذف اطلاعات حریم خصوصی"
},
"reject": {
"message": "عدم پذیرش"
},
"providerRequest": {
"likeToConnect": {
"message": "1$1 میخواهید تا با حساب تان وصل شوید"
},
"providerRequestInfo": {
"message": "این سایت در حال درخواست دسترسی است تا آدرس فعلی حساب تان را مشاهده نماید. همیشه متوجه باشید که بالای سایتی که با آن معامله میکنید، اعتماد دارید یا خیر."
},
"about": {
"message": "درباره"
},
@ -248,9 +236,6 @@
"connect": {
"message": "اتصال"
},
"connectRequest": {
"message": "درخواست اتصال"
},
"connectingTo": {
"message": "در حال اتصال به 1$1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "در صورتیکه شما کدام ایتر داشته باشید، سریعترین روش برای گرفتن ایتر در کیف جدید تان توسط پرداخت مستقیم."
},
"dismiss": {
"message": "لغو کردن"
},
"done": {
"message": "تمام"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Yksityisyystila on nyt oletusarvoisesti käytössä"
},
"chartOnlyAvailableEth": {
"message": "Kaavio saatavilla vain Ethereum-verkoissa."
},
"confirmClear": {
"message": "Haluatko varmasti tyhjentää hyväksytyt verkkosivustot?"
},
"contractInteraction": {
"message": "Sopimustoiminta"
},
"clearApprovalData": {
"message": "Tyhjennä yksityisyystiedot"
},
"reject": {
"message": "Hylkää"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 haluaisi yhdistää tiliisi"
},
"providerRequestInfo": {
"message": "Tämä sivusto pyytää oikeuksia nähdä nykyisen tiliosoitteesi. Varmista aina, että luotat sivustoihin, joiden kanssa toimit."
},
"about": {
"message": "Tietoja asetuksista"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Muodosta yhteys"
},
"connectRequest": {
"message": "Yhdistämispyyntö"
},
"connectingTo": {
"message": "Yhdistetään summaan $1 "
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Jos sinulla on jo etheriä, nopein tapa hankkia etheriä uuteen lompakkoosi on suoratalletus."
},
"dismiss": {
"message": "Piilota"
},
"done": {
"message": "Valmis"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Naka-enable ang Privacy Mode bilang default"
},
"chartOnlyAvailableEth": {
"message": "Available lang ang chart sa mga Ethereum network."
},
"confirmClear": {
"message": "Sigurado ka bang gusto mong i-clear ang mga inaprubahang website?"
},
"contractInteraction": {
"message": "Paggamit sa Contract"
},
"clearApprovalData": {
"message": "I-clear ang Privacy Data"
},
"reject": {
"message": "Tanggihan"
},
"providerRequest": {
"likeToConnect": {
"message": "Gusto ng $1 na kumonekta sa iyong account"
},
"providerRequestInfo": {
"message": "Humihiling ng access ang site na ito na tingnan ang kasalukuyan mong account address. Palaging tiyaking pinagkakatiwalaan mo ang mga site na pinupuntahan mo."
},
"about": {
"message": "Tungkol sa"
},
@ -338,9 +326,6 @@
"directDepositEtherExplainer": {
"message": "Kung mayroon ka nang Ether, ang pinakamabilis na paraan para magkaroon ng Ether sa iyong bagong wallet ay sa pamamagitan ng direkang deposito."
},
"dismiss": {
"message": "Balewalain"
},
"done": {
"message": "Tapos na"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Le mode conversation privée est maintenant activé par défaut"
},
"chartOnlyAvailableEth": {
"message": "Tableau disponible uniquement sur les réseaux Ethereum."
},
"confirmClear": {
"message": "Êtes-vous sûr de vouloir supprimer les sites Web approuvés?"
},
"contractInteraction": {
"message": "Interaction avec un contrat"
},
"clearApprovalData": {
"message": "Effacer les données d'approbation"
},
"reject": {
"message": "Rejeter"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 voudrait se connecter à votre compte"
},
"providerRequestInfo": {
"message": "Le domaine répertorié ci-dessous tente de demander l'accès à l'API Ethereum pour pouvoir interagir avec la chaîne de blocs Ethereum. Vérifiez toujours que vous êtes sur le bon site avant d'autoriser l'accès à Ethereum."
},
"about": {
"message": "À propos"
},
@ -236,9 +224,6 @@
"connect": {
"message": "Connecter"
},
"connectRequest": {
"message": "Demande de connexion"
},
"connectingTo": {
"message": "Connexion à $1"
},
@ -356,9 +341,6 @@
"directDepositEtherExplainer": {
"message": "Si vous avez déjà de l'Ether, le moyen le plus rapide d'obtenir des Ether dans votre nouveau portefeuille est par dépôt direct."
},
"dismiss": {
"message": "Ignorer"
},
"done": {
"message": "Terminé"
},

@ -54,9 +54,6 @@
"details": {
"message": "વિગત"
},
"dismiss": {
"message": "ક"
},
"done": {
"message": "થઈ ગય"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "מצב פרטיות זמין עכשיו כברירת מחדל"
},
"chartOnlyAvailableEth": {
"message": "טבלה זמינה רק ברשתות אתריום."
},
"confirmClear": {
"message": "הנך בטוח/ה כי ברצונך למחוק אתרים שאושרו?"
},
"contractInteraction": {
"message": "אינטראקציית חוזה"
},
"clearApprovalData": {
"message": "נקה נתוני פרטיות"
},
"reject": {
"message": "דחה"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 מבקש להתחבר לחשבון שלך"
},
"providerRequestInfo": {
"message": "אתר זה מבקש גישה לצפייה בכתובת החשבון הנוכחית שלך. יש לוודא תמיד כי הנך בוטח/ת באתרים עמם הנך מתקשר/ת."
},
"about": {
"message": "מידע כללי"
},
@ -248,9 +236,6 @@
"connect": {
"message": "התחברות"
},
"connectRequest": {
"message": "חבר/י בקשה"
},
"connectingTo": {
"message": "מתחבר ל- $1 "
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "אם כבר יש ברשותך את'ר (Ether) , הדרך המהירה ביותר להכניס את'ר לארנק החדש שלך היא באמצעות הפקדה ישירה."
},
"dismiss": {
"message": "סגור"
},
"done": {
"message": "סיום"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "गपनय मड अब डिट रप स सकषम ह"
},
"chartOnlyAvailableEth": {
"message": "कवल ईथरअम नटवरक पर उपलबध चट।"
},
"confirmClear": {
"message": "क आप वकई सत वबसइटियर करनहत?"
},
"contractInteraction": {
"message": "कट कतचत"
},
"clearApprovalData": {
"message": "गपनयतित कर"
},
"reject": {
"message": "असर कर"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 आपक कनट हहत"
},
"providerRequestInfo": {
"message": "यह सइट आपक वरतमन ख पतखनिए एकस क अनध कर रह। हमिित करि आप जिन सइट पर जिवसनय ह।"
},
"about": {
"message": "इसक"
},
@ -248,9 +236,6 @@
"connect": {
"message": "कनट कर"
},
"connectRequest": {
"message": "सपरक अनध"
},
"connectingTo": {
"message": "$1 स कनट ह रह"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "यदि आपकस पहलछ Ether ह, त अपन नए वट म Ether प सबस तर जम करन।"
},
"dismiss": {
"message": "खिज कर"
},
"done": {
"message": "पण"
},

@ -1,19 +1,10 @@
{
"confirmClear": {
"message": "क आप वकई अनित वबसइट करनहत?"
},
"clearApprovalData": {
"message": "अनदन ड कर"
},
"approve": {
"message": "मर"
},
"reject": {
"message": "असर"
},
"providerRequestInfo": {
"message": "नबदध डन वब 3 एपआई तक पहच क अनध करनरयस कर रहि यह एथियम बकचन सतचत कर सक। वब 3 एकस क पहल हम सहच करि आप सहइट पर ह।"
},
"account": {
"message": "ख"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "Način se Privatnost sada zadano omogućava"
},
"chartOnlyAvailableEth": {
"message": "Grafikon je dostupan samo na mrežama Ethereum."
},
"confirmClear": {
"message": "Sigurno želite očistiti odobrena mrežna mjesta?"
},
"contractInteraction": {
"message": "Ugovorna interakcija"
},
"clearApprovalData": {
"message": "Očisti podatke o privatnosti"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Odbaci"
},
"providerRequest": {
"likeToConnect": {
"message": "Korisnik $1 želi se povezati na vaš račun"
},
"providerRequestInfo": {
"message": "Na ovom se mjestu zahtijeva pristup za pregledavanje vaše trenutačne adrese računa. Uvijek pazite da vjerujete mrežnim mjestima s kojima rukujete."
},
"about": {
"message": "O opcijama"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Povežite se"
},
"connectRequest": {
"message": "Zahtjev za povezivanjem"
},
"connectingTo": {
"message": "Povezivanje na $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Ako imate nešto Ethera, najbrži je način prebacivanja Ethera u vaš novi novčanik izravan polog."
},
"dismiss": {
"message": "Odbaci"
},
"done": {
"message": "Gotovo"
},

@ -1,13 +1,4 @@
{
"confirmClear": {
"message": "Èske ou sèten ou vle klè sitwèb apwouve?"
},
"clearApprovalData": {
"message": "Klè Done sou vi prive"
},
"providerRequestInfo": {
"message": "Domèn ki nan lis anba a ap mande pou jwenn aksè a blòkchou Ethereum ak pou wè kont ou ye kounye a. Toujou double tcheke ke ou sou sit ki kòrèk la anvan apwouve aksè."
},
"accessingYourCamera": {
"message": "Aksè a Kamera"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "Az adatvédelmi mód mostantól alapbeállításként engedélyezve van"
},
"chartOnlyAvailableEth": {
"message": "A diagram csak Ethereum hálózatokon érhető el"
},
"confirmClear": {
"message": "Biztosan törölni szeretnéd a jóváhagyott weboldalakat?"
},
"contractInteraction": {
"message": "Szerződéses interakció"
},
"clearApprovalData": {
"message": "Adatvédelmi adatok törlése"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Elutasítás"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 szeretne kapcsolódni az ön fiókjához"
},
"providerRequestInfo": {
"message": "A webhely hozzáférést kér az ön jelenlegi fiókcímének megtekintéséhez. Mindig győződjön meg arról, hogy megbízható webhellyel létesít kapcsolatot."
},
"about": {
"message": "Névjegy"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Csatlakozás"
},
"connectRequest": {
"message": "Csatlakozási kérelem"
},
"connectingTo": {
"message": "Kapcsolódás: $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Amennyiben már rendelkezik némi Ether-rel, a közvetlen letéttel gyorsan elhelyezheti azt új pénztárcájában."
},
"dismiss": {
"message": "Elvetés"
},
"done": {
"message": "Kész"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "Modus Privasi kini aktif secara default"
},
"chartOnlyAvailableEth": {
"message": "Grafik hanya tersedia pada jaringan Ethereum."
},
"confirmClear": {
"message": "Yakin ingin mengosongkan website yang disetujui?"
},
"contractInteraction": {
"message": "Interaksi Kontrak"
},
"clearApprovalData": {
"message": "Bersihkan Data Privasi"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Tolak"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ingin menghubungkan ke akun Anda"
},
"providerRequestInfo": {
"message": "Situs ini meminta akses untuk melihat alamat akun Anda saat ini. Selalu pastikan bahwa Anda bisa mempercayai situs yang berinteraksi dengan Anda."
},
"about": {
"message": "Tentang"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Sambungkan"
},
"connectRequest": {
"message": "Permintaan Sambungan"
},
"connectingTo": {
"message": "Menghubungkan ke $1"
},
@ -362,9 +347,6 @@
"directDepositEtherExplainer": {
"message": "Jika Anda sudah memiliki Ether, cara tercepat mendapatkan Ether di dompet baru lewat deposit langsung."
},
"dismiss": {
"message": "Tutup"
},
"done": {
"message": "Selesai"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "La modalità privacy è ora abilitata per impostazione predefinita"
},
"chartOnlyAvailableEth": {
"message": "Grafico disponibile solo per le reti Ethereum."
},
"confirmClear": {
"message": "Sei sicuro di voler cancellare i siti Web approvati?"
},
"contractInteraction": {
"message": "Interazione Contratto"
},
"clearApprovalData": {
"message": "Cancella i dati di approvazione"
},
"reject": {
"message": "Annulla"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 vorrebbe connettersi al tuo account"
},
"providerRequestInfo": {
"message": "Il dominio elencato di seguito sta tentando di richiedere l'accesso all'API Ethereum in modo che possa interagire con la blockchain di Ethereum. Controlla sempre di essere sul sito corretto prima di approvare l'accesso a Ethereum."
},
"about": {
"message": "Informazioni"
},
@ -227,9 +215,6 @@
"connect": {
"message": "Connetti"
},
"connectRequest": {
"message": "Richiesta Connessione"
},
"connectingTo": {
"message": "Connessione in corso a $1"
},
@ -347,9 +332,6 @@
"directDepositEtherExplainer": {
"message": "Se possiedi già degli Ether, questa è la via più veloce per aggiungere Ether al tuo portafoglio con un deposito diretto."
},
"dismiss": {
"message": "Ignora"
},
"done": {
"message": "Finito"
},
@ -1343,24 +1325,6 @@
"zeroGasPriceOnSpeedUpError": {
"message": "Prezzo del gas maggiore di zero"
},
"connections": {
"message": "Connessioni"
},
"connectionsSettingsDescription": {
"message": "Siti autorizzati ad accedere ai tuoi accounts"
},
"addSite": {
"message": "Aggiungi Sito"
},
"addSiteDescription": {
"message": "Aggiungi un sito autorizzato ad accedere ai tuoi accounts, utile per dapps obsolete"
},
"connected": {
"message": "Connesso"
},
"connectedDescription": {
"message": "La lista di siti web autorizzati ad accedere ai tuoi indirizzi"
},
"contacts": {
"message": "Contatti"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "プライバシーモードがデフォルトで有効になりました"
},
"chartOnlyAvailableEth": {
"message": "チャートはEthereumネットワークでのみ利用可能です。"
},
"confirmClear": {
"message": "承認されたウェブサイトをクリアしてもよろしいですか?"
},
"contractInteraction": {
"message": "コントラクトへのアクセス"
},
"clearApprovalData": {
"message": "承認データのクリア"
},
"reject": {
"message": "拒否"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 はあなたのアカウントにアクセスしようとしています。"
},
"providerRequestInfo": {
"message": "下記のドメインは、Ethereumブロックチェーンとやり取りできるようにEthereum APIへのアクセスをリクエストしようとしています。 Web3アクセスを承認する前に、正しいサイトにいることを常に確認してください。"
},
"aboutSettingsDescription": {
"message": "バージョンやサポート、問合せ先など"
},
@ -84,7 +72,7 @@
"message": "推奨トークンを追加"
},
"addAcquiredTokens": {
"message": "Metamaskで獲得したトークンを追加する"
"message": "MetaMaskで獲得したトークンを追加する"
},
"amount": {
"message": "金額"

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "ಗಯತ ಅನ ಆಗಿ ಸಕಿಯಗಿಸಲಿ"
},
"chartOnlyAvailableEth": {
"message": "ಎಥಿಯಮವರಗಳಲಿರವಗಳ ಲಭಯವಿತವ."
},
"confirmClear": {
"message": "ನ ಅನಿಿದ ವಗಳನರವಿಸಲ ಬಯಸಿ?"
},
"contractInteraction": {
"message": "ಒಪದದ ಸವಹನ"
},
"clearApprovalData": {
"message": "ಗಯತವನರವಿಿ"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "ತಿರಸಕರಿಿ"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ನಿಮ ಖಪರಿಸಲ ಬಯಸಿ"
},
"providerRequestInfo": {
"message": "ಈ ಸಿಮ ಪರಸತ ಖಿಸವನಿಸಲರವಶವನಿಿಿ. ನವಹನ ನಡವ ಸಗಳನಿಿಿದನಗಲ ಖಚಿತಪಡಿಿಿ."
},
"about": {
"message": "ಕಿ"
},
@ -248,9 +236,6 @@
"connect": {
"message": "ಸಪರಿ"
},
"connectRequest": {
"message": "ವಿಿಯನಪರಕಪಡಿಿ"
},
"connectingTo": {
"message": "$1 ಗಪರಕಪಡಿಸಲಿ"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "ನ ಈಗಗಲಲವ ಎಥರಿದರ, ನರ ಠವಣಿಲಕ ನಿಮ ಹಸ ವನಲಿ ಎಥರ ಅನ ಪಡವ ತವರಿತ ಮಗ."
},
"dismiss": {
"message": "ವಜಿಿ"
},
"done": {
"message": "ಮಿಿ"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "이제 프라이버시 모드가 기본 설정으로 활성화됐습니다"
},
"chartOnlyAvailableEth": {
"message": "이더리움 네트워크에서만 사용 가능한 차트."
},
"confirmClear": {
"message": "승인 된 웹 사이트를 삭제 하시겠습니까?"
},
"contractInteraction": {
"message": "계약 상호 작용"
},
"clearApprovalData": {
"message": "승인 데이터 삭제"
},
"reject": {
"message": "거부"
},
"providerRequest": {
"likeToConnect": {
"message": "$1이 당신의 계정에 연결하길 원합니다."
},
"providerRequestInfo": {
"message": "아래 나열된 도메인은 Web3 API에 대한 액세스를 요청하여 Ethereum 블록 체인과 상호 작용할 수 있습니다. Ethereum 액세스를 승인하기 전에 항상 올바른 사이트에 있는지 다시 확인하십시오."
},
"about": {
"message": "정보"
},
@ -245,9 +233,6 @@
"connect": {
"message": "연결"
},
"connectRequest": {
"message": "연결 요청"
},
"connectingTo": {
"message": "$1에 연결"
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "약간의 이더를 이미 보유하고 있다면, 새로 만든 지갑에 직접 입금하여 이더를 보유할 수 있습니다."
},
"dismiss": {
"message": "숨기기"
},
"done": {
"message": "완료"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Dabar privatumo režimas suaktyvintas pagal numatytąją nuostatą"
},
"chartOnlyAvailableEth": {
"message": "Diagramos yra tik „Ethereum“ tinkluose."
},
"confirmClear": {
"message": "Ar tikrai norite panaikinti patvirtintas svetaines?"
},
"contractInteraction": {
"message": "Sutartinė sąveika"
},
"clearApprovalData": {
"message": "Išvalyti asmeninius duomenis"
},
"reject": {
"message": "Atmesti"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 norėtų prisijungti prie jūsų paskyros"
},
"providerRequestInfo": {
"message": "Ši svetainė prašo prieigos peržiūrėti jūsų dabartinės paskyros adresą. Visada patikrinkite, ar pasitikite svetainėmis, su kuriomis sąveikaujate."
},
"about": {
"message": "Apie"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Prisijungti"
},
"connectRequest": {
"message": "Prijungimo užklausa"
},
"connectingTo": {
"message": "Jungiamasi prie $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Jeigu jau turite šiek tiek eterių, sparčiausias būdas gauti eterių į naują piniginę yra tiesioginis įnašas."
},
"dismiss": {
"message": "Atsisakyti"
},
"done": {
"message": "Atlikta"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "Privātais režīms tagad ieslēgts pēc noklusējuma"
},
"chartOnlyAvailableEth": {
"message": "Grafiks pieejams vienīgi Ethereum tīklos."
},
"confirmClear": {
"message": "Vai tiešām vēlaties dzēst apstiprinātās vietnes?"
},
"contractInteraction": {
"message": "Līguma mijiedarbības"
},
"clearApprovalData": {
"message": "Notīrīt konfidencialitātes datus"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Noraidīt"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 vēlas izveidot savienojumu ar jūsu kontu"
},
"providerRequestInfo": {
"message": "Šī lapa pieprasa piekļuvi jūsu pašreizēja konta adreses informācijai. Vienmēr pārliecinieties, ka uzticaties lapām, kuras apmeklējat."
},
"about": {
"message": "Par"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Pievienošana"
},
"connectRequest": {
"message": "Savienojuma pieprasījums"
},
"connectingTo": {
"message": "Pieslēdzas $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Ja jums jau ir Ether, tad visātrāk Ether savā makā varat saņemt ar tiešo iemaksu."
},
"dismiss": {
"message": "Noraidīt"
},
"done": {
"message": "Pabeigts"
},

@ -54,9 +54,6 @@
"details": {
"message": "വിശദശങങൾ"
},
"dismiss": {
"message": "ബഹികരിക"
},
"done": {
"message": "പർതിി"
},

@ -54,9 +54,6 @@
"details": {
"message": "तपशल"
},
"dismiss": {
"message": "डिसमिस कर"
},
"done": {
"message": "पण झ"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Mod Privasi kini diaktifkan secara lalai"
},
"chartOnlyAvailableEth": {
"message": "Carta hanya tersedia di rangkaian Ethereum."
},
"confirmClear": {
"message": "Adakah anda pasti mahu mengosongkan tapak web diluluskan?"
},
"contractInteraction": {
"message": "Interaksi Kontrak"
},
"clearApprovalData": {
"message": "Kosongkan Data Privasi"
},
"reject": {
"message": "Tolak"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ingin menyambung kepada akaun anda"
},
"providerRequestInfo": {
"message": "Tapak ini meminta akses untuk melihat alamat akaun semasa anda. Sentiasa pastikan anda mempercayai tapak web yang anda berinteraksi."
},
"about": {
"message": "Mengenai"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Sambung"
},
"connectRequest": {
"message": "Sambungkan Permintaan"
},
"connectingTo": {
"message": "Menyambungkan kepada $1"
},
@ -359,9 +344,6 @@
"directDepositEtherExplainer": {
"message": "Jika anda sudah mempunyai Ether, cara paling cepat untuk mendapatkan Ether di dompet baru anda ialah dengan deposit langsung."
},
"dismiss": {
"message": "Singkirkan"
},
"done": {
"message": "Selesai"
},

@ -1,16 +1,7 @@
{
"confirmClear": {
"message": "Weet je zeker dat je goedgekeurde websites wilt wissen?"
},
"clearApprovalData": {
"message": "Gegevens over goedkeuring wissen"
},
"reject": {
"message": "Afwijzen"
},
"providerRequestInfo": {
"message": "Het onderstaande domein probeert toegang tot de Ethereum API te vragen zodat deze kan communiceren met de Ethereum-blockchain. Controleer altijd eerst of u op de juiste site bent voordat u Ethereum-toegang goedkeurt."
},
"accountDetails": {
"message": "Accountgegevens"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Personvernmodus er nå aktivert som standard"
},
"chartOnlyAvailableEth": {
"message": "Diagram kun tilgjengelig på Ethereum-nettverk."
},
"confirmClear": {
"message": "Er du sikker på at du vil tømme godkjente nettsteder?"
},
"contractInteraction": {
"message": "Kontraktssamhandling"
},
"clearApprovalData": {
"message": "Tøm personvernsdata"
},
"reject": {
"message": "Avslå"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ønsker å forbindes med kontoen din "
},
"providerRequestInfo": {
"message": "Dette nettstedet ber om tilgang til å vise din nåværende kontoadresse. Alltid kontroller at du stoler på nettstedene du samhandler med."
},
"about": {
"message": "Info"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Koble til"
},
"connectRequest": {
"message": "Kontaktforespørsel "
},
"connectingTo": {
"message": "Forbinder til $1 "
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Hvis du allerede har noe Ether, er den raskeste måten å få Ether i den nye lommeboken din på ved hjelp av direkte innskudd."
},
"dismiss": {
"message": "Lukk"
},
"done": {
"message": "Ferdig"
},

@ -1,10 +1,4 @@
{
"confirmClear": {
"message": "Sigurado ka bang gusto mong i-clear ang mga naaprubahang website?"
},
"clearApprovalData": {
"message": "Tanggalin ang data ng pag-apruba"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -15,9 +9,6 @@
"reject": {
"message": "Tanggihan"
},
"providerRequestInfo": {
"message": "Ang domain na nakalista sa ibaba ay sinusubukang humiling ng access sa Ethereum API upang maaari itong makipag-ugnayan sa Ethereum blockchain. Laging i-double check na ikaw ay nasa tamang site bago aprubahan ang Ethereum access."
},
"accountDetails": {
"message": "Detalye ng Account"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Tryb prywatny jest domyślnie włączony"
},
"chartOnlyAvailableEth": {
"message": "Wykres dostępny tylko w sieciach Ethereum"
},
"confirmClear": {
"message": "Czy na pewno chcesz usunąć zatwierdzone strony internetowe?"
},
"contractInteraction": {
"message": "Interakcja z kontraktem"
},
"clearApprovalData": {
"message": "Usuń dane poufne"
},
"reject": {
"message": "Odrzuć"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 chce połączyć się z Twoim kontem"
},
"providerRequestInfo": {
"message": "Ta strona prosi o dostęp, aby zobaczyć adres Twojego aktualnego konta. Zawsze upewnij się, że ufasz stronom, z którymi wchodzisz w interakcję."
},
"about": {
"message": "Informacje"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Połącz"
},
"connectRequest": {
"message": "Potwierdź żądanie"
},
"connectingTo": {
"message": "Łączenie z $1"
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Jeśli już masz Eter, najszybciej umieścisz go w swoim nowym portfelu przy pomocy bezpośredniego depozytu."
},
"dismiss": {
"message": "Zamknij"
},
"done": {
"message": "Gotowe"
},

@ -1,16 +1,7 @@
{
"confirmClear": {
"message": "Tem certeza de que deseja limpar sites aprovados?"
},
"clearApprovalData": {
"message": "Limpar dados de aprovação"
},
"reject": {
"message": "Rejeitar"
},
"providerRequestInfo": {
"message": "O domínio listado abaixo está tentando solicitar acesso à API Ethereum para que ele possa interagir com o blockchain Ethereum. Sempre verifique se você está no site correto antes de aprovar o acesso à Ethereum."
},
"account": {
"message": "Conta"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "O Modo de Privacidade está ativado por padrão"
},
"chartOnlyAvailableEth": {
"message": "Tabela disponível apenas em redes de Ethereum."
},
"confirmClear": {
"message": "Tem certeza de que deseja limpar os sites aprovados?"
},
"contractInteraction": {
"message": "Interação do Contrato"
},
"clearApprovalData": {
"message": "Limpar Dados de Privacidade"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Rejeitar"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 gostaria de se conectar à sua conta"
},
"providerRequestInfo": {
"message": "Este site está solicitando acesso para visualizar o seu endereço de conta atual. Certifique-se sempre de confiar nos sites com os quais você interage."
},
"about": {
"message": "Sobre"
},
@ -242,9 +230,6 @@
"connect": {
"message": "Conectar-se"
},
"connectRequest": {
"message": "Solicitação de Conexão"
},
"connectingTo": {
"message": "Conectando a $1"
},
@ -362,9 +347,6 @@
"directDepositEtherExplainer": {
"message": "Se você já tem Ether, a forma mais rápida de colocá-lo em sua nova carteira é o depósito direto."
},
"dismiss": {
"message": "Dispensar"
},
"done": {
"message": "Concluído"
},

@ -63,9 +63,6 @@
"details": {
"message": "Detalhes"
},
"dismiss": {
"message": "Ignorar"
},
"done": {
"message": "Concluído"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Modul de confidențialitate este activat acum în mod implicit"
},
"chartOnlyAvailableEth": {
"message": "Grafic disponibil numai pe rețelele Ethereum."
},
"confirmClear": {
"message": "Sunteți sigur că doriți să ștergeți site-urile aprobate?"
},
"contractInteraction": {
"message": "Interacțiune contract"
},
"clearApprovalData": {
"message": "Ștergeți datele confidențiale"
},
"reject": {
"message": "Respingeți"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ar dori să se conecteze la contul dvs."
},
"providerRequestInfo": {
"message": "Acest site solicită acces pentru a vedea adresa curentă a contului dvs. Asigurați-vă întotdeauna că aveți încredere în site-urile cu care interacționați."
},
"about": {
"message": "Despre"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Conectează-te"
},
"connectRequest": {
"message": "Solicitare de conectare"
},
"connectingTo": {
"message": "Se conectează la $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Dacă deja aveți Ether, cel mai rapid mod de a avea Ether în portofelul nou prin depunere directă."
},
"dismiss": {
"message": "Închide"
},
"done": {
"message": "Efectuat"
},

@ -1,16 +1,7 @@
{
"confirmClear": {
"message": "Вы уверены, что хотите очистить утвержденные веб-сайты?Tem certeza de que deseja limpar sites aprovados?"
},
"clearApprovalData": {
"message": "Четкие данные об утверждении"
},
"reject": {
"message": "Отклонить"
},
"providerRequestInfo": {
"message": "Домен, указанный ниже, пытается запросить доступ к API-интерфейсу Ethereum, чтобы он мог взаимодействовать с блокчейном Ethereum. Всегда проверяйте, что вы находитесь на правильном сайте, прежде чем одобрять доступ к веб-сайту."
},
"account": {
"message": "Счет"
},
@ -545,16 +536,13 @@
"youSign": {
"message": "Вы подписываете"
},
"privacyModeDefault": {
"message": "Режим конфиденциальности теперь включен по умолчанию"
},
"chartOnlyAvailableEth": {
"message": "Диаграмма доступна только в сетях Ethereum."
},
"contractInteraction": {
"message": "Взаимодействие с контрактом"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 запрашивает доступ к вашему аккаунту"
},
"about": {
@ -707,9 +695,6 @@
"connect": {
"message": "Подключиться"
},
"connectRequest": {
"message": "Запрос на подключение"
},
"connectingTo": {
"message": "Подключение к $1"
},
@ -752,9 +737,6 @@
"deleteAccount": {
"message": "Удалить аккаунт"
},
"dismiss": {
"message": "Отклюнить"
},
"downloadGoogleChrome": {
"message": "Скачать Google Chrome"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Režim súkromia je povolený. Je prednastavený automaticky"
},
"chartOnlyAvailableEth": {
"message": "Graf je k dispozícii iba v sieťach Ethereum."
},
"confirmClear": {
"message": "Naozaj chcete vymazať schválené webové stránky?"
},
"contractInteraction": {
"message": "Zmluvná interakcia"
},
"clearApprovalData": {
"message": "Jasné údaje o schválení"
},
"reject": {
"message": "Odmítnout"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 sa chce pripojiť k vášmu účtu"
},
"providerRequestInfo": {
"message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě."
},
"about": {
"message": "Informácie"
},
@ -239,9 +227,6 @@
"connect": {
"message": "Pripojenie"
},
"connectRequest": {
"message": "Požiadavka na pripojenie"
},
"connectingTo": {
"message": "Pripája sa k $1"
},
@ -359,9 +344,6 @@
"directDepositEtherExplainer": {
"message": "Pokud už vlastníte nějaký Ether, nejrychleji ho dostanete do peněženky přímým vkladem."
},
"dismiss": {
"message": "Zatvoriť"
},
"done": {
"message": "Hotovo"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Zasebnostni način je zdaj privzeto omogočen"
},
"chartOnlyAvailableEth": {
"message": "Grafikon na voljo le v glavnih omrežjih."
},
"confirmClear": {
"message": "Ste prepričani da želite počistiti odobrene spletne strani?"
},
"contractInteraction": {
"message": "Interakcija s pogodbo"
},
"clearApprovalData": {
"message": "Počisti podatke o odobritvi"
},
"reject": {
"message": "Zavrni"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 se želi povezati z vašim računom."
},
"providerRequestInfo": {
"message": "Domena zahteva dostop do verige blokov in ogled vašega računa. Pred potrditvjo vedno preverite ali ste na želeni spletni strani."
},
"about": {
"message": "O možnostih"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Poveži"
},
"connectRequest": {
"message": "Zahteva za povezavo"
},
"connectingTo": {
"message": "Povezovanje na $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Če že imate Ether, ga lahko najhitreje dobite v MetaMask z neposrednim vplačilom."
},
"dismiss": {
"message": "Opusti"
},
"done": {
"message": "Končano"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Režim privatnosti je podrazumevano omogućen"
},
"chartOnlyAvailableEth": {
"message": "Grafikon dostupan jedino na mrežama Ethereum."
},
"confirmClear": {
"message": "Da li ste sigurni da želite da obrišete odobrene veb lokacije?"
},
"contractInteraction": {
"message": "Ugovorna interakcija"
},
"clearApprovalData": {
"message": "Obrišite privatne podatke"
},
"reject": {
"message": "Одбиј"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 bi hteo da se poveže sa vašim nalogom"
},
"providerRequestInfo": {
"message": "Ovaj sajt traži pristup kako bi video vašu trenutnu adresu naloga. Uvek budite sigurni da verujete sajtovima s kojima komunicirate."
},
"about": {
"message": "Основни подаци"
},
@ -245,9 +233,6 @@
"connect": {
"message": "Повезивање"
},
"connectRequest": {
"message": "Zahtev za povezivanjem"
},
"connectingTo": {
"message": "Povezuje se na $1"
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "Ako već imate neki Ether, najbrži način da preuzmete Ether u svoj novi novčanik jeste direktnim deponovanjem."
},
"dismiss": {
"message": "Одбаци"
},
"done": {
"message": "Gotovo"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "Integritetsläge är nu aktiverat som standard"
},
"chartOnlyAvailableEth": {
"message": "Tabellen är endast tillgänglig på Ethereum-nätverk."
},
"confirmClear": {
"message": "Är du säker på att du vill rensa godkända webbplatser?"
},
"contractInteraction": {
"message": "Kontraktinteraktion"
},
"clearApprovalData": {
"message": "Rensa personlig data"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Avvisa"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 vill ansluta till ditt konto"
},
"providerRequestInfo": {
"message": "Den här sidan begär åtkomst till din aktuella kontoadress. Se till att du kan lita på de sidor du använder dig av."
},
"about": {
"message": "Om"
},
@ -242,9 +230,6 @@
"connect": {
"message": "Ansluta"
},
"connectRequest": {
"message": "Anslutningsförfrågan"
},
"connectingTo": {
"message": "Ansluter till $1"
},
@ -362,9 +347,6 @@
"directDepositEtherExplainer": {
"message": "Om du redan har Ether är det snabbaste sättet att få Ether i din nya plånbok att göra en direktinsättning."
},
"dismiss": {
"message": "Ta bort permanent"
},
"done": {
"message": "Klart"
},

@ -1,19 +1,10 @@
{
"privacyModeDefault": {
"message": "Hali ya Faragha sasa imewezeshwa kwa chaguomsingi"
},
"chartOnlyAvailableEth": {
"message": "Zogoa inapatikana kwenye mitandao ya Ethereum pekee."
},
"confirmClear": {
"message": "Una uhakika unataka kufuta tovuti zilizodihinishwa?"
},
"contractInteraction": {
"message": "Mwingiliono wa Mkataba"
},
"clearApprovalData": {
"message": "Futa Data za Faragha"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
@ -21,12 +12,9 @@
"reject": {
"message": "Kataa"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ingependa kuunganishwa kwenye akaunti yako"
},
"providerRequestInfo": {
"message": "Tovuti hii inaomba idhini ya kuangalia anwani yako ya akaunti ya sasa. Daima hakikisha unaziamami tovuti ambazo unaingiliana nazo."
},
"about": {
"message": "Kuhusu"
},
@ -242,9 +230,6 @@
"connect": {
"message": "Unganisha"
},
"connectRequest": {
"message": "Unganisha Ombi"
},
"connectingTo": {
"message": "Inaunganisha kwenye $1"
},
@ -362,9 +347,6 @@
"directDepositEtherExplainer": {
"message": "Ikiwa tayari una sarafu kadhaa za Ether, njia rahisi ya kupata Ether kwenye waleti yako mpya kupitia kuweka moja kwa moja."
},
"dismiss": {
"message": "Ondoa"
},
"done": {
"message": "Imekamilika"
},

@ -1,19 +1,10 @@
{
"confirmClear": {
"message": "அஙகரிகபபடட வலதளஙகளிசயமக நக விிகள?"
},
"clearApprovalData": {
"message": "ஒபதல தரவ அழி"
},
"approve": {
"message": "ஒபதல"
},
"reject": {
"message": "நிகரி"
},
"providerRequestInfo": {
"message": "க படியலிடபபடள ட Web3 ஏபிஐ அணகலவதறயறிிறத, எனவ இத Ethereum blockchain உடனடரள மி. Web3 அண அஙகரிபதற சரின தளதி இரபத எப இர சரிகவ."
},
"account": {
"message": "கணக"
},
@ -578,9 +569,6 @@
"delete": {
"message": "ந"
},
"dismiss": {
"message": "நிகரி"
},
"fast": {
"message": "வகமன"
},

@ -54,9 +54,6 @@
"details": {
"message": "వివర"
},
"dismiss": {
"message": "తలగి"
},
"done": {
"message": "పతయిి"
},

@ -1,19 +1,10 @@
{
"confirmClear": {
"message": "คณแนใจหรอไมาตองการลางเวบไซตานการอน"
},
"clearApprovalData": {
"message": "ลางขอมลการอน"
},
"reject": {
"message": "ปฏเสธ"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 ตองการเชอมตอกบบญชของคณ"
},
"providerRequestInfo": {
"message": "โดเมนทแสดงดานลางกำลงพยายามขอเขาถง API ของ Ethereum เพอใหสามารถโตตอบกบบลอค Ethereum ได ตรวจสอบวาคณอยในไซตกตองกอนทจะอนการเขาถง Ethereum เสมอ"
},
"about": {
"message": "เกยวกบ"
},
@ -184,9 +175,6 @@
"directDepositEtherExplainer": {
"message": "ถาคณมเธอรอยแลววการทเรวทดในการเอาเงนเขากระเปาใหมอการโอนตรงๆ"
},
"dismiss": {
"message": "ปด"
},
"done": {
"message": "เสรจสน"
},

@ -1,16 +1,7 @@
{
"confirmClear": {
"message": "Onaylanmış web sitelerini silmek istediğinizden emin misiniz?"
},
"clearApprovalData": {
"message": "Onay verilerini temizle"
},
"reject": {
"message": "Reddetmek"
},
"providerRequestInfo": {
"message": "Aşağıda listelenen etki alanı, Ethereum API'sine erişim talep etmeye çalışmaktadır, böylece Ethereum blockchain ile etkileşime girebilir. Web3 erişimini onaylamadan önce her zaman doğru sitede olduğunuzu kontrol edin."
},
"account": {
"message": "Hesap"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "Режим конфіденційності тепер увімкнено за замовчуванням"
},
"chartOnlyAvailableEth": {
"message": "Таблиця доступна тільки в мережах Ethereum."
},
"confirmClear": {
"message": "Ви впевнені, що хочете очистити затверджені веб-сайти?"
},
"contractInteraction": {
"message": "Контрактна взаємодія"
},
"clearApprovalData": {
"message": "Очистити приватні дані"
},
"reject": {
"message": "Відхилити"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 бажає підключитися до вашого облікового запису"
},
"providerRequestInfo": {
"message": "Цей сайт запитує доступ на перегляд вашої поточної адреси облікового запису. Завжди взаємодійте лише з веб-сайтами, яким довіряєте."
},
"about": {
"message": "Про Google Chrome"
},
@ -248,9 +236,6 @@
"connect": {
"message": "Під’єднатися"
},
"connectRequest": {
"message": "Запит на з'єднання"
},
"connectingTo": {
"message": "Під'єднуємось до $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "Якщо ви вже маєте ефір, пряме переведення – найшвидший спосіб передати ефір у свій гаманець."
},
"dismiss": {
"message": "Відхилити"
},
"done": {
"message": "Готово"
},

@ -1,16 +1,7 @@
{
"confirmClear": {
"message": "Bạn có chắc chắn muốn xóa các trang web được phê duyệt không?"
},
"clearApprovalData": {
"message": "Xóa dữ liệu phê duyệt"
},
"reject": {
"message": "Từ chối"
},
"providerRequestInfo": {
"message": "Miền được liệt kê bên dưới đang cố gắng yêu cầu quyền truy cập vào API Ethereum để nó có thể tương tác với chuỗi khối Ethereum. Luôn kiểm tra kỹ xem bạn có đang ở đúng trang web trước khi phê duyệt quyền truy cập Ethereum hay không."
},
"account": {
"message": "Tài khoản"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "现已默认启用隐私模式"
},
"chartOnlyAvailableEth": {
"message": "聊天功能仅对以太坊网络开放。"
},
"confirmClear": {
"message": "您确定要清除已批准的网站吗?"
},
"contractInteraction": {
"message": "合约交互"
},
"clearApprovalData": {
"message": "清除批准数据"
},
"reject": {
"message": "拒绝"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 希望关联您的账户"
},
"providerRequestInfo": {
"message": "下面列出的域正在尝试请求访问Ethereum API,以便它可以与以太坊区块链进行交互。在批准Ethereum访问之前,请务必仔细检查您是否在正确的站点上。"
},
"about": {
"message": "关于"
},
@ -87,7 +75,7 @@
"message": "添加推荐代币"
},
"addAcquiredTokens": {
"message": "在Metamask上添加已用的代币"
"message": "在MetaMask上添加已用的代币"
},
"amount": {
"message": "数量"
@ -248,9 +236,6 @@
"connect": {
"message": "连接"
},
"connectRequest": {
"message": "关联请求"
},
"connectingTo": {
"message": "正在连接 $1"
},
@ -368,9 +353,6 @@
"directDepositEtherExplainer": {
"message": "如果你已经有了一些 Ether,通过直接转入是你的新钱包获取 Ether 的最快捷方式。"
},
"dismiss": {
"message": "关闭"
},
"done": {
"message": "完成"
},

@ -1,28 +1,16 @@
{
"privacyModeDefault": {
"message": "隱私模式現已根據預設開啟"
},
"chartOnlyAvailableEth": {
"message": "圖表僅適用於以太坊網路。"
},
"confirmClear": {
"message": "您確定要清除已批准的網站紀錄?"
},
"contractInteraction": {
"message": "合約互動"
},
"clearApprovalData": {
"message": "清除批准數據"
},
"reject": {
"message": "拒絕"
},
"providerRequest": {
"likeToConnect": {
"message": "$1 請求訪問帳戶權限"
},
"providerRequestInfo": {
"message": "此網站希望能讀取您的帳戶資訊。請務必確認您信任這個網站、並了解後續可能的交易行為。"
},
"about": {
"message": "關於"
},
@ -245,9 +233,6 @@
"connect": {
"message": "連線"
},
"connectRequest": {
"message": "連線請求"
},
"connectingTo": {
"message": "連線到$1"
},
@ -365,9 +350,6 @@
"directDepositEtherExplainer": {
"message": "如果您已經擁有乙太幣,直接存入功能是讓新錢包最快取得乙太幣的方式。"
},
"dismiss": {
"message": "關閉"
},
"done": {
"message": "完成"
},

@ -0,0 +1,3 @@
<svg width="131" height="2" viewBox="0 0 131 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1H134" stroke="#CDD1E4" stroke-linejoin="round" stroke-dasharray="8 0"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6483 7.41741C14.3482 7.41741 15.8272 8.36346 16.5874 9.75779L18.8081 8.36985C17.5833 6.23784 15.2835 4.802 12.6483 4.802C9.11954 4.802 6.19214 7.37666 5.64216 10.7499H2.125C1.50368 10.7499 1 11.2536 1 11.8749C1 12.4962 1.50368 12.9999 2.125 12.9999H5.63386C6.16206 16.3987 9.1014 18.9999 12.6483 18.9999C15.2235 18.9999 17.4784 17.6287 18.7228 15.5766L16.5042 14.1901C15.7225 15.5041 14.2882 16.3845 12.6483 16.3845C10.1721 16.3845 8.16472 14.3772 8.16472 11.901C8.16472 9.42476 10.1721 7.41741 12.6483 7.41741ZM11.7002 11.9999C11.7002 12.5522 12.1479 12.9999 12.7002 12.9999C13.2525 12.9999 13.7002 12.5522 13.7002 11.9999C13.7002 11.4476 13.2525 10.9999 12.7002 10.9999C12.1479 10.9999 11.7002 11.4476 11.7002 11.9999ZM12.7002 14.9999C11.0433 14.9999 9.7002 13.6568 9.7002 11.9999C9.7002 10.3431 11.0433 8.99992 12.7002 8.99992C13.9109 8.99992 14.9542 9.71715 15.4282 10.7499H21.8751C22.4965 10.7499 23.0001 11.2536 23.0001 11.8749C23.0001 12.4962 22.4965 12.9999 21.8751 12.9999H15.5295C15.1177 14.1651 14.0064 14.9999 12.7002 14.9999Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

@ -64,6 +64,7 @@ const isEdge = !isIE && !!window.StyleMedia
let popupIsOpen = false
let notificationIsOpen = false
const openMetamaskTabsIDs = {}
const requestAccountTabIds = {}
// state persistence
const localStore = new LocalStore()
@ -75,7 +76,6 @@ initialize().catch(log.error)
// setup metamask mesh testing container
const { submitMeshMetricsEntry } = setupMetamaskMeshMetrics()
/**
* An object representing a transaction, in whatever state it is in.
* @typedef TransactionMeta
@ -248,6 +248,12 @@ function setupController (initState, initLangCode) {
// platform specific api
platform,
encryptor: isEdge ? new EdgeEncryptor() : undefined,
getRequestAccountTabIds: () => {
return requestAccountTabIds
},
getOpenMetamaskTabsIds: () => {
return openMetamaskTabsIDs
},
})
const provider = controller.provider
@ -387,6 +393,17 @@ function setupController (initState, initLangCode) {
})
}
} else {
if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) {
const tabId = remotePort.sender.tab.id
const url = new URL(remotePort.sender.url)
const origin = url.hostname
remotePort.onMessage.addListener(msg => {
if (msg.data && msg.data.method === 'eth_requestAccounts') {
requestAccountTabIds[origin] = tabId
}
})
}
connectExternal(remotePort)
}
}
@ -411,7 +428,7 @@ function setupController (initState, initLangCode) {
controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)
controller.typedMessageManager.on('updateBadge', updateBadge)
controller.providerApprovalController.memStore.on('update', updateBadge)
controller.permissionsController.permissions.subscribe(updateBadge)
/**
* Updates the Web Extension's "badge" number, on the little fox in the toolbar.
@ -423,8 +440,8 @@ function setupController (initState, initLangCode) {
const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
const unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount
const unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount
const pendingProviderRequests = controller.providerApprovalController.memStore.getState().providerRequests.length
const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingProviderRequests
const pendingPermissionRequests = Object.keys(controller.permissionsController.permissions.state.permissionsRequests).length
const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingPermissionRequests
if (count) {
label = String(count)
}

@ -2,7 +2,6 @@ const fs = require('fs')
const path = require('path')
const pump = require('pump')
const log = require('loglevel')
const Dnode = require('dnode')
const querystring = require('querystring')
const { Writable } = require('readable-stream')
const LocalMessageDuplexStream = require('post-message-stream')
@ -21,7 +20,7 @@ const inpageBundle = inpageContent + inpageSuffix
// If we create a FireFox-only code path using that API,
// MetaMask will be much faster loading and performant on Firefox.
if (shouldInjectWeb3()) {
if (shouldInjectProvider()) {
injectScript(inpageBundle)
start()
}
@ -40,7 +39,7 @@ function injectScript (content) {
container.insertBefore(scriptTag, container.children[0])
container.removeChild(scriptTag)
} catch (e) {
console.error('MetaMask script injection failed', e)
console.error('MetaMask provider injection failed.', e)
}
}
@ -132,12 +131,6 @@ async function setupStreams () {
// connect "phishing" channel to warning system
const phishingStream = extensionMux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning)
// connect "publicApi" channel to submit page metadata
const publicApiStream = extensionMux.createStream('publicApi')
const background = await setupPublicApi(publicApiStream)
return { background }
}
function forwardTrafficBetweenMuxers (channelName, muxA, muxB) {
@ -151,39 +144,6 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) {
)
}
async function setupPublicApi (outStream) {
const api = {
getSiteMetadata: (cb) => cb(null, getSiteMetadata()),
}
const dnode = Dnode(api)
pump(
outStream,
dnode,
outStream,
(err) => {
// report any error
if (err) {
log.error(err)
}
}
)
const background = await new Promise(resolve => dnode.once('remote', resolve))
return background
}
/**
* Gets site metadata and returns it
*
*/
function getSiteMetadata () {
// get metadata
const metadata = {
name: getSiteName(window),
icon: getSiteIcon(window),
}
return metadata
}
/**
* Error handler for page to extension stream disconnections
*
@ -199,11 +159,11 @@ function logStreamDisconnectWarning (remoteLabel, err) {
}
/**
* Determines if Web3 should be injected
* Determines if the provider should be injected
*
* @returns {boolean} {@code true} if Web3 should be injected
* @returns {boolean} {@code true} if the provider should be injected
*/
function shouldInjectWeb3 () {
function shouldInjectProvider () {
return doctypeCheck() && suffixCheck() &&
documentElementCheck() && !blacklistedDomainCheck()
}
@ -226,8 +186,8 @@ function doctypeCheck () {
* Returns whether or not the extension (suffix) of the current document is prohibited
*
* This checks {@code window.location.pathname} against a set of file extensions
* that should not have web3 injected into them. This check is indifferent of query parameters
* in the location.
* that we should not inject the provider into. This check is indifferent of
* query parameters in the location.
*
* @returns {boolean} whether or not the extension of the current document is prohibited
*/
@ -300,46 +260,6 @@ function redirectToPhishingWarning () {
})}`
}
/**
* Extracts a name for the site from the DOM
*/
function getSiteName (window) {
const document = window.document
const siteName = document.querySelector('head > meta[property="og:site_name"]')
if (siteName) {
return siteName.content
}
const metaTitle = document.querySelector('head > meta[name="title"]')
if (metaTitle) {
return metaTitle.content
}
return document.title
}
/**
* Extracts an icon for the site from the DOM
*/
function getSiteIcon (window) {
const document = window.document
// Use the site's favicon if it exists
const shortcutIcon = document.querySelector('head > link[rel="shortcut icon"]')
if (shortcutIcon) {
return shortcutIcon.href
}
// Search through available icons in no particular order
const icon = Array.from(document.querySelectorAll('head > link[rel="icon"]')).find((icon) => Boolean(icon.href))
if (icon) {
return icon.href
}
return null
}
/**
* Returns a promise that resolves when the DOM is loaded (does not wait for images to load)
*/
@ -349,5 +269,5 @@ async function domIsReady () {
return
}
// wait for load
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true }))
return new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true }))
}

@ -0,0 +1,377 @@
const JsonRpcEngine = require('json-rpc-engine')
const asMiddleware = require('json-rpc-engine/src/asMiddleware')
const ObservableStore = require('obs-store')
const RpcCap = require('rpc-cap').CapabilitiesController
const { ethErrors } = require('eth-json-rpc-errors')
const getRestrictedMethods = require('./restrictedMethods')
const createMethodMiddleware = require('./methodMiddleware')
const createLoggerMiddleware = require('./loggerMiddleware')
// Methods that do not require any permissions to use:
const SAFE_METHODS = require('./permissions-safe-methods.json')
// some constants
const METADATA_STORE_KEY = 'domainMetadata'
const LOG_STORE_KEY = 'permissionsLog'
const HISTORY_STORE_KEY = 'permissionsHistory'
const WALLET_METHOD_PREFIX = 'wallet_'
const CAVEAT_NAMES = {
exposedAccounts: 'exposedAccounts',
}
const ACCOUNTS_CHANGED_NOTIFICATION = 'wallet_accountsChanged'
class PermissionsController {
constructor (
{
platform, notifyDomain, notifyAllDomains, keyringController,
} = {},
restoredPermissions = {},
restoredState = {}) {
this.store = new ObservableStore({
[METADATA_STORE_KEY]: restoredState[METADATA_STORE_KEY] || {},
[LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [],
[HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {},
})
this.notifyDomain = notifyDomain
this.notifyAllDomains = notifyAllDomains
this.keyringController = keyringController
this._platform = platform
this._restrictedMethods = getRestrictedMethods(this)
this._initializePermissions(restoredPermissions)
}
createMiddleware ({ origin, extensionId }) {
if (extensionId) {
this.store.updateState({
[METADATA_STORE_KEY]: {
...this.store.getState()[METADATA_STORE_KEY],
[origin]: { extensionId },
},
})
}
const engine = new JsonRpcEngine()
engine.push(createLoggerMiddleware({
walletPrefix: WALLET_METHOD_PREFIX,
restrictedMethods: Object.keys(this._restrictedMethods),
ignoreMethods: [ 'wallet_sendDomainMetadata' ],
store: this.store,
logStoreKey: LOG_STORE_KEY,
historyStoreKey: HISTORY_STORE_KEY,
}))
engine.push(createMethodMiddleware({
store: this.store,
storeKey: METADATA_STORE_KEY,
getAccounts: this.getAccounts.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind(
this, origin, { eth_accounts: {} }
),
}))
engine.push(this.permissions.providerMiddlewareFunction.bind(
this.permissions, { origin }
))
return asMiddleware(engine)
}
/**
* Returns the accounts that should be exposed for the given origin domain,
* if any. This method exists for when a trusted context needs to know
* which accounts are exposed to a given domain.
*
* @param {string} origin - The origin string.
*/
getAccounts (origin) {
return new Promise((resolve, _) => {
const req = { method: 'eth_accounts' }
const res = {}
this.permissions.providerMiddlewareFunction(
{ origin }, req, res, () => {}, _end
)
function _end () {
if (res.error || !Array.isArray(res.result)) {
resolve([])
} else {
resolve(res.result)
}
}
})
}
/**
* Submits a permissions request to rpc-cap. Internal use only.
*
* @param {string} origin - The origin string.
* @param {IRequestedPermissions} permissions - The requested permissions.
*/
_requestPermissions (origin, permissions) {
return new Promise((resolve, reject) => {
const req = { method: 'wallet_requestPermissions', params: [permissions] }
const res = {}
this.permissions.providerMiddlewareFunction(
{ origin }, req, res, () => {}, _end
)
function _end (err) {
if (err || res.error) {
reject(err || res.error)
} else {
resolve(res.result)
}
}
})
}
/**
* User approval callback. The request can fail if the request is invalid.
*
* @param {object} approved the approved request object
*/
async approvePermissionsRequest (approved, accounts) {
const { id } = approved.metadata
const approval = this.pendingApprovals[id]
try {
// attempt to finalize the request and resolve it
await this.finalizePermissionsRequest(approved.permissions, accounts)
approval.resolve(approved.permissions)
} catch (err) {
// if finalization fails, reject the request
approval.reject(ethErrors.rpc.invalidRequest({
message: err.message, data: err,
}))
}
delete this.pendingApprovals[id]
}
/**
* User rejection callback.
*
* @param {string} id the id of the rejected request
*/
async rejectPermissionsRequest (id) {
const approval = this.pendingApprovals[id]
approval.reject(ethErrors.provider.userRejectedRequest())
delete this.pendingApprovals[id]
}
/**
* Grants the given origin the eth_accounts permission for the given account(s).
* This method should ONLY be called as a result of direct user action in the UI,
* with the intention of supporting legacy dapps that don't support EIP 1102.
*
* @param {string} origin - The origin to expose the account(s) to.
* @param {Array<string>} accounts - The account(s) to expose.
*/
async legacyExposeAccounts (origin, accounts) {
const permissions = {
eth_accounts: {},
}
await this.finalizePermissionsRequest(permissions, accounts)
let error
try {
await new Promise((resolve, reject) => {
this.permissions.grantNewPermissions(origin, permissions, {}, err => err ? resolve() : reject(err))
})
} catch (err) {
error = err
}
if (error) {
if (error.code === 4001) {
throw error
} else {
throw ethErrors.rpc.internal({
message: `Failed to add 'eth_accounts' to '${origin}'.`,
data: {
originalError: error,
accounts,
},
})
}
}
}
/**
* Update the accounts exposed to the given origin.
* Throws error if the update fails.
*
* @param {string} origin - The origin to change the exposed accounts for.
* @param {string[]} accounts - The new account(s) to expose.
*/
async updateExposedAccounts (origin, accounts) {
await this.validateExposedAccounts(accounts)
this.permissions.updateCaveatFor(
origin, 'eth_accounts', CAVEAT_NAMES.exposedAccounts, accounts
)
this.notifyDomain(origin, {
method: ACCOUNTS_CHANGED_NOTIFICATION,
result: accounts,
})
}
/**
* Finalizes a permissions request.
* Throws if request validation fails.
*
* @param {Object} requestedPermissions - The requested permissions.
* @param {string[]} accounts - The accounts to expose, if any.
*/
async finalizePermissionsRequest (requestedPermissions, accounts) {
const { eth_accounts: ethAccounts } = requestedPermissions
if (ethAccounts) {
await this.validateExposedAccounts(accounts)
if (!ethAccounts.caveats) {
ethAccounts.caveats = []
}
// caveat names are unique, and we will only construct this caveat here
ethAccounts.caveats = ethAccounts.caveats.filter(c => (
c.name !== CAVEAT_NAMES.exposedAccounts
))
ethAccounts.caveats.push(
{
type: 'filterResponse',
value: accounts,
name: CAVEAT_NAMES.exposedAccounts,
},
)
}
}
/**
* Validate an array of accounts representing accounts to be exposed
* to a domain. Throws error if validation fails.
*
* @param {string[]} accounts - An array of addresses.
*/
async validateExposedAccounts (accounts) {
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error('Must provide non-empty array of account(s).')
}
// assert accounts exist
const allAccounts = await this.keyringController.getAccounts()
accounts.forEach(acc => {
if (!allAccounts.includes(acc)) {
throw new Error(`Unknown account: ${acc}`)
}
})
}
/**
* Removes the given permissions for the given domain.
* @param {object} domains { origin: [permissions] }
*/
removePermissionsFor (domains) {
Object.entries(domains).forEach(([origin, perms]) => {
this.permissions.removePermissionsFor(
origin,
perms.map(methodName => {
if (methodName === 'eth_accounts') {
this.notifyDomain(
origin,
{ method: ACCOUNTS_CHANGED_NOTIFICATION, result: [] }
)
}
return { parentCapability: methodName }
})
)
})
}
/**
* Removes all known domains and their related permissions.
*/
clearPermissions () {
this.permissions.clearDomains()
this.notifyAllDomains({
method: ACCOUNTS_CHANGED_NOTIFICATION,
result: [],
})
}
/**
* A convenience method for retrieving a login object
* or creating a new one if needed.
*
* @param {string} origin = The origin string representing the domain.
*/
_initializePermissions (restoredState) {
// these permission requests are almost certainly stale
const initState = { ...restoredState, permissionsRequests: [] }
this.pendingApprovals = {}
this.permissions = new RpcCap({
// Supports passthrough methods:
safeMethods: SAFE_METHODS,
// optional prefix for internal methods
methodPrefix: WALLET_METHOD_PREFIX,
restrictedMethods: this._restrictedMethods,
/**
* A promise-returning callback used to determine whether to approve
* permissions requests or not.
*
* Currently only returns a boolean, but eventually should return any
* specific parameters or amendments to the permissions.
*
* @param {string} req - The internal rpc-cap user request object.
*/
requestUserApproval: async (req) => {
const { metadata: { id } } = req
this._platform.openExtensionInBrowser('connect')
return new Promise((resolve, reject) => {
this.pendingApprovals[id] = { resolve, reject }
})
},
}, initState)
}
}
module.exports = {
PermissionsController,
addInternalMethodPrefix: prefix,
CAVEAT_NAMES,
}
function prefix (method) {
return WALLET_METHOD_PREFIX + method
}

@ -0,0 +1,169 @@
const clone = require('clone')
const { isValidAddress } = require('ethereumjs-util')
const LOG_LIMIT = 100
/**
* Create middleware for logging requests and responses to restricted and
* permissions-related methods.
*/
module.exports = function createLoggerMiddleware ({
walletPrefix, restrictedMethods, store, logStoreKey, historyStoreKey, ignoreMethods,
}) {
return (req, res, next, _end) => {
let activityEntry, requestedMethods
const { origin, method } = req
const isInternal = method.startsWith(walletPrefix)
if ((isInternal || restrictedMethods.includes(method)) && !ignoreMethods.includes(method)) {
activityEntry = logActivity(req, isInternal)
if (method === `${walletPrefix}requestPermissions`) {
requestedMethods = getRequestedMethods(req)
}
} else if (method === 'eth_requestAccounts') {
activityEntry = logActivity(req, isInternal)
requestedMethods = [ 'eth_accounts' ]
} else {
return next()
}
next(cb => {
const time = Date.now()
addResponse(activityEntry, res, time)
if (!res.error && requestedMethods) {
logHistory(requestedMethods, origin, res.result, time, method === 'eth_requestAccounts')
}
cb()
})
}
function logActivity (request, isInternal) {
const activityEntry = {
id: request.id,
method: request.method,
methodType: isInternal ? 'internal' : 'restricted',
origin: request.origin,
request: cloneObj(request),
requestTime: Date.now(),
response: null,
responseTime: null,
success: null,
}
commitActivity(activityEntry)
return activityEntry
}
function addResponse (activityEntry, response, time) {
if (!response) {
return
}
activityEntry.response = cloneObj(response)
activityEntry.responseTime = time
activityEntry.success = !response.error
}
function commitActivity (entry) {
const logs = store.getState()[logStoreKey]
if (logs.length > LOG_LIMIT - 2) {
logs.pop()
}
logs.push(entry)
store.updateState({ [logStoreKey]: logs })
}
function getRequestedMethods (request) {
if (
!request.params ||
typeof request.params[0] !== 'object' ||
Array.isArray(request.params[0])
) {
return null
}
return Object.keys(request.params[0])
}
function logHistory (requestedMethods, origin, result, time, isEthRequestAccounts) {
let accounts, entries
if (isEthRequestAccounts) {
accounts = result
const accountToTimeMap = accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {})
entries = { 'eth_accounts': { accounts: accountToTimeMap, lastApproved: time } }
} else {
entries = result
? result
.map(perm => {
if (perm.parentCapability === 'eth_accounts') {
accounts = getAccountsFromPermission(perm)
}
return perm.parentCapability
})
.reduce((acc, m) => {
if (requestedMethods.includes(m)) {
if (m === 'eth_accounts') {
const accountToTimeMap = accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {})
acc[m] = { lastApproved: time, accounts: accountToTimeMap }
} else {
acc[m] = { lastApproved: time }
}
}
return acc
}, {})
: {}
}
if (Object.keys(entries).length > 0) {
commitHistory(origin, entries)
}
}
function commitHistory (origin, entries) {
const history = store.getState()[historyStoreKey] || {}
const newOriginHistory = {
...history[origin],
...entries,
}
if (history[origin] && history[origin]['eth_accounts'] && entries['eth_accounts']) {
newOriginHistory['eth_accounts'] = {
lastApproved: entries['eth_accounts'].lastApproved,
accounts: {
...history[origin]['eth_accounts'].accounts,
...entries['eth_accounts'].accounts,
},
}
}
history[origin] = newOriginHistory
store.updateState({ [historyStoreKey]: history })
}
}
// the call to clone is set to disallow circular references
// we attempt cloning at a depth of 3 and 2, then return a
// shallow copy of the object
function cloneObj (obj) {
for (let i = 3; i > 1; i--) {
try {
return clone(obj, false, i)
} catch (_) {}
}
return { ...obj }
}
function getAccountsFromPermission (perm) {
if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) {
return []
}
const accounts = {}
for (const c of perm.caveats) {
if (c.type === 'filterResponse' && Array.isArray(c.value)) {
for (const v of c.value) {
if (isValidAddress(v)) {
accounts[v] = true
}
}
}
}
return Object.keys(accounts)
}

@ -0,0 +1,90 @@
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
const { ethErrors } = require('eth-json-rpc-errors')
/**
* Create middleware for handling certain methods and preprocessing permissions requests.
*/
module.exports = function createMethodMiddleware ({
store, storeKey, getAccounts, requestAccountsPermission,
}) {
return createAsyncMiddleware(async (req, res, next) => {
if (typeof req.method !== 'string') {
res.error = ethErrors.rpc.invalidRequest({ data: req})
return
}
switch (req.method) {
// intercepting eth_accounts requests for backwards compatibility,
// i.e. return an empty array instead of an error
case 'eth_accounts':
res.result = await getAccounts()
return
case 'eth_requestAccounts':
// first, just try to get accounts
let accounts = await getAccounts()
if (accounts.length > 0) {
res.result = accounts
return
}
// if no accounts, request the accounts permission
try {
await requestAccountsPermission()
} catch (err) {
res.error = err
return
}
// get the accounts again
accounts = await getAccounts()
if (accounts.length > 0) {
res.result = accounts
} else {
// this should never happen
res.error = ethErrors.rpc.internal(
'Accounts unexpectedly unavailable. Please report this bug.'
)
}
return
// custom method for getting metadata from the requesting domain
case 'wallet_sendDomainMetadata':
const storeState = store.getState()[storeKey]
const extensionId = storeState[req.origin]
? storeState[req.origin].extensionId
: undefined
if (
req.domainMetadata &&
typeof req.domainMetadata.name === 'string'
) {
store.updateState({
[storeKey]: {
...storeState,
[req.origin]: {
extensionId,
...req.domainMetadata,
},
},
})
}
res.result = true
return
default:
break
}
next()
})
}

@ -0,0 +1,49 @@
[
"web3_sha3",
"net_listening",
"net_peerCount",
"net_version",
"eth_blockNumber",
"eth_call",
"eth_chainId",
"eth_coinbase",
"eth_estimateGas",
"eth_gasPrice",
"eth_getBalance",
"eth_getBlockByHash",
"eth_getBlockByNumber",
"eth_getBlockTransactionCountByHash",
"eth_getBlockTransactionCountByNumber",
"eth_getCode",
"eth_getFilterChanges",
"eth_getFilterLogs",
"eth_getLogs",
"eth_getStorageAt",
"eth_getTransactionByBlockHashAndIndex",
"eth_getTransactionByBlockNumberAndIndex",
"eth_getTransactionByHash",
"eth_getTransactionCount",
"eth_getTransactionReceipt",
"eth_getUncleByBlockHashAndIndex",
"eth_getUncleByBlockNumberAndIndex",
"eth_getUncleCountByBlockHash",
"eth_getUncleCountByBlockNumber",
"eth_getWork",
"eth_hashrate",
"eth_mining",
"eth_newBlockFilter",
"eth_newFilter",
"eth_newPendingTransactionFilter",
"eth_protocolVersion",
"eth_sendRawTransaction",
"eth_sendTransaction",
"eth_sign",
"personal_sign",
"eth_signTypedData",
"eth_signTypedData_v1",
"eth_signTypedData_v3",
"eth_submitHashrate",
"eth_submitWork",
"eth_syncing",
"eth_uninstallFilter"
]

@ -0,0 +1,20 @@
module.exports = function getRestrictedMethods (permissionsController) {
return {
'eth_accounts': {
description: 'View the address of the selected account',
method: (_, res, __, end) => {
permissionsController.keyringController.getAccounts()
.then((accounts) => {
res.result = accounts
end()
})
.catch((err) => {
res.error = err
end(err)
})
},
},
}
}

@ -1,4 +1,5 @@
const ObservableStore = require('obs-store')
const { addInternalMethodPrefix } = require('./permissions')
const normalizeAddress = require('eth-sig-util').normalize
const { isValidAddress, sha3, bufferToHex } = require('ethereumjs-util')
const extend = require('xtend')
@ -57,7 +58,6 @@ class PreferencesController {
useNativeCurrencyAsPrimaryCurrency: true,
},
completedOnboarding: false,
migratedPrivacyMode: false,
metaMetricsId: null,
metaMetricsSendCount: 0,
}, opts.initState)
@ -187,7 +187,10 @@ class PreferencesController {
* @param {Function} - end
*/
async requestWatchAsset (req, res, next, end) {
if (req.method === 'metamask_watchAsset' || req.method === 'wallet_watchAsset') {
if (
req.method === 'metamask_watchAsset' ||
req.method === addInternalMethodPrefix('watchAsset')
) {
const { type, options } = req.params
switch (type) {
case 'ERC20':
@ -644,13 +647,6 @@ class PreferencesController {
return Promise.resolve(true)
}
unsetMigratedPrivacyMode () {
this.store.updateState({
migratedPrivacyMode: false,
})
return Promise.resolve()
}
//
// PRIVATE METHODS
//

@ -1,177 +0,0 @@
const ObservableStore = require('obs-store')
const SafeEventEmitter = require('safe-event-emitter')
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
const { errors: rpcErrors } = require('eth-json-rpc-errors')
/**
* A controller that services user-approved requests for a full Ethereum provider API
*/
class ProviderApprovalController extends SafeEventEmitter {
/**
* Creates a ProviderApprovalController
*
* @param {Object} [config] - Options to configure controller
*/
constructor ({ closePopup, initState, keyringController, openPopup, preferencesController } = {}) {
super()
this.closePopup = closePopup
this.keyringController = keyringController
this.openPopup = openPopup
this.preferencesController = preferencesController
this.memStore = new ObservableStore({
providerRequests: [],
})
const defaultState = { approvedOrigins: {} }
this.store = new ObservableStore(Object.assign(defaultState, initState))
}
/**
* Called when a user approves access to a full Ethereum provider API
*
* @param {object} opts - opts for the middleware contains the origin for the middleware
*/
createMiddleware ({ senderUrl, extensionId, getSiteMetadata }) {
return createAsyncMiddleware(async (req, res, next) => {
// only handle requestAccounts
if (req.method !== 'eth_requestAccounts') {
return next()
}
// if already approved or privacy mode disabled, return early
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const origin = senderUrl.hostname
if (this.shouldExposeAccounts(origin) && isUnlocked) {
res.result = [this.preferencesController.getSelectedAddress()]
return
}
// register the provider request
const metadata = { hostname: senderUrl.hostname, origin }
if (extensionId) {
metadata.extensionId = extensionId
} else {
const siteMetadata = await getSiteMetadata(origin)
Object.assign(metadata, { siteTitle: siteMetadata.name, siteImage: siteMetadata.icon})
}
this._handleProviderRequest(metadata)
// wait for resolution of request
const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved)))
if (approved) {
res.result = [this.preferencesController.getSelectedAddress()]
} else {
throw rpcErrors.eth.userRejectedRequest('User denied account authorization')
}
})
}
/**
* @typedef {Object} SiteMetadata
* @param {string} hostname - The hostname of the site
* @param {string} origin - The origin of the site
* @param {string} [siteTitle] - The title of the site
* @param {string} [siteImage] - The icon for the site
* @param {string} [extensionId] - The extension ID of the extension
*/
/**
* Called when a tab requests access to a full Ethereum provider API
*
* @param {SiteMetadata} siteMetadata - The metadata for the site requesting full provider access
*/
_handleProviderRequest (siteMetadata) {
const { providerRequests } = this.memStore.getState()
const origin = siteMetadata.origin
this.memStore.updateState({
providerRequests: [
...providerRequests,
siteMetadata,
],
})
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const { approvedOrigins } = this.store.getState()
const originAlreadyHandled = approvedOrigins[origin]
if (originAlreadyHandled && isUnlocked) {
return
}
this.openPopup && this.openPopup()
}
/**
* Called when a user approves access to a full Ethereum provider API
*
* @param {string} origin - origin of the domain that had provider access approved
*/
approveProviderRequestByOrigin (origin) {
if (this.closePopup) {
this.closePopup()
}
const { approvedOrigins } = this.store.getState()
const { providerRequests } = this.memStore.getState()
const providerRequest = providerRequests.find((request) => request.origin === origin)
const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin)
this.store.updateState({
approvedOrigins: {
...approvedOrigins,
[origin]: {
siteTitle: providerRequest ? providerRequest.siteTitle : null,
siteImage: providerRequest ? providerRequest.siteImage : null,
hostname: providerRequest ? providerRequest.hostname : null,
},
},
})
this.memStore.updateState({ providerRequests: remainingProviderRequests })
this.emit(`resolvedRequest:${origin}`, { approved: true })
}
/**
* Called when a tab rejects access to a full Ethereum provider API
*
* @param {string} origin - origin of the domain that had provider access approved
*/
rejectProviderRequestByOrigin (origin) {
if (this.closePopup) {
this.closePopup()
}
const { approvedOrigins } = this.store.getState()
const { providerRequests } = this.memStore.getState()
const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin)
// We're cloning and deleting keys here because we don't want to keep unneeded keys
const _approvedOrigins = Object.assign({}, approvedOrigins)
delete _approvedOrigins[origin]
this.store.putState({ approvedOrigins: _approvedOrigins })
this.memStore.putState({ providerRequests: remainingProviderRequests })
this.emit(`resolvedRequest:${origin}`, { approved: false })
}
/**
* Clears any approvals for user-approved origins
*/
clearApprovedOrigins () {
this.store.updateState({
approvedOrigins: {},
})
}
/**
* Determines if a given origin should have accounts exposed
*
* @param {string} origin - Domain origin to check for approval status
* @returns {boolean} - True if the origin has been approved
*/
shouldExposeAccounts (origin) {
return Boolean(this.store.getState().approvedOrigins[origin])
}
/**
* Returns a merged state representation
* @return {object}
* @private
*/
_getMergedState () {
return Object.assign({}, this.memStore.getState(), this.store.getState())
}
}
module.exports = ProviderApprovalController

@ -3,7 +3,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
const { errors: rpcErrors } = require('eth-json-rpc-errors')
const { ethErrors } = require('eth-json-rpc-errors')
const abi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
@ -54,6 +54,7 @@ const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util')
@param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for
@param {Function} [opts.getGasPrice] - optional gas price calculator
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
@param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@ -66,6 +67,7 @@ class TransactionController extends EventEmitter {
this.networkStore = opts.networkStore || new ObservableStore({})
this.preferencesStore = opts.preferencesStore || new ObservableStore({})
this.provider = opts.provider
this.getPermittedAccounts = opts.getPermittedAccounts
this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction
this.getGasPrice = opts.getGasPrice
@ -133,7 +135,7 @@ class TransactionController extends EventEmitter {
/**
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
*/
addTx (txMeta) {
this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta)
@ -148,18 +150,18 @@ class TransactionController extends EventEmitter {
}
/**
add a new unapproved transaction to the pipeline
@returns {Promise<string>} the hash of the transaction after being submitted to the network
@param txParams {object} - txParams for the transaction
@param opts {object} - with the key origin to put the origin on the txMeta
* Add a new unapproved transaction to the pipeline
*
* @returns {Promise<string>} the hash of the transaction after being submitted to the network
* @param txParams {object} - txParams for the transaction
* @param opts {object} - with the key origin to put the origin on the txMeta
*/
async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
initialTxMeta.origin = opts.origin
this.txStateManager.updateTx(initialTxMeta, '#newUnapprovedTransaction - adding the origin')
const initialTxMeta = await this.addUnapprovedTransaction(txParams, opts.origin)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
@ -167,30 +169,27 @@ class TransactionController extends EventEmitter {
case 'submitted':
return resolve(finishedTxMeta.hash)
case 'rejected':
return reject(cleanErrorStack(rpcErrors.eth.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.')))
return reject(cleanErrorStack(ethErrors.provider.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.')))
case 'failed':
return reject(cleanErrorStack(rpcErrors.internal(finishedTxMeta.err.message)))
return reject(cleanErrorStack(ethErrors.rpc.internal(finishedTxMeta.err.message)))
default:
return reject(cleanErrorStack(rpcErrors.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)))
return reject(cleanErrorStack(ethErrors.rpc.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)))
}
})
})
}
/**
Validates and generates a txMeta with defaults and puts it in txStateManager
store
@returns {txMeta}
*/
* Validates and generates a txMeta with defaults and puts it in txStateManager
* store.
*
* @returns {txMeta}
*/
async addUnapprovedTransaction (txParams, origin) {
async addUnapprovedTransaction (txParams) {
// validate
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
// Assert the from address is the selected address
if (normalizedTxParams.from !== this.getSelectedAddress()) {
throw new Error(`Transaction from address isn't valid for this account`)
}
txUtils.validateTxParams(normalizedTxParams)
/**
`generateTxMeta` adds the default txMeta properties to the passed object.
@ -202,6 +201,30 @@ class TransactionController extends EventEmitter {
txParams: normalizedTxParams,
type: TRANSACTION_TYPE_STANDARD,
})
if (origin === 'metamask') {
// Assert the from address is the selected address
if (normalizedTxParams.from !== this.getSelectedAddress()) {
throw ethErrors.rpc.internal({
message: `Internally initiated transaction is using invalid account.`,
data: {
origin,
fromAddress: normalizedTxParams.from,
selectedAddress: this.getSelectedAddress(),
},
})
}
} else {
// Assert that the origin has permissions to initiate transactions from
// the specified address
const permittedAddresses = await this.getPermittedAccounts(origin)
if (!permittedAddresses.includes(normalizedTxParams.from)) {
throw ethErrors.provider.unauthorized({ data: { origin }})
}
}
txMeta['origin'] = origin
const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams)
txMeta.transactionCategory = transactionCategory
this.addTx(txMeta)
@ -222,15 +245,16 @@ class TransactionController extends EventEmitter {
txMeta.loadingDefaults = false
// save txMeta
this.txStateManager.updateTx(txMeta)
this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.')
return txMeta
}
/**
adds the tx gas defaults: gas && gasPrice
@param txMeta {Object} - the txMeta object
@returns {Promise<object>} resolves with txMeta
*/
* Adds the tx gas defaults: gas && gasPrice
* @param txMeta {Object} - the txMeta object
* @returns {Promise<object>} resolves with txMeta
*/
async addTxGasDefaults (txMeta, getCodeResponse) {
const txParams = txMeta.txParams
// ensure value
@ -416,6 +440,7 @@ class TransactionController extends EventEmitter {
this.inProcessOfSigning.delete(txId)
}
}
/**
adds the chain id and signs the transaction and set the status to signed
@param txId {number} - the tx's Id

@ -1,73 +0,0 @@
class StandardProvider {
_isConnected
_provider
constructor (provider) {
this._provider = provider
this._subscribe()
// indicate that we've connected, mostly just for standard compliance
setTimeout(() => {
this._onConnect()
})
}
_onClose () {
if (this._isConnected === undefined || this._isConnected) {
this._provider.emit('close', {
code: 1011,
reason: 'Network connection error',
})
}
this._isConnected = false
}
_onConnect () {
!this._isConnected && this._provider.emit('connect')
this._isConnected = true
}
_subscribe () {
this._provider.on('data', (error, { method, params }) => {
if (!error && method === 'eth_subscription') {
this._provider.emit('notification', params.result)
}
})
}
/**
* Initiate an RPC method call
*
* @param {string} method - RPC method name to call
* @param {string[]} params - Array of RPC method parameters
* @returns {Promise<*>} Promise resolving to the result if successful
*/
send (method, params = []) {
return new Promise((resolve, reject) => {
try {
this._provider.sendAsync({ id: 1, jsonrpc: '2.0', method, params }, (error, response) => {
error = error || response.error
error ? reject(error) : resolve(response)
})
} catch (error) {
reject(error)
}
})
}
}
/**
* Converts a legacy provider into an EIP-1193-compliant standard provider
* @param {Object} provider - Legacy provider to convert
* @returns {Object} Standard provider
*/
export default function createStandardProvider (provider) {
const standardProvider = new StandardProvider(provider)
const sendLegacy = provider.send
provider.send = (methodOrPayload, callbackOrArgs) => {
if (typeof methodOrPayload === 'string' && !callbackOrArgs || Array.isArray(callbackOrArgs)) {
return standardProvider.send(methodOrPayload, callbackOrArgs)
}
return sendLegacy.call(provider, methodOrPayload, callbackOrArgs)
}
return provider
}

@ -1,6 +1,5 @@
/*global Web3*/
// need to make sure we aren't affected by overlapping namespaces
// and that we dont affect the app with our namespace
// mostly a fix for web3's BigNumber if AMD's "define" is defined...
@ -32,17 +31,14 @@ const restoreContextAfterImports = () => {
}
cleanContextForImports()
require('web3/dist/web3.min.js')
const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('metamask-inpage-provider')
const ObjectMultiplex = require('obj-multiplex')
const pump = require('pump')
const promisify = require('pify')
const createStandardProvider = require('./createStandardProvider').default
let warned = false
// TODO:deprecate:2020-01-13
require('web3/dist/web3.min.js')
const setupDappAutoReload = require('./lib/auto-reload.js')
restoreContextAfterImports()
@ -64,106 +60,6 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream)
// set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100)
const pageMux = new ObjectMultiplex()
const onboardingStream = pageMux.createStream('onboarding')
pump(
pageMux,
metamaskStream,
error => log.error('MetaMask muxed in-page traffic failed', error)
)
let warnedOfAutoRefreshDeprecation = false
// augment the provider with its enable method
inpageProvider.enable = function ({ force } = {}) {
if (
!warnedOfAutoRefreshDeprecation &&
inpageProvider.autoRefreshOnNetworkChange
) {
console.warn(`MetaMask: MetaMask will soon stop reloading pages on network change.
If you rely upon this behavior, add a 'networkChanged' event handler to trigger the reload manually: https://metamask.github.io/metamask-docs/API_Reference/Ethereum_Provider#ethereum.on(eventname%2C-callback)
Set 'ethereum.autoRefreshOnNetworkChange' to 'false' to silence this warning: https://metamask.github.io/metamask-docs/API_Reference/Ethereum_Provider#ethereum.autorefreshonnetworkchange'
`)
warnedOfAutoRefreshDeprecation = true
}
return new Promise((resolve, reject) => {
inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => {
if (error || response.error) {
reject(error || response.error)
} else {
resolve(response.result)
}
})
})
}
// give the dapps control of a refresh they can toggle this off on the window.ethereum
// this will be default true so it does not break any old apps.
inpageProvider.autoRefreshOnNetworkChange = true
// publicConfig isn't populated until we get a message from background.
// Using this getter will ensure the state is available
const getPublicConfigWhenReady = async () => {
const store = inpageProvider.publicConfigStore
let state = store.getState()
// if state is missing, wait for first update
if (!state.networkVersion) {
state = await new Promise(resolve => store.once('update', resolve))
console.log('new state', state)
}
return state
}
// add metamask-specific convenience methods
inpageProvider._metamask = new Proxy({
/**
* Synchronously determines if this domain is currently enabled, with a potential false negative if called to soon
*
* @returns {boolean} - returns true if this domain is currently enabled
*/
isEnabled: function () {
const { isEnabled } = inpageProvider.publicConfigStore.getState()
return Boolean(isEnabled)
},
/**
* Asynchronously determines if this domain is currently enabled
*
* @returns {Promise<boolean>} - Promise resolving to true if this domain is currently enabled
*/
isApproved: async function () {
const { isEnabled } = await getPublicConfigWhenReady()
return Boolean(isEnabled)
},
/**
* Determines if MetaMask is unlocked by the user
*
* @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked
*/
isUnlocked: async function () {
const { isUnlocked } = await getPublicConfigWhenReady()
return Boolean(isUnlocked)
},
/**
* Registers a page as having initated onboarding. This facilitates MetaMask focusing the initiating tab after onboarding.
*
* @returns {Promise} - Promise resolving to undefined
*/
registerOnboarding: async () => {
await promisify(onboardingStream.write({ type: 'registerOnboarding' }))
},
}, {
get: function (obj, prop) {
!warned && console.warn('Heads up! ethereum._metamask exposes methods that have ' +
'not been standardized yet. This means that these methods may not be implemented ' +
'in other dapp browsers and may be removed from MetaMask in the future.')
warned = true
return obj[prop]
},
})
// Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound
// `sendAsync` method on the prototype, causing `this` reference issues
const proxiedInpageProvider = new Proxy(inpageProvider, {
@ -172,12 +68,12 @@ const proxiedInpageProvider = new Proxy(inpageProvider, {
deleteProperty: () => true,
})
window.ethereum = createStandardProvider(proxiedInpageProvider)
//
// setup web3
// TODO:deprecate:2020-01-13
//
// setup web3
if (typeof window.web3 !== 'undefined') {
throw new Error(`MetaMask detected another web3.
MetaMask will not work reliably with another web3 extension.
@ -192,9 +88,13 @@ web3.setProvider = function () {
}
log.debug('MetaMask - injected web3')
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
proxiedInpageProvider._web3Ref = web3.eth
// set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress
})
// setup dapp auto reload AND proxy web3
setupDappAutoReload(web3, inpageProvider._publicConfigStore)
//
// end deprecate:2020-01-13
//
window.ethereum = proxiedInpageProvider

@ -1,3 +1,6 @@
// TODO:deprecate:2020-01-13
module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
@ -13,7 +16,7 @@ function setupDappAutoReload (web3, observable) {
lastTimeUsed = Date.now()
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider\nhttps://medium.com/metamask/4a899ad6e59e')
console.warn(`MetaMask: On 2020-01-13, MetaMask will no longer inject web3. For more information, see: https://medium.com/metamask/no-longer-injecting-web3-js-4a899ad6e59e`)
hasBeenWarned = true
}
// return value normally

@ -1,3 +1,4 @@
module.exports = createOriginMiddleware
/**

@ -1,5 +1,6 @@
const extension = require('extensionizer')
const log = require('loglevel')
const { checkForError } = require('./util')
/**
* A wrapper around the extension's storage local API
@ -90,21 +91,3 @@ module.exports = class ExtensionStore {
function isEmpty (obj) {
return Object.keys(obj).length === 0
}
/**
* Returns an Error if extension.runtime.lastError is present
* this is a workaround for the non-standard error object thats used
* @returns {Error}
*/
function checkForError () {
const lastError = extension.runtime.lastError
if (!lastError) {
return
}
// if it quacks like an Error, its an Error
if (lastError.stack && lastError.message) {
return lastError
}
// repair incomplete error object (eg chromium v77)
return new Error(lastError.message)
}

@ -1,7 +1,7 @@
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const { errors: rpcErrors } = require('eth-json-rpc-errors')
const { ethErrors } = require('eth-json-rpc-errors')
const createId = require('./random-id')
/**
@ -85,7 +85,7 @@ module.exports = class MessageManager extends EventEmitter {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.'))
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.'))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}

@ -1,7 +1,7 @@
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const { errors: rpcErrors } = require('eth-json-rpc-errors')
const { ethErrors } = require('eth-json-rpc-errors')
const createId = require('./random-id')
const hexRe = /^[0-9A-Fa-f]+$/g
const log = require('loglevel')
@ -91,7 +91,7 @@ module.exports = class PersonalMessageManager extends EventEmitter {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.'))
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.'))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}

@ -2,7 +2,7 @@ const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const createId = require('./random-id')
const assert = require('assert')
const { errors: rpcErrors } = require('eth-json-rpc-errors')
const { ethErrors } = require('eth-json-rpc-errors')
const sigUtil = require('eth-sig-util')
const log = require('loglevel')
const jsonschema = require('jsonschema')
@ -81,7 +81,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.'))
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.'))
case 'errored':
return reject(new Error(`MetaMask Message Signature: ${data.error}`))
default:

@ -1,3 +1,4 @@
const extension = require('extensionizer')
const ethUtil = require('ethereumjs-util')
const assert = require('assert')
const BN = require('bn.js')
@ -148,6 +149,32 @@ function getRandomArrayItem (array) {
return array[Math.floor((Math.random() * array.length))]
}
function mapObjectValues (object, cb) {
const mappedObject = {}
Object.keys(object).forEach(key => {
mappedObject[key] = cb(key, object[key])
})
return mappedObject
}
/**
* Returns an Error if extension.runtime.lastError is present
* this is a workaround for the non-standard error object thats used
* @returns {Error}
*/
function checkForError () {
const lastError = extension.runtime.lastError
if (!lastError) {
return
}
// if it quacks like an Error, its an Error
if (lastError.stack && lastError.message) {
return lastError
}
// repair incomplete error object (eg chromium v77)
return new Error(lastError.message)
}
module.exports = {
removeListeners,
applyListeners,
@ -159,4 +186,6 @@ module.exports = {
bnToHex,
BnMultiplyByFraction,
getRandomArrayItem,
mapObjectValues,
checkForError,
}

@ -8,11 +8,9 @@ const assert = require('assert').strict
const EventEmitter = require('events')
const pump = require('pump')
const Dnode = require('dnode')
const pify = require('pify')
const extension = require('extensionizer')
const ObservableStore = require('obs-store')
const ComposableObservableStore = require('./lib/ComposableObservableStore')
const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter')
const asStream = require('obs-store/lib/asStream')
const AccountTracker = require('./lib/account-tracker')
const RpcEngine = require('json-rpc-engine')
@ -20,8 +18,8 @@ const debounce = require('debounce')
const createEngineStream = require('json-rpc-middleware-stream/engineStream')
const createFilterMiddleware = require('eth-json-rpc-filters')
const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager')
const createOriginMiddleware = require('./lib/createOriginMiddleware')
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const createOriginMiddleware = require('./lib/createOriginMiddleware')
const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware')
const {setupMultiplex} = require('./lib/stream-utils.js')
const KeyringController = require('eth-keyring-controller')
@ -41,8 +39,8 @@ const TypedMessageManager = require('./lib/typed-message-manager')
const TransactionController = require('./controllers/transactions')
const TokenRatesController = require('./controllers/token-rates')
const DetectTokensController = require('./controllers/detect-tokens')
const ProviderApprovalController = require('./controllers/provider-approval')
const ABTestController = require('./controllers/ab-test')
const { PermissionsController } = require('./controllers/permissions/')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
@ -58,6 +56,7 @@ const TrezorKeyring = require('eth-trezor-keyring')
const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util')
const nanoid = require('nanoid')
const contractMap = require('eth-contract-metadata')
const {
AddressBookController,
@ -90,16 +89,24 @@ module.exports = class MetamaskController extends EventEmitter {
// platform-specific api
this.platform = opts.platform
this.getRequestAccountTabIds = opts.getRequestAccountTabIds
this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds
// observable state store
this.store = new ComposableObservableStore(initState)
// external connections by origin
// Do not modify directly. Use the associated methods.
this.connections = {}
// lock to ensure only one vault created at once
this.createVaultMutex = new Mutex()
// network store
// next, we will initialize the controllers
// controller initializaiton order matters
this.networkController = new NetworkController(initState.NetworkController)
// preferences controller
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
@ -107,17 +114,14 @@ module.exports = class MetamaskController extends EventEmitter {
network: this.networkController,
})
// app-state controller
this.appStateController = new AppStateController({
preferencesStore: this.preferencesController.store,
onInactiveTimeout: () => this.setLocked(),
initState: initState.AppStateController,
})
// currency controller
this.currencyRateController = new CurrencyRateController(undefined, initState.CurrencyController)
// infura controller
this.infuraController = new InfuraController({
initState: initState.InfuraController,
})
@ -125,7 +129,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.phishingController = new PhishingController()
// rpc provider
// now we can initialize the RPC provider, which other controllers require
this.initializeProvider()
this.provider = this.networkController.getProviderAndBlockTracker().provider
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker
@ -154,7 +158,7 @@ module.exports = class MetamaskController extends EventEmitter {
initState: initState.IncomingTransactionsController,
})
// account tracker watches balances, nonces, and any code at their address.
// account tracker watches balances, nonces, and any code at their address
this.accountTracker = new AccountTracker({
provider: this.provider,
blockTracker: this.blockTracker,
@ -188,7 +192,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.accountTracker._updateAccounts()
})
// key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
this.keyringController = new KeyringController({
keyringTypes: additionalKeyrings,
@ -196,16 +199,25 @@ module.exports = class MetamaskController extends EventEmitter {
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
encryptor: opts.encryptor || undefined,
})
this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
// detect tokens controller
this.permissionsController = new PermissionsController({
keyringController: this.keyringController,
platform: opts.platform,
notifyDomain: this.notifyConnections.bind(this),
notifyAllDomains: this.notifyAllConnections.bind(this),
}, initState.PermissionsController, initState.PermissionsMetadata)
this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
})
this.abTestController = new ABTestController({
initState: initState.ABTestController,
})
this.addressBookController = new AddressBookController(undefined, initState.AddressBookController)
this.threeBoxController = new ThreeBoxController({
@ -217,9 +229,9 @@ module.exports = class MetamaskController extends EventEmitter {
version,
})
// tx mgmt
this.txController = new TransactionController({
initState: initState.TransactionController || initState.TransactionManager,
getPermittedAccounts: this.permissionsController.getAccounts.bind(this.permissionsController),
networkStore: this.networkController.networkStore,
preferencesStore: this.preferencesController.store,
txHistoryLimit: 40,
@ -270,18 +282,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen
})
this.providerApprovalController = new ProviderApprovalController({
closePopup: opts.closePopup,
initState: initState.ProviderApprovalController,
keyringController: this.keyringController,
openPopup: opts.openPopup,
preferencesController: this.preferencesController,
})
this.abTestController = new ABTestController({
initState: initState.ABTestController,
})
this.store.updateStructure({
AppStateController: this.appStateController.store,
TransactionController: this.txController.store,
@ -294,10 +294,11 @@ module.exports = class MetamaskController extends EventEmitter {
InfuraController: this.infuraController.store,
CachedBalancesController: this.cachedBalancesController.store,
OnboardingController: this.onboardingController.store,
ProviderApprovalController: this.providerApprovalController.store,
IncomingTransactionsController: this.incomingTransactionsController.store,
ThreeBoxController: this.threeBoxController.store,
ABTestController: this.abTestController.store,
PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store,
ThreeBoxController: this.threeBoxController.store,
})
this.memStore = new ComposableObservableStore(null, {
@ -318,11 +319,9 @@ module.exports = class MetamaskController extends EventEmitter {
ShapeshiftController: this.shapeshiftController,
InfuraController: this.infuraController.store,
OnboardingController: this.onboardingController.store,
// ProviderApprovalController
ProviderApprovalController: this.providerApprovalController.store,
ProviderApprovalControllerMemStore: this.providerApprovalController.memStore,
IncomingTransactionsController: this.incomingTransactionsController.store,
// ThreeBoxController
PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store,
ThreeBoxController: this.threeBoxController.store,
ABTestController: this.abTestController.store,
// ENS Controller
@ -343,20 +342,15 @@ module.exports = class MetamaskController extends EventEmitter {
version,
// account mgmt
getAccounts: async ({ origin }) => {
// Expose no accounts if this origin has not been approved, preventing
// account-requring RPC methods from completing successfully
const exposeAccounts = this.providerApprovalController.shouldExposeAccounts(origin)
if (origin !== 'metamask' && !exposeAccounts) {
return []
}
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const selectedAddress = this.preferencesController.getSelectedAddress()
// only show address if account is unlocked
if (isUnlocked && selectedAddress) {
return [selectedAddress]
} else {
return []
if (origin === 'metamask') {
const selectedAddress = this.preferencesController.getSelectedAddress()
return selectedAddress ? [selectedAddress] : []
} else if (
this.keyringController.memStore.getState().isUnlocked
) {
return await this.permissionsController.getAccounts(origin)
}
return [] // changing this is a breaking change
},
// tx signing
processTransaction: this.newUnapprovedTransaction.bind(this),
@ -377,7 +371,7 @@ module.exports = class MetamaskController extends EventEmitter {
* Constructor helper: initialize a public config store.
* This store is used to make some config info available to Dapps synchronously.
*/
createPublicConfigStore ({ checkIsEnabled }) {
createPublicConfigStore () {
// subset of state for metamask inpage provider
const publicConfigStore = new ObservableStore()
@ -390,23 +384,16 @@ module.exports = class MetamaskController extends EventEmitter {
}
function updatePublicConfigStore (memState) {
const publicState = selectPublicState(memState)
publicConfigStore.putState(publicState)
publicConfigStore.putState(selectPublicState(memState))
}
function selectPublicState ({ isUnlocked, selectedAddress, network, provider }) {
const isEnabled = checkIsEnabled()
const isReady = isUnlocked && isEnabled
const result = {
function selectPublicState ({ isUnlocked, network, provider }) {
return {
isUnlocked,
isEnabled,
selectedAddress: isReady ? selectedAddress : null,
networkVersion: network,
chainId: selectChainId({ network, provider }),
}
return result
}
return publicConfigStore
}
@ -438,13 +425,13 @@ module.exports = class MetamaskController extends EventEmitter {
*/
getApi () {
const keyringController = this.keyringController
const preferencesController = this.preferencesController
const txController = this.txController
const networkController = this.networkController
const providerApprovalController = this.providerApprovalController
const onboardingController = this.onboardingController
const permissionsController = this.permissionsController
const preferencesController = this.preferencesController
const threeBoxController = this.threeBoxController
const abTestController = this.abTestController
const txController = this.txController
return {
// etc
@ -502,7 +489,6 @@ module.exports = class MetamaskController extends EventEmitter {
setPreference: nodeify(preferencesController.setPreference, preferencesController),
completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController),
addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController),
unsetMigratedPrivacyMode: nodeify(preferencesController.unsetMigratedPrivacyMode, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
@ -550,11 +536,6 @@ module.exports = class MetamaskController extends EventEmitter {
signTypedMessage: nodeify(this.signTypedMessage, this),
cancelTypedMessage: this.cancelTypedMessage.bind(this),
// provider approval
approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController),
rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController),
clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController),
// onboarding controller
setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController),
@ -568,10 +549,21 @@ module.exports = class MetamaskController extends EventEmitter {
// a/b test controller
getAssignedABTestGroupName: nodeify(abTestController.getAssignedABTestGroupName, abTestController),
// permissions
approvePermissionsRequest: nodeify(permissionsController.approvePermissionsRequest, permissionsController),
clearPermissions: permissionsController.clearPermissions.bind(permissionsController),
getApprovedAccounts: nodeify(permissionsController.getAccounts.bind(permissionsController)),
rejectPermissionsRequest: nodeify(permissionsController.rejectPermissionsRequest, permissionsController),
removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController),
updateExposedAccounts: nodeify(permissionsController.updateExposedAccounts, permissionsController),
legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController),
getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()),
getOpenMetamaskTabsIds: (cb) => cb(null, this.getOpenMetamaskTabsIds()),
}
}
//=============================================================================
// VAULT / KEYRING RELATED METHODS
//=============================================================================
@ -1351,10 +1343,10 @@ module.exports = class MetamaskController extends EventEmitter {
// setup multiplexing
const mux = setupMultiplex(connectionStream)
// connect features
const publicApi = this.setupPublicApi(mux.createStream('publicApi'))
this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId, publicApi)
this.setupPublicConfig(mux.createStream('publicConfig'), senderUrl)
// messages between inpage and background
this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId)
this.setupPublicConfig(mux.createStream('publicConfig'))
}
/**
@ -1432,13 +1424,15 @@ module.exports = class MetamaskController extends EventEmitter {
* resource is an extension.
* @param {object} publicApi - The public API
*/
setupProviderConnection (outStream, senderUrl, extensionId, publicApi) {
const getSiteMetadata = publicApi && publicApi.getSiteMetadata
const engine = this.setupProviderEngine(senderUrl, extensionId, getSiteMetadata)
setupProviderConnection (outStream, senderUrl, extensionId) {
const origin = senderUrl.hostname
const engine = this.setupProviderEngine(senderUrl, extensionId)
// setup connection
const providerStream = createEngineStream({ engine })
const connectionId = this.addConnection(origin, { engine })
pump(
outStream,
providerStream,
@ -1450,6 +1444,7 @@ module.exports = class MetamaskController extends EventEmitter {
mid.destroy()
}
})
connectionId && this.removeConnection(origin, connectionId)
if (err) {
log.error(err)
}
@ -1460,7 +1455,8 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A method for creating a provider that is safely restricted for the requesting domain.
**/
setupProviderEngine (senderUrl, extensionId, getSiteMetadata) {
setupProviderEngine (senderUrl, extensionId) {
const origin = senderUrl.hostname
// setup json rpc engine stack
const engine = new RpcEngine()
@ -1474,20 +1470,17 @@ module.exports = class MetamaskController extends EventEmitter {
const subscriptionManager = createSubscriptionManager({ provider, blockTracker })
subscriptionManager.events.on('notification', (message) => engine.emit('notification', message))
// metadata
// append origin to each request
engine.push(createOriginMiddleware({ origin }))
// logging
engine.push(createLoggerMiddleware({ origin }))
// filter and subscription polyfills
engine.push(filterMiddleware)
engine.push(subscriptionManager.middleware)
// permissions
engine.push(this.permissionsController.createMiddleware({ origin, extensionId }))
// watch asset
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
// requestAccounts
engine.push(this.providerApprovalController.createMiddleware({
senderUrl,
extensionId,
getSiteMetadata,
}))
// forward to metamask primary provider
engine.push(providerAsMiddleware(provider))
return engine
@ -1502,13 +1495,9 @@ module.exports = class MetamaskController extends EventEmitter {
* this is a good candidate for deprecation.
*
* @param {*} outStream - The stream to provide public config over.
* @param {URL} senderUrl - The URL of requesting resource
*/
setupPublicConfig (outStream, senderUrl) {
const configStore = this.createPublicConfigStore({
// check the providerApprovalController's approvedOrigins
checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(senderUrl.hostname),
})
setupPublicConfig (outStream) {
const configStore = this.createPublicConfigStore()
const configStream = asStream(configStore)
pump(
@ -1524,6 +1513,8 @@ module.exports = class MetamaskController extends EventEmitter {
)
}
// manage external connections
onMessage (message, sender, sendResponse) {
if (!message || !message.type) {
log.debug(`Ignoring invalid message: '${JSON.stringify(message)}'`)
@ -1562,39 +1553,98 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* A method for providing our public api over a stream.
* This includes a method for setting site metadata like title and image
* Adds a reference to a connection by origin. Ignores the 'metamask' origin.
* Caller must ensure that the returned id is stored such that the reference
* can be deleted later.
*
* @param {*} outStream - The stream to provide the api over.
* @param {string} origin - The connection's origin string.
* @param {Object} options - Data associated with the connection
* @param {Object} options.engine - The connection's JSON Rpc Engine
* @returns {string} - The connection's id (so that it can be deleted later)
*/
setupPublicApi (outStream) {
const dnode = Dnode()
// connect dnode api to remote connection
pump(
outStream,
dnode,
outStream,
(err) => {
// report any error
if (err) {
log.error(err)
}
}
)
addConnection (origin, { engine }) {
if (origin === 'metamask') {
return null
}
const getRemote = createDnodeRemoteGetter(dnode)
if (!this.connections[origin]) {
this.connections[origin] = {}
}
const publicApi = {
// wrap with an await remote
getSiteMetadata: async () => {
const remote = await getRemote()
return await pify(remote.getSiteMetadata)()
},
const id = nanoid()
this.connections[origin][id] = {
engine,
}
return id
}
/**
* Deletes a reference to a connection, by origin and id.
* Ignores unknown origins.
*
* @param {string} origin - The connection's origin string.
* @param {string} id - The connection's id, as returned from addConnection.
*/
removeConnection (origin, id) {
const connections = this.connections[origin]
if (!connections) {
return
}
delete connections[id]
if (Object.keys(connections.length === 0)) {
delete this.connections[origin]
}
}
/**
* Causes the RPC engines associated with the connections to the given origin
* to emit a notification event with the given payload.
* Does nothing if the extension is locked or the origin is unknown.
*
* @param {string} origin - The connection's origin string.
* @param {any} payload - The event payload.
*/
notifyConnections (origin, payload) {
const { isUnlocked } = this.getState()
const connections = this.connections[origin]
if (!isUnlocked || !connections) {
return
}
return publicApi
Object.values(connections).forEach(conn => {
conn.engine && conn.engine.emit('notification', payload)
})
}
/**
* Causes the RPC engines associated with all connections to emit a
* notification event with the given payload.
* Does nothing if the extension is locked.
*
* @param {any} payload - The event payload.
*/
notifyAllConnections (payload) {
const { isUnlocked } = this.getState()
if (!isUnlocked) {
return
}
Object.values(this.connections).forEach(origin => {
Object.values(origin).forEach(conn => {
conn.engine && conn.engine.emit('notification', payload)
})
})
}
// handlers
/**
* Handle a KeyringController update
* @param {object} state the KC state
@ -1623,6 +1673,8 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
// misc
/**
* A method for emitting the full MetaMask state to all registered listeners.
* @private
@ -1631,6 +1683,10 @@ module.exports = class MetamaskController extends EventEmitter {
this.emit('update', this.getState())
}
//=============================================================================
// MISCELLANEOUS
//=============================================================================
/**
* A method for estimating a good gas price at recent prices.
* Returns the lowest price that would have been included in

@ -24,4 +24,3 @@ module.exports = {
return isApproved && now - createdTime > unacceptableDelay
}),
}

@ -0,0 +1,23 @@
const version = 40
const clone = require('clone')
/**
* Site connections are now managed by the PermissionsController, and the
* ProviderApprovalController is removed. This migration deletes all
* ProviderApprovalController state.
*/
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
versionedData.data = transformState(state)
return versionedData
},
}
function transformState (state) {
delete state.ProviderApprovalController
return state
}

@ -1,7 +1,7 @@
const extension = require('extensionizer')
const {createExplorerLink: explorerLink} = require('etherscan-link')
const {getEnvironmentType} = require('../lib/util')
const { getEnvironmentType, checkForError } = require('../lib/util')
const {ENVIRONMENT_TYPE_BACKGROUND} = require('../lib/enums')
class ExtensionPlatform {
@ -66,6 +66,58 @@ class ExtensionPlatform {
}
}
queryTabs () {
return new Promise((resolve, reject) => {
extension.tabs.query({}, tabs => {
const err = checkForError()
if (err) {
reject(err)
} else {
resolve(tabs)
}
})
})
}
currentTab () {
return new Promise((resolve, reject) => {
extension.tabs.getCurrent(tab => {
const err = checkForError()
if (err) {
reject(err)
} else {
resolve(tab)
}
})
})
}
switchToTab (tabId) {
return new Promise((resolve, reject) => {
extension.tabs.update(tabId, {highlighted: true}, (tab) => {
const err = checkForError()
if (err) {
reject(err)
} else {
resolve(tab)
}
})
})
}
closeTab (tabId) {
return new Promise((resolve, reject) => {
extension.tabs.remove(tabId, () => {
const err = checkForError()
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
_showConfirmedTransaction (txMeta) {
this._subscribeToNotificationClicked()

@ -18,8 +18,8 @@ const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine')
/**
* returns a provider restricted to the requesting domain
**/
function incomingConnection (domain, getSiteMetadata) {
const engine = metamaskController.setupProviderEngine(domain, getSiteMetadata)
function incomingConnection (domain) {
const engine = metamaskController.setupProviderEngine(domain)
const provider = providerFromEngine(engine)
return provider
}
@ -32,15 +32,6 @@ const filterMiddleware = engine._middleware.filter(mid => mid.name === 'filterMi
filterMiddleware.destroy()
```
### getSiteMetadata()
This method is used to enhance our confirmation screens with images and text representing the requesting domain.
It should return a promise that resolves with an object with the following properties:
- `name`: The requesting site's name.
- `icon`: A URI representing the site's logo.
### Using the Streams Interface
Only use this if you intend to construct the [metamask-inpage-provider](https://github.com/MetaMask/metamask-inpage-provider) over a stream!

@ -93,7 +93,7 @@
"eth-block-tracker": "^4.4.2",
"eth-contract-metadata": "^1.11.0",
"eth-ens-namehash": "^2.0.8",
"eth-json-rpc-errors": "^1.1.0",
"eth-json-rpc-errors": "^2.0.0",
"eth-json-rpc-filters": "^4.1.1",
"eth-json-rpc-infura": "^4.0.1",
"eth-json-rpc-middleware": "^4.2.0",
@ -116,6 +116,7 @@
"ethjs-query": "^0.3.4",
"extension-port-stream": "^1.0.0",
"extensionizer": "^1.0.1",
"fast-deep-equal": "^2.0.1",
"fast-json-patch": "^2.0.4",
"fuse.js": "^3.2.0",
"gaba": "^1.9.0",
@ -128,10 +129,11 @@
"lodash.shuffle": "^4.2.0",
"loglevel": "^1.4.1",
"luxon": "^1.8.2",
"metamask-inpage-provider": "^3.0.0",
"metamask-inpage-provider": "^4.0.2",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
"multihashes": "^0.4.12",
"nanoid": "^2.1.6",
"nonce-tracker": "^1.0.0",
"number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
@ -173,7 +175,9 @@
"redux-thunk": "^2.2.0",
"request-promise": "^4.2.1",
"reselect": "^3.0.1",
"rpc-cap": "^1.0.1",
"safe-event-emitter": "^1.0.1",
"safe-json-stringify": "^1.2.0",
"single-call-balance-checker-abi": "^1.0.0",
"string.prototype.matchall": "^3.0.1",
"swappable-obj-proxy": "^1.1.0",

@ -49,6 +49,8 @@ const initialize = () => {
const approveTokensWithoutGas = document.getElementById('approveTokensWithoutGas')
const signTypedData = document.getElementById('signTypedData')
const signTypedDataResults = document.getElementById('signTypedDataResult')
const getAccountsButton = document.getElementById('getAccounts')
const getAccountsResults = document.getElementById('getAccountsResult')
const contractStatus = document.getElementById('contractStatus')
const tokenAddress = document.getElementById('tokenAddress')
@ -317,6 +319,16 @@ const initialize = () => {
})
})
getAccountsButton.addEventListener('click', async () => {
try {
const accounts = await ethereum.send({ method: 'eth_accounts' })
getAccountsResults.innerHTML = accounts[0] || 'Not able to get accounts'
} catch (error) {
console.error(error)
getAccountsResults.innerHTML = `Error: ${error}`
}
})
}
updateButtons()

@ -42,6 +42,11 @@
<button id="approveTokensWithoutGas">Approve Tokens Without Gas</button>
</div>
</section>
<section>
<h2>Get Accounts</h2>
<button id="getAccounts">eth_accounts</button>
<div id="getAccountsResult"></div>
</section>
<section>
<h2>Status</h2>
<div>

@ -113,7 +113,8 @@ describe('MetaMask', function () {
let extension
let popup
let dapp
it('switches to a dapp', async () => {
it('connects to the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
@ -126,19 +127,27 @@ describe('MetaMask', function () {
const windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
await driver.switchTo().window(popup)
await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click()
const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
await accountButton.click()
const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
await submitButton.click()
await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
})
it('has the ganache network id within the dapp', async () => {
const networkDiv = await findElement(driver, By.css('#network'))
await delay(regularDelayMs)
assert.equal(await networkDiv.getText(), '5777')
})

@ -134,7 +134,7 @@ describe('MetaMask', function () {
it('show account details dropdown menu', async () => {
await driver.findElement(By.css('div.menu-bar__open-in-browser')).click()
const options = await driver.findElements(By.css('div.menu.account-details-dropdown div.menu__item'))
assert.equal(options.length, 3) // HD Wallet type does not have to show the Remove Account option
assert.equal(options.length, 4) // HD Wallet type does not have to show the Remove Account option
await delay(regularDelayMs)
})
})

@ -432,7 +432,7 @@ describe('MetaMask', function () {
await delay(largeDelayMs)
})
it('starts a send transaction inside the dapp', async () => {
it('connects the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
@ -445,15 +445,22 @@ describe('MetaMask', function () {
windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
await driver.switchTo().window(popup)
await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click()
const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
await accountButton.click()
const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
await submitButton.click()
await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(dapp)
await delay(2000)
await delay(regularDelayMs)
})
it('initiates a send from the dapp', async () => {

@ -0,0 +1,201 @@
const assert = require('assert')
const webdriver = require('selenium-webdriver')
const { By, until } = webdriver
const {
delay,
} = require('./func')
const {
checkBrowserForConsoleErrors,
findElement,
findElements,
openNewPage,
verboseReportOnFailure,
waitUntilXWindowHandles,
switchToWindowWithTitle,
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let driver
let publicAddress
const tinyDelayMs = 200
const regularDelayMs = tinyDelayMs * 2
const largeDelayMs = regularDelayMs * 2
this.timeout(0)
this.bail(true)
before(async function () {
const result = await prepareExtensionForTesting()
driver = result.driver
await setupFetchMocking(driver)
})
afterEach(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await checkBrowserForConsoleErrors(driver)
if (errors.length) {
const errorReports = errors.map(err => err.message)
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
console.error(new Error(errorMessage))
}
}
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(driver, this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('Going through the first time flow, but skipping the seed phrase challenge', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
it('clicks the "Create New Wallet" option', async () => {
const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Create a Wallet')]`))
customRpcButton.click()
await delay(largeDelayMs)
})
it('clicks the "No thanks" option on the metametrics opt-in screen', async () => {
const optOutButton = await findElement(driver, By.css('.btn-default'))
optOutButton.click()
await delay(largeDelayMs)
})
it('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
const button = await findElement(driver, By.css('.first-time-flow__form button'))
await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple')
const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox'))
await tosCheckBox.click()
await button.click()
await delay(largeDelayMs)
})
it('skips the seed phrase challenge', async () => {
const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
await button.click()
await delay(regularDelayMs)
const detailsButton = await findElement(driver, By.css('.account-details__details-button'))
await detailsButton.click()
await delay(regularDelayMs)
})
it('gets the current accounts address', async () => {
const addressInput = await findElement(driver, By.css('.qr-ellip-address'))
publicAddress = await addressInput.getAttribute('value')
const accountModal = await driver.findElement(By.css('span .modal'))
await driver.executeScript("document.querySelector('.account-modal-close').click()")
await driver.wait(until.stalenessOf(accountModal))
await delay(regularDelayMs)
})
})
describe('sets permissions', () => {
let extension
let popup
let dapp
it('connects to the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await connectButton.click()
await waitUntilXWindowHandles(driver, 3)
const windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
await driver.switchTo().window(popup)
await delay(regularDelayMs)
const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
await accountButton.click()
const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
await submitButton.click()
await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
})
it('shows connected sites', async () => {
const connectedSites = await findElement(driver, By.xpath(`//button[contains(text(), 'Connected Sites')]`))
await connectedSites.click()
await findElement(driver, By.css('.connected-sites__title'))
const domains = await findElements(driver, By.css('.connected-sites-list__domain'))
assert.equal(domains.length, 1)
const domainName = await findElement(driver, By.css('.connected-sites-list__domain-name'))
assert.equal(await domainName.getText(), 'E2E Test Dapp')
await domains[0].click()
const permissionDescription = await findElement(driver, By.css('.connected-sites-list__permission-description'))
assert.equal(await permissionDescription.getText(), 'View the address of the selected account')
})
it('can get accounts within the dapp', async () => {
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const getAccountsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'eth_accounts')]`))
await getAccountsButton.click()
const getAccountsResult = await findElement(driver, By.css('#getAccountsResult'))
assert.equal((await getAccountsResult.getText()).toLowerCase(), publicAddress.toLowerCase())
})
it('can disconnect all accounts', async () => {
await driver.switchTo().window(extension)
const disconnectAllButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Disconnect All')]`))
await disconnectAllButton.click()
const disconnectModal = await driver.findElement(By.css('span .modal'))
const disconnectAllModalButton = await findElement(driver, By.css('.disconnect-all-modal .btn-danger'))
await disconnectAllModalButton.click()
await driver.wait(until.stalenessOf(disconnectModal))
await delay(regularDelayMs)
})
it('can no longer get accounts within the dapp', async () => {
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const getAccountsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'eth_accounts')]`))
await getAccountsButton.click()
const getAccountsResult = await findElement(driver, By.css('#getAccountsResult'))
assert.equal(await getAccountsResult.getText(), 'Not able to get accounts')
})
})
})

@ -60,6 +60,14 @@ concurrently --kill-others \
'yarn dapp' \
'sleep 5 && mocha test/e2e/ethereum-on.spec'
concurrently --kill-others \
--names 'ganache,dapp,e2e' \
--prefix '[{time}][{name}]' \
--success first \
'yarn ganache:start' \
'yarn dapp' \
'sleep 5 && mocha test/e2e/permissions.spec'
export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
concurrently --kill-others \
--names 'ganache,sendwithprivatedapp,e2e' \

@ -119,12 +119,13 @@ describe('MetaMask', function () {
})
})
describe('provider listening for events', () => {
describe('successfuly signs typed data', () => {
let extension
let popup
let dapp
let windowHandles
it('switches to a dapp', async () => {
it('connects to the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
@ -134,18 +135,24 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 3)
windowHandles = await driver.getAllWindowHandles()
const windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
await driver.switchTo().window(popup)
await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click()
const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
await accountButton.click()
const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
await submitButton.click()
await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
})
it('creates a sign typed data signature request', async () => {
@ -153,6 +160,7 @@ describe('MetaMask', function () {
await signTypedMessage.click()
await delay(largeDelayMs)
await delay(regularDelayMs)
windowHandles = await driver.getAllWindowHandles()
await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
await delay(regularDelayMs)

@ -1,6 +1,7 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
const { addInternalMethodPrefix } = require('../../../../app/scripts/controllers/permissions')
const sinon = require('sinon')
describe('preferences controller', function () {
@ -375,7 +376,7 @@ describe('preferences controller', function () {
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubEnd)
sandbox.assert.notCalled(stubNext)
req.method = 'wallet_watchAsset'
req.method = addInternalMethodPrefix('watchAsset')
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.calledTwice(stubEnd)

@ -1,330 +0,0 @@
const assert = require('assert')
const sinon = require('sinon')
const ProviderApprovalController = require('../../../../app/scripts/controllers/provider-approval')
const mockLockedKeyringController = {
memStore: {
getState: () => ({
isUnlocked: false,
}),
},
}
const mockUnlockedKeyringController = {
memStore: {
getState: () => ({
isUnlocked: true,
}),
},
}
describe('ProviderApprovalController', () => {
describe('#_handleProviderRequest', () => {
it('should add a pending provider request when unlocked', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: [metadata],
})
})
it('should add a pending provider request when locked', () => {
const controller = new ProviderApprovalController({
keyringController: mockLockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: [metadata],
})
})
it('should add a 2nd pending provider request when unlocked', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = [{
hostname: 'https://example1.com',
origin: 'example1.com',
siteTitle: 'Example 1',
siteImage: 'https://example1.com/logo.svg',
}, {
hostname: 'https://example2.com',
origin: 'example2.com',
siteTitle: 'Example 2',
siteImage: 'https://example2.com/logo.svg',
}]
controller._handleProviderRequest(metadata[0])
controller._handleProviderRequest(metadata[1])
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: metadata,
})
})
it('should add a 2nd pending provider request when locked', () => {
const controller = new ProviderApprovalController({
keyringController: mockLockedKeyringController,
})
const metadata = [{
hostname: 'https://example1.com',
origin: 'example1.com',
siteTitle: 'Example 1',
siteImage: 'https://example1.com/logo.svg',
}, {
hostname: 'https://example2.com',
origin: 'example2.com',
siteTitle: 'Example 2',
siteImage: 'https://example2.com/logo.svg',
}]
controller._handleProviderRequest(metadata[0])
controller._handleProviderRequest(metadata[1])
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: metadata,
})
})
it('should call openPopup when unlocked and when given', () => {
const openPopup = sinon.spy()
const controller = new ProviderApprovalController({
openPopup,
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.ok(openPopup.calledOnce)
})
it('should call openPopup when locked and when given', () => {
const openPopup = sinon.spy()
const controller = new ProviderApprovalController({
openPopup,
keyringController: mockLockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.ok(openPopup.calledOnce)
})
it('should NOT call openPopup when unlocked and when the domain has already been approved', () => {
const openPopup = sinon.spy()
const controller = new ProviderApprovalController({
openPopup,
keyringController: mockUnlockedKeyringController,
})
controller.store.updateState({
approvedOrigins: {
'example.com': {
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
},
},
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.ok(openPopup.notCalled)
})
})
describe('#approveProviderRequestByOrigin', () => {
it('should mark the origin as approved and remove the provider request', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {
'example.com': {
hostname: 'https://example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
},
},
})
})
it('should mark the origin as approved and multiple requests for the same domain', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {
'example.com': {
hostname: 'https://example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
},
},
})
})
it('should mark the origin as approved without a provider request', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
controller.approveProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {
'example.com': {
hostname: null,
siteTitle: null,
siteImage: null,
},
},
})
})
})
describe('#rejectProviderRequestByOrigin', () => {
it('should remove the origin from approved', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
controller.rejectProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {},
})
})
it('should reject the origin even without a pending request', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
controller.rejectProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {},
})
})
})
describe('#clearApprovedOrigins', () => {
it('should clear the approved origins', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
controller.clearApprovedOrigins()
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {},
})
})
})
describe('#shouldExposeAccounts', () => {
it('should return true for an approved origin', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.ok(controller.shouldExposeAccounts('example.com'))
})
it('should return false for an origin not yet approved', () => {
const controller = new ProviderApprovalController({
keyringController: mockUnlockedKeyringController,
})
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.ok(!controller.shouldExposeAccounts('bad.website'))
})
})
})

@ -48,6 +48,7 @@ describe('Transaction Controller', function () {
ethTx.sign(fromAccount.key)
resolve()
}),
getPermittedAccounts: () => {},
})
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
})
@ -176,13 +177,15 @@ describe('Transaction Controller', function () {
describe('#addUnapprovedTransaction', function () {
const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d'
let getSelectedAddress
let getSelectedAddress, getPermittedAccounts
beforeEach(function () {
getSelectedAddress = sinon.stub(txController, 'getSelectedAddress').returns(selectedAddress)
getPermittedAccounts = sinon.stub(txController, 'getPermittedAccounts').returns([selectedAddress])
})
afterEach(function () {
getSelectedAddress.restore()
getPermittedAccounts.restore()
})
it('should add an unapproved transaction and return a valid txMeta', function (done) {

@ -19,9 +19,11 @@ export default class AccountDetails extends Component {
static propTypes = {
hideSidebar: PropTypes.func,
showAccountDetailModal: PropTypes.func,
showConnectedSites: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
checksummedAddress: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
history: PropTypes.object.isRequired,
}
state = {
@ -48,6 +50,7 @@ export default class AccountDetails extends Component {
const {
hideSidebar,
showAccountDetailModal,
showConnectedSites,
label,
checksummedAddress,
name,
@ -65,14 +68,19 @@ export default class AccountDetails extends Component {
<div className="account-details__keyring-label allcaps">
{label}
</div>
<div className="flex-column flex-center account-details__name-container" onClick={showAccountDetailModal}>
<Identicon diameter={54} address={checksummedAddress} />
<div className="flex-column flex-center account-details__name-container">
<Identicon diameter={54} address={checksummedAddress} onClick={showAccountDetailModal} />
<span className="account-details__account-name">
{name}
</span>
<button className="btn-secondary account-details__details-button">
{t('details')}
</button>
<div className="account-details__details-buttons">
<button className="btn-secondary account-details__details-button" onClick={showAccountDetailModal} >
{t('details')}
</button>
<button className="btn-secondary account-details__details-button" onClick={showConnectedSites}>
{t('connectedSites')}
</button>
</div>
</div>
</div>
<Tooltip

@ -1,4 +1,7 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withRouter } from 'react-router-dom'
import PropTypes from 'prop-types'
import { hideSidebar, showModal } from '../../../store/actions'
import AccountDetails from './account-details.component'
@ -11,4 +14,16 @@ function mapDispatchToProps (dispatch) {
}
}
export default connect(null, mapDispatchToProps)(AccountDetails)
const AccountDetailsContainer = compose(
withRouter,
connect(null, mapDispatchToProps)
)(AccountDetails)
AccountDetailsContainer.propTypes = {
label: PropTypes.string.isRequired,
checksummedAddress: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
showConnectedSites: PropTypes.func.isRequired,
}
export default AccountDetailsContainer

@ -31,6 +31,12 @@
text-align: center;
}
&__details-buttons {
display: flex;
justify-content: space-between;
width: 175px;
}
&__details-button {
font-size: 10px;
border-radius: 17px;

@ -6,6 +6,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import Tooltip from '../../ui/tooltip'
import Identicon from '../../ui/identicon'
import IconWithFallBack from '../../ui/icon-with-fallback'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { PRIMARY } from '../../../helpers/constants/common'
import {
@ -35,6 +36,8 @@ export default class AccountMenu extends PureComponent {
showAccountDetail: PropTypes.func,
showRemoveAccountConfirmationModal: PropTypes.func,
toggleAccountMenu: PropTypes.func,
addressConnectedDomainMap: PropTypes.object,
originOfCurrentTab: PropTypes.string,
}
state = {
@ -57,6 +60,8 @@ export default class AccountMenu extends PureComponent {
selectedAddress,
keyrings,
showAccountDetail,
addressConnectedDomainMap,
originOfCurrentTab,
} = this.props
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
@ -71,6 +76,8 @@ export default class AccountMenu extends PureComponent {
const keyring = keyrings.find(kr => {
return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address)
})
const addressDomains = addressConnectedDomainMap[identity.address] || {}
const iconAndNameForOpenDomain = addressDomains[originOfCurrentTab]
return (
<div
@ -104,6 +111,14 @@ export default class AccountMenu extends PureComponent {
type={PRIMARY}
/>
</div>
{ iconAndNameForOpenDomain
? (
<div className="account-menu__icon-list">
<IconWithFallBack icon={iconAndNameForOpenDomain.icon} name={iconAndNameForOpenDomain.name} />
</div>
)
: null
}
{ this.renderKeyringType(keyring) }
{ this.renderRemoveAccount(keyring, identity) }
</div>

@ -11,7 +11,12 @@ import {
showInfoPage,
showModal,
} from '../../../store/actions'
import { getMetaMaskAccounts } from '../../../selectors/selectors'
import {
getAddressConnectedDomainMap,
getMetaMaskAccounts,
getOriginOfCurrentTab,
} from '../../../selectors/selectors'
import AccountMenu from './account-menu.component'
function mapStateToProps (state) {
@ -23,6 +28,8 @@ function mapStateToProps (state) {
keyrings,
identities,
accounts: getMetaMaskAccounts(state),
addressConnectedDomainMap: getAddressConnectedDomainMap(state),
originOfCurrentTab: getOriginOfCurrentTab(state),
}
}

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

Loading…
Cancel
Save