merging upstream branch

feature/default_network_editable
Vimal 6 years ago
commit 6bb92a8672
  1. 32
      .circleci/config.yml
  2. 53
      CHANGELOG.md
  3. 2
      README.md
  4. 45
      app/_locales/cs/messages.json
  5. 45
      app/_locales/de/messages.json
  6. 95
      app/_locales/en/messages.json
  7. 59
      app/_locales/es/messages.json
  8. 39
      app/_locales/fr/messages.json
  9. 45
      app/_locales/hn/messages.json
  10. 2602
      app/_locales/ht/messages.json
  11. 41
      app/_locales/index.json
  12. 524
      app/_locales/it/messages.json
  13. 45
      app/_locales/ja/messages.json
  14. 131
      app/_locales/ko/messages.json
  15. 47
      app/_locales/nl/messages.json
  16. 45
      app/_locales/ph/messages.json
  17. 1213
      app/_locales/pl/messages.json
  18. 45
      app/_locales/pt/messages.json
  19. 45
      app/_locales/ru/messages.json
  20. 654
      app/_locales/sl/messages.json
  21. 45
      app/_locales/th/messages.json
  22. 45
      app/_locales/tml/messages.json
  23. 45
      app/_locales/tr/messages.json
  24. 45
      app/_locales/vi/messages.json
  25. 45
      app/_locales/zh_CN/messages.json
  26. 45
      app/_locales/zh_TW/messages.json
  27. 1
      app/home.html
  28. 14
      app/images/eth.svg
  29. 7
      app/images/mm-secure.svg
  30. 20
      app/images/provider-approval-check.svg
  31. 12
      app/loading.html
  32. 4
      app/manifest.json
  33. 1
      app/notification.html
  34. 1
      app/popup.html
  35. 30
      app/scripts/background.js
  36. 151
      app/scripts/contentscript.js
  37. 25
      app/scripts/controllers/blacklist.js
  38. 80
      app/scripts/controllers/currency.js
  39. 33
      app/scripts/controllers/network/createInfuraClient.js
  40. 2
      app/scripts/controllers/network/createMetamaskMiddleware.js
  41. 60
      app/scripts/controllers/network/network.js
  42. 106
      app/scripts/controllers/preferences.js
  43. 158
      app/scripts/controllers/provider-approval.js
  44. 11
      app/scripts/controllers/token-rates.js
  45. 35
      app/scripts/controllers/transactions/index.js
  46. 35
      app/scripts/controllers/transactions/tx-gas-utils.js
  47. 5
      app/scripts/controllers/transactions/tx-state-manager.js
  48. 142
      app/scripts/inpage.js
  49. 0
      app/scripts/lib/ens-ipfs/contracts/registrar.js
  50. 0
      app/scripts/lib/ens-ipfs/contracts/resolver.js
  51. 54
      app/scripts/lib/ens-ipfs/resolver.js
  52. 63
      app/scripts/lib/ens-ipfs/setup.js
  53. 46
      app/scripts/lib/ipfsContent.js
  54. 6
      app/scripts/lib/reportFailedTxToSentry.js
  55. 71
      app/scripts/lib/resolver.js
  56. 36
      app/scripts/lib/setupFetchDebugging.js
  57. 75
      app/scripts/lib/setupSentry.js
  58. 88
      app/scripts/metamask-controller.js
  59. 12
      app/scripts/platforms/extension.js
  60. 14
      app/scripts/ui.js
  61. 5
      development/states/add-token.json
  62. 5
      development/states/confirm-sig-requests.json
  63. 5
      development/states/currency-localization.json
  64. 5
      development/states/first-time.json
  65. 5
      development/states/send-new-ui.json
  66. 5
      development/states/tx-list-items.json
  67. 2
      development/verify-locale-strings.js
  68. 50
      gulpfile.js
  69. 12
      old-ui/app/app.js
  70. 15
      old-ui/app/components/app-bar.js
  71. 11
      old-ui/app/components/balance.js
  72. 12
      old-ui/app/components/eth-balance.js
  73. 16
      old-ui/app/components/pending-tx.js
  74. 132
      old-ui/app/config.js
  75. 64
      old-ui/app/provider-approval.js
  76. 8
      old-ui/app/util.js
  77. 15290
      package-lock.json
  78. 12
      package.json
  79. 4
      test/data/mock-state.json
  80. 199
      test/e2e/beta/contract-test/contract.js
  81. 25
      test/e2e/beta/drizzle.spec.js
  82. 4
      test/e2e/beta/from-import-beta-ui.spec.js
  83. 17
      test/e2e/beta/helpers.js
  84. 360
      test/e2e/beta/metamask-beta-responsive-ui.spec.js
  85. 88
      test/e2e/beta/metamask-beta-ui.spec.js
  86. 1
      test/e2e/beta/run-all.sh
  87. 2
      test/e2e/beta/run-drizzle.sh
  88. 22
      test/e2e/func.js
  89. 140
      test/integration/lib/add-token.js
  90. 2
      test/integration/lib/currency-localization.js
  91. 2
      test/integration/lib/mascara-first-time.js
  92. 20
      test/integration/lib/send-new-ui.js
  93. 2
      test/unit/app/controllers/network-contoller-test.js
  94. 54
      test/unit/app/controllers/preferences-controller-test.js
  95. 44
      test/unit/components/balance-component-test.js
  96. 2
      test/unit/ui/app/actions.spec.js
  97. 998
      test/unit/ui/app/reducers/app.spec.js
  98. 576
      test/unit/ui/app/reducers/metamask.spec.js
  99. 63
      ui/app/actions.js
  100. 8
      ui/app/app.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -47,14 +47,14 @@ workflows:
requires:
- prep-deps-npm
- prep-build
- test-integration-mascara-chrome:
requires:
- prep-deps-npm
- prep-scss
- test-integration-mascara-firefox:
requires:
- prep-deps-npm
- prep-scss
# - test-integration-mascara-chrome:
# requires:
# - prep-deps-npm
# - prep-scss
# - test-integration-mascara-firefox:
# requires:
# - prep-deps-npm
# - prep-scss
- test-integration-flat-chrome:
requires:
- prep-deps-npm
@ -73,8 +73,8 @@ workflows:
- test-e2e-beta-chrome
- test-e2e-beta-firefox
- test-e2e-beta-drizzle
- test-integration-mascara-chrome
- test-integration-mascara-firefox
# - test-integration-mascara-chrome
# - test-integration-mascara-firefox
- test-integration-flat-chrome
- test-integration-flat-firefox
- job-screens:
@ -293,9 +293,9 @@ jobs:
- checkout
- attach_workspace:
at: .
- store_artifacts:
path: dist/mascara
destination: builds/mascara
# - store_artifacts:
# path: dist/mascara
# destination: builds/mascara
- store_artifacts:
path: dist/sourcemaps
destination: builds/sourcemaps
@ -322,9 +322,9 @@ jobs:
- run:
name: github gh-pages docs publish
command: >
git config user.name metamaskbot &&
git config user.email admin@metamask.io &&
gh-pages -d docs/jsdocs
git config --global user.name "metamaskbot" &&
git config --global user.email "admin@metamask.io" &&
npm run publish-docs
test-unit:
docker:

@ -2,19 +2,66 @@
## Current Develop Branch
## 5.0.2 Friday November 9 2018
- Fixed bug that caused accounts to update slowly to sites. #5717
- Fixed bug that could lead to some sites crashing. #5709
## 5.0.1 Wednesday November 7 2018
- Fixed bug in privacy mode that made it not work correctly on Firefox.
## 5.0.0 Tuesday November 6 2018
- Implements EIP 1102 as a user-activated "Privacy Mode".
## 4.17.1 Saturday November 3 2018
- Revert chain ID lookup change which introduced a bug which caused problems when connecting to mainnet via Infura's RESTful API.
## 4.17.0 Thursday November 1 2018
- Fix bug where data lookups like balances would get stale data (stopped block-tracker bug)
- Transaction Details now show entry for onchain failure
- [#5559](https://github.com/MetaMask/metamask-extension/pull/5559) Localize language names in translation select list
- [#5283](https://github.com/MetaMask/metamask-extension/pull/5283): Fix bug when eth.getCode() called with no contract
- [#5563](https://github.com/MetaMask/metamask-extension/pull/5563#pullrequestreview-166769174) Feature: improve Hatian Creole translations
- Feature: improve Slovenian translations
- Add support for alternate `wallet_watchAsset` rpc method name
- Attempt chain ID lookup via `eth_chainId` before `net_version`
- Fix account display width for large currency values
## 4.16.0 Wednesday October 17 2018
- Feature: Add toggle for primary currency (eth/fiat)
- Feature: add tooltip for view etherscan tx
- Feature: add Polish translations
- Feature: improve Korean translations
- Feature: improve Italian translations
- Bug Fix: Fix bug with "pending" block reference
- Bug Fix: Force AccountTracker to update balances on network change
- Bug Fix: Fix document extension check when injecting web3
- Bug Fix: Fix some support links
## 4.15.0 Thursday October 11 2018
- A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.14.0` is found to have bugs.
## 4.14.0 Thursday October 11 2018
- Update transaction statuses when switching networks.
- [#5470](https://github.com/MetaMask/metamask-extension/pull/5470) 100% coverage in French locale, fixed the procedure to verify proposed locale.
- Added rudimentary support for the subscription API to support web3 1.0 and Truffle's Drizzle.
- [#5502](https://github.com/MetaMask/metamask-extension/pull/5502) Update Italian translation.
## 4.12.0 Thursday September 27 2018
- Reintroduces changes from 4.10.0
## 4.13.0
- A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.12.0` is found to have bugs.
## 4.12.0 Thursday September 27 2018
- Reintroduces changes from 4.10.0
## 4.11.1 Tuesday September 25 2018
- Adds Ledger support.

@ -1,5 +1,5 @@
# MetaMask Browser Extension
[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension)
[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension)
## Support

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Režim súkromia"
},
"privacyModeDescription": {
"message": "Webové stránky musia požiadať o prístup k zobrazeniu informácií o vašom účte."
},
"exposeAccounts": {
"message": "Vystavte účty"
},
"exposeDescription": {
"message": "Vystavte účty na aktuální webové stránky. Užitečné pro starší dappy."
},
"confirmExpose": {
"message": "Opravdu chcete své účty vystavit na stávajícím webu?"
},
"confirmClear": {
"message": "Naozaj chcete vymazať schválené webové stránky?"
},
"clearApprovalDataSuccess": {
"message": "Schválené údaje webových stránek byly úspěšně zrušeny."
},
"approvalData": {
"message": "Údaje o schválení"
},
"approvalDataDescription": {
"message": "Vymažte schválené údaje webových stránek, aby všechny weby znovu požádaly o schválení."
},
"clearApprovalData": {
"message": "Jasné údaje o schválení"
},
"approve": {
"message": "Schválit"
},
"reject": {
"message": "Odmítnout"
},
"providerAPIRequest": {
"message": "Požadavek API Ethereum"
},
"reviewProviderRequest": {
"message": "Přečtěte si prosím tuto žádost API Ethereum."
},
"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ě."
},
"accept": {
"message": "Přijmout"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Datenschutzmodus"
},
"privacyModeDescription": {
"message": "Websites müssen Zugriff anfordern, um Ihre Kontoinformationen anzuzeigen."
},
"exposeAccounts": {
"message": "Expose Konten"
},
"exposeDescription": {
"message": "Expose Konten auf der aktuellen Website. Nützlich für ältere Dapps."
},
"confirmExpose": {
"message": "Möchten Sie Ihre Konten wirklich der aktuellen Website zugänglich machen?"
},
"confirmClear": {
"message": "Möchten Sie die genehmigten Websites wirklich löschen?"
},
"clearApprovalDataSuccess": {
"message": "Genehmigte Website-Daten wurden erfolgreich gelöscht."
},
"approvalData": {
"message": "Genehmigungsdaten"
},
"approvalDataDescription": {
"message": "Löschen Sie die genehmigten Website-Daten, damit alle Websites erneut eine Genehmigung anfordern müssen."
},
"clearApprovalData": {
"message": "Genehmigungsdaten löschen"
},
"approve": {
"message": "Genehmigen"
},
"reject": {
"message": "Ablehnen"
},
"providerAPIRequest": {
"message": "Web3-API-Anfrage"
},
"reviewProviderRequest": {
"message": "Bitte lesen Sie diese Ethereum-API-Anfrage."
},
"providerRequestInfo": {
"message": "Die unten aufgeführte Domäne versucht, Zugriff auf die Ethereum-API anzufordern, damit sie mit der Ethereum-Blockchain interagieren kann. Überprüfen Sie immer, dass Sie sich auf der richtigen Website befinden, bevor Sie den Web3-Zugriff genehmigen."
},
"accept": {
"message": "Annehmen"
},

@ -1,4 +1,43 @@
{
"privacyMode": {
"message": "Privacy Mode"
},
"privacyModeDescription": {
"message": "Websites must request access to view your account information."
},
"exposeAccounts": {
"message": "Expose Accounts"
},
"exposeDescription": {
"message": "Expose accounts to the current website. Useful for legacy dapps."
},
"confirmExpose": {
"message": "Are you sure you want to expose your accounts to the current website?"
},
"confirmClear": {
"message": "Are you sure you want to clear approved websites?"
},
"clearApprovalDataSuccess": {
"message": "Approved website data cleared successfully."
},
"approvalData": {
"message": "Privacy Data"
},
"approvalDataDescription": {
"message": "Clear privacy data so all websites must request access to view account information again."
},
"clearApprovalData": {
"message": "Clear Privacy Data"
},
"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."
},
"accept": {
"message": "Accept"
},
@ -146,6 +185,9 @@
"clickCopy": {
"message": "Click to Copy"
},
"clickToAdd": {
"message": "Click on $1 to add them to your account"
},
"close": {
"message": "Close"
},
@ -173,6 +215,9 @@
"connect": {
"message": "Connect"
},
"connectRequest": {
"message": "Connect Request"
},
"connecting": {
"message": "Connecting..."
},
@ -370,6 +415,9 @@
"enterPasswordContinue": {
"message": "Enter password to continue"
},
"eth": {
"message": "ETH"
},
"etherscanView": {
"message": "View account on Etherscan"
},
@ -389,7 +437,7 @@
"message": "Failed"
},
"fiat": {
"message": "FIAT",
"message": "Fiat",
"description": "Exchange type"
},
"fileImportFail": {
@ -433,6 +481,9 @@
"gasLimitTooLow": {
"message": "Gas limit must be at least 21000"
},
"gasUsed": {
"message": "Gas Used"
},
"generatingSeed": {
"message": "Generating Seed..."
},
@ -644,6 +695,9 @@
"min": {
"message": "Minimum"
},
"missingYourTokens": {
"message": "Don't see your tokens?"
},
"myAccounts": {
"message": "My Accounts"
},
@ -689,9 +743,27 @@
"newRecipient": {
"message": "New Recipient"
},
"newRPC": {
"newNetwork": {
"message": "New Network"
},
"rpcURL": {
"message": "New RPC URL"
},
"showAdvancedOptions": {
"message": "Show Advanced Options"
},
"hideAdvancedOptions": {
"message": "Hide Advanced Options"
},
"optionalChainId": {
"message": "ChainID (optional)"
},
"optionalSymbol": {
"message": "Symbol (optional)"
},
"optionalNickname": {
"message": "Nickname (optional)"
},
"next": {
"message": "Next"
},
@ -796,6 +868,12 @@
"prev": {
"message": "Prev"
},
"primaryCurrencySetting": {
"message": "Primary Currency"
},
"primaryCurrencySettingDescription": {
"message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency."
},
"privacyMsg": {
"message": "Privacy Policy"
},
@ -804,7 +882,7 @@
"description": "select this type of file to use to import an account"
},
"privateKeyWarning": {
"message": "Warning: Never disclose this key. Anyone with your private keys can take steal any assets held in your account."
"message": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account."
},
"privateNetwork": {
"message": "Private Network"
@ -833,9 +911,6 @@
"refundAddress": {
"message": "Your Refund Address"
},
"reject": {
"message": "Reject"
},
"rejectAll": {
"message": "Reject All"
},
@ -1160,12 +1235,18 @@
"transactionUpdatedGas": {
"message": "Transaction updated with a gas price of $1 on $2."
},
"transactionErrored": {
"message": "Transaction encountered an error."
},
"transactions": {
"message": "transactions"
},
"transactionError": {
"message": "Transaction Error. Exception thrown in contract code."
},
"transactionErrorNoContract": {
"message": "Trying to call a function on a non-contract address."
},
"transactionMemo": {
"message": "Transaction memo (optional)"
},
@ -1195,7 +1276,7 @@
"message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret."
},
"typePassword": {
"message": "Type Your Password"
"message": "Type your MetaMask password"
},
"uiWelcome": {
"message": "Welcome to the New UI (Beta)"

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Modo privado"
},
"privacyModeDescription": {
"message": "Los sitios web deben solicitar acceso para ver la información de su cuenta."
},
"exposeAccounts": {
"message": "Exponer cuentas"
},
"exposeDescription": {
"message": "Exponer cuentas al sitio web actual. Útil para dapps heredados."
},
"confirmExpose": {
"message": "¿Seguro que quieres exponer tus cuentas al sitio web actual?"
},
"confirmClear": {
"message": "¿Seguro que quieres borrar los sitios web aprobados?"
},
"clearApprovalDataSuccess": {
"message": "Los datos aprobados del sitio web se borraron con éxito."
},
"approvalData": {
"message": "Datos de aprobación"
},
"approvalDataDescription": {
"message": "Borre los datos del sitio web aprobado para que todos los sitios deban volver a solicitar la aprobación."
},
"clearApprovalData": {
"message": "Borrar datos de aprobación"
},
"approve": {
"message": "Aprobar"
},
"reject": {
"message": "Rechazar"
},
"providerAPIRequest": {
"message": "Solicitud de API Web3"
},
"reviewProviderRequest": {
"message": "Por favor, revise esta solicitud API Ethereum."
},
"providerRequestInfo": {
"message": "El dominio que se muestra a continuación intenta solicitar acceso a la API Ethereum para que pueda interactuar con la cadena de bloques de Ethereum. Siempre verifique que esté en el sitio correcto antes de aprobar el acceso Ethereum."
},
"accept": {
"message": "Aceptar"
},
@ -165,7 +210,7 @@
"message": " Copiar "
},
"copyPrivateKey": {
"message": "Ésta es tu llave privada (haz click para copiar)"
"message": "Ésta es tu clave privada (haz click para copiar)"
},
"copyToClipboard": {
"message": "Copiar al portapapeles"
@ -287,10 +332,10 @@
"message": "Tipo de cambio"
},
"exportPrivateKey": {
"message": "Exportar llave privada"
"message": "Exportar clave privada"
},
"exportPrivateKeyWarning": {
"message": "Exportar llaves privadas bajo TU PROPIO riesgo"
"message": "Exportar claves privadas bajo TU PROPIO riesgo"
},
"failed": {
"message": "Fallo"
@ -588,8 +633,8 @@
"description": "En el proceso de creación de contraseña, esta no es lo suficientemente larga para ser segura"
},
"pastePrivateKey": {
"message": "Pega tu llave privada aqui",
"description": "Para importar una cuenta desde una llave privada"
"message": "Pega tu clave privada aqui",
"description": "Para importar una cuenta desde una clave privada"
},
"pasteSeed": {
"message": "¡Pega tu frase semilla aquí!"
@ -604,7 +649,7 @@
"message": "Política de privacidad"
},
"privateKey": {
"message": "Llave privada",
"message": "Clave privada",
"description": "Selecciona este tupo de archivo para importar una cuenta"
},
"privateKeyWarning": {
@ -721,7 +766,7 @@
"message": "Comprar con ShapeShift"
},
"showPrivateKeys": {
"message": "Mostrar llaves privadas"
"message": "Mostrar claves privadas"
},
"showQRCode": {
"message": "Mostrar codigo QR"

@ -1,4 +1,43 @@
{
"privacyMode": {
"message": "Les sites Web doivent demander un accès pour afficher les informations de votre compte."
},
"privacyModeDescription": {
"message": "Les sites Web doivent demander un accès pour afficher les informations de votre compte."
},
"exposeAccounts": {
"message": "Exposer les comptes"
},
"exposeDescription": {
"message": "Exposer des comptes sur le site Web actuel. Utile pour les dapps hérités."
},
"confirmExpose": {
"message": "Êtes-vous sûr de vouloir exposer vos comptes au site Web actuel?"
},
"confirmClear": {
"message": "Êtes-vous sûr de vouloir supprimer les sites Web approuvés?"
},
"clearApprovalDataSuccess": {
"message": "Les données de site Web approuvées ont été supprimées."
},
"approvalData": {
"message": "Données d'approbation"
},
"approvalDataDescription": {
"message": "Effacer les données de site Web approuvées afin que tous les sites doivent à nouveau demander l'approbation."
},
"clearApprovalData": {
"message": "Effacer les données d'approbation"
},
"providerAPIRequest": {
"message": "Demande d'API Web3"
},
"reviewProviderRequest": {
"message": "Veuillez consulter cette demande d'API Ethereum."
},
"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."
},
"accept": {
"message": "Accepter"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "गपनयतड"
},
"privacyModeDescription": {
"message": "वबसइट आपकनकखनिए पहच क अनध करन।"
},
"exposeAccounts": {
"message": "ख परश कर"
},
"exposeDescription": {
"message": "मबसइट पर ख परश कर। विसत डस किए उपय।"
},
"confirmExpose": {
"message": "क आप वकई अपन वरतमन वबसइट पर बनकब करनहत?"
},
"confirmClear": {
"message": "क आप वकई अनित वबसइट करनहत?"
},
"clearApprovalDataSuccess": {
"message": "सत वबसइट ड सफलतवक म।"
},
"approvalData": {
"message": "सि"
},
"approvalDataDescription": {
"message": "अनित वबसइट ड करि सभइटिर स अनदन क अनध करन।"
},
"clearApprovalData": {
"message": "अनदन ड कर"
},
"approve": {
"message": "मर"
},
"reject": {
"message": "असर"
},
"providerAPIRequest": {
"message": "वब 3 एपआई अनध"
},
"reviewProviderRequest": {
"message": "कपय इस वब 3 एपआई अनध क सम कर।"
},
"providerRequestInfo": {
"message": "नबदध डन वब 3 एपआई तक पहच क अनध करनरयस कर रहि यह एथियम बकचन सतचत कर सक। वब 3 एकस क पहल हम सहच करि आप सहइट पर ह।"
},
"accept": {
"message": "सर कर"
},

File diff suppressed because it is too large Load Diff

@ -1,23 +1,24 @@
[
{ "code": "cs", "name": "Czech" },
{ "code": "de", "name": "German" },
{ "code": "cs", "name": "Čeština" },
{ "code": "de", "name": "Deutsche" },
{ "code": "en", "name": "English" },
{ "code": "es", "name": "Spanish" },
{ "code": "fr", "name": "French" },
{ "code": "ht", "name": "Haitian Creole" },
{ "code": "hn", "name": "Hindi" },
{ "code": "it", "name": "Italian" },
{ "code": "ja", "name": "Japanese" },
{ "code": "ko", "name": "Korean" },
{ "code": "nl", "name": "Dutch" },
{ "code": "ph", "name": "Tagalog" },
{ "code": "pt", "name": "Portuguese" },
{ "code": "ru", "name": "Russian" },
{ "code": "sl", "name": "Slovenian" },
{ "code": "th", "name": "Thai" },
{ "code": "tml", "name": "Tamil" },
{ "code": "tr", "name": "Turkish" },
{ "code": "vi", "name": "Vietnamese" },
{ "code": "zh_CN", "name": "Chinese (Simplified)" },
{ "code": "zh_TW", "name": "Chinese (Traditional)" }
{ "code": "es", "name": "Español" },
{ "code": "fr", "name": "Français" },
{ "code": "ht", "name": "Kreyòl ayisyen" },
{ "code": "hn", "name": "हि" },
{ "code": "it", "name": "Italiano" },
{ "code": "ja", "name": "日本語" },
{ "code": "ko", "name": "한국어" },
{ "code": "nl", "name": "Nederlands" },
{ "code": "ph", "name": "Pilipino" },
{ "code": "pl", "name": "Polskie" },
{ "code": "pt", "name": "Português" },
{ "code": "ru", "name": "Русский" },
{ "code": "sl", "name": "Slovenščina" },
{ "code": "th", "name": "ไทย" },
{ "code": "tml", "name": "தமி" },
{ "code": "tr", "name": "Türkçe" },
{ "code": "vi", "name": "Tiếng Việt" },
{ "code": "zh_CN", "name": "中文(简体)" },
{ "code": "zh_TW", "name": "中文(繁體)" }
]

@ -1,7 +1,52 @@
{
"privacyMode": {
"message": "Modalità di privacy"
},
"privacyModeDescription": {
"message": "I siti Web devono richiedere l'accesso per visualizzare le informazioni del tuo account."
},
"exposeAccounts": {
"message": "Expose Accounts"
},
"exposeDescription": {
"message": "Esporre gli account al sito Web corrente. Utile per dapps legacy."
},
"confirmExpose": {
"message": "Sei sicuro di voler esporre i tuoi account al sito web corrente?"
},
"confirmClear": {
"message": "Sei sicuro di voler cancellare i siti Web approvati?"
},
"clearApprovalDataSuccess": {
"message": "Dati del sito Web approvati cancellati correttamente."
},
"approvalData": {
"message": "Dati di approvazione"
},
"approvalDataDescription": {
"message": "Cancella i dati del sito web approvati, quindi tutti i siti devono richiedere nuovamente l'approvazione."
},
"clearApprovalData": {
"message": "Cancella i dati di approvazione"
},
"reject": {
"message": "Rifiutare"
},
"providerAPIRequest": {
"message": "Richiesta API Web3"
},
"reviewProviderRequest": {
"message": "Si prega di rivedere questa richiesta API Ethereum."
},
"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."
},
"accept": {
"message": "Accetta"
},
"accessingYourCamera": {
"message": "Accesso alla fotocamera..."
},
"account": {
"message": "Account"
},
@ -11,6 +56,15 @@
"accountName": {
"message": "Nome Account"
},
"accountOptions": {
"message": "Account Options"
},
"accountSelectionRequired": {
"message": "Devi selezionare un account!"
},
"activityLog": {
"message": "log attività"
},
"address": {
"message": "Indirizzo"
},
@ -23,6 +77,12 @@
"addTokens": {
"message": "Aggiungi più token"
},
"addSuggestedTokens": {
"message": "Aggiungi Token Suggeriti"
},
"addAcquiredTokens": {
"message": "Aggiungi i token che hai acquistato usando MetaMask"
},
"amount": {
"message": "Importo"
},
@ -37,9 +97,21 @@
"message": "MetaMask",
"description": "Il nome dell'applicazione"
},
"approve": {
"message": "Approva"
},
"approved": {
"message": "Approvato"
},
"attemptingConnect": {
"message": "Tentativo di connessione alla blockchain."
},
"attemptToCancel": {
"message": "Tentativo di Annullamento?"
},
"attemptToCancelDescription": {
"message": "Tentare di annullare non garantisce che la transazione verrà annullata. Se annullata, è comunque richiesto pagare alla rete la commissione sulla transazione."
},
"attributions": {
"message": "Attribuzioni"
},
@ -71,8 +143,11 @@
"borrowDharma": {
"message": "Prendi in presisito con Dharma (Beta)"
},
"browserNotSupported": {
"message": "Il tuo Browser non è supportato..."
},
"builtInCalifornia": {
"message": "MetaMask è progettato e costruito in California."
"message": "MetaMask è progettato e realizzato in California."
},
"buy": {
"message": "Compra"
@ -89,8 +164,23 @@
"buyCoinSwitchExplainer": {
"message": "CoinSwitch è la destinazione one-stop per lo scambio di oltre 300 criptovalute alla migliore tariffa."
},
"bytes": {
"message": "Bytes"
},
"ok": {
"message": "Ok"
},
"cancel": {
"message": "Cancella"
"message": "Annulla"
},
"cancelAttempt": {
"message": "Tentativo di Annullamento"
},
"cancellationGasFee": {
"message": "Commissione di Annullamento in Gas"
},
"cancelN": {
"message": "Cancel all $1 transactions"
},
"classicInterface": {
"message": "Usa l'interfaccia classica"
@ -98,9 +188,18 @@
"clickCopy": {
"message": "Clicca per Copiare"
},
"close": {
"message": "Chiudi"
},
"chromeRequiredForHardwareWallets": {
"message": "Devi usare MetaMask con Google Chrome per connettere il tuo Portafoglio Hardware"
},
"confirm": {
"message": "Conferma"
},
"confirmed": {
"message": "Confermata"
},
"confirmContract": {
"message": "Conferma Contratto"
},
@ -110,6 +209,36 @@
"confirmTransaction": {
"message": "Conferma Transazione"
},
"connectHardwareWallet": {
"message": "Connetti Portafoglio Hardware"
},
"connect": {
"message": "Connetti"
},
"connecting": {
"message": "Connessione..."
},
"connectingToKovan": {
"message": "Connessione alla Rete di test Kovan"
},
"connectingToMainnet": {
"message": "Connessione alla Rete Ethereum Principale"
},
"connectingToRopsten": {
"message": "Connessione alla Rete di test Ropsten"
},
"connectingToRinkeby": {
"message": "Connessione alla Rete di test Rinkeby"
},
"connectingToUnknown": {
"message": "Connessione ad una Rete Sconosciuta"
},
"connectToLedger": {
"message": "Connettersi al Ledger"
},
"connectToTrezor": {
"message": "Connettersi al Trezor"
},
"continue": {
"message": "Continua"
},
@ -140,6 +269,9 @@
"copy": {
"message": "Copia"
},
"copyAddress": {
"message": "Copia l'indirizzo"
},
"copyToClipboard": {
"message": "Copia negli appunti"
},
@ -165,12 +297,21 @@
"currentConversion": {
"message": "Cambio Corrente"
},
"currentLanguage": {
"message": "Lingua Corrente"
},
"currentNetwork": {
"message": "Rete Corrente"
},
"currentRpc": {
"message": "RPC Corrente"
},
"customGas": {
"message": "Personalizza Gas"
},
"customToken": {
"message": "Token Personalizzato"
},
"customize": {
"message": "Personalizza"
},
@ -232,33 +373,54 @@
"done": {
"message": "Finito"
},
"downloadGoogleChrome": {
"message": "Scarica Google Chrome"
},
"downloadStateLogs": {
"message": "Scarica i log di Stato"
},
"dontHaveAHardwareWallet": {
"message": "Non hai un portafoglio hardware?"
},
"dropped": {
"message": "Abbandonata"
},
"edit": {
"message": "Modifica"
},
"editAccountName": {
"message": "Modifica Nome Account"
},
"editingTransaction": {
"message": "Modifica la transazione"
},
"emailUs": {
"message": "Mandaci una mail!"
},
"encryptNewDen": {
"message": "Cripta il tuo nuovo DEN"
},
"ensNameNotFound": {
"message": "Nome ENS non trovato"
},
"enterPassword": {
"message": "Inserisci password"
},
"enterPasswordConfirm": {
"message": "Inserisci la tua password per confermare"
},
"enterPasswordContinue": {
"message": "Inserisci la tua password per continuare"
},
"etherscanView": {
"message": "Vedi account su Etherscan"
},
"exchangeRate": {
"message": "Tasso di cambio"
},
"expandView": {
"message": "Expand View"
},
"exportPrivateKey": {
"message": "Esporta Chiave Privata"
},
@ -266,7 +428,7 @@
"message": "Esporta chiave privata a tuo rischio."
},
"failed": {
"message": "Fallito"
"message": "Fallita"
},
"fiat": {
"message": "FIAT",
@ -279,6 +441,9 @@
"followTwitter": {
"message": "Seguici su Twitter"
},
"forgetDevice": {
"message": "Dimentica questo dispositivo"
},
"from": {
"message": "Da"
},
@ -288,6 +453,9 @@
"fromShapeShift": {
"message": "Da ShapeShift"
},
"functionType": {
"message": "Tipo della Funzione"
},
"gas": {
"message": "Gas",
"description": "Piccola indicazione del costo del gas"
@ -319,6 +487,9 @@
"gasPriceRequired": {
"message": "Prezzo Gas Richiesto"
},
"generatingTransaction": {
"message": "Generando la transazione"
},
"getEther": {
"message": "Ottieni Ether"
},
@ -326,10 +497,28 @@
"message": "Ottieni Get Ether da un faucet per $1",
"description": "Visualizza il nome della rete per il faucet Ether"
},
"getHelp": {
"message": "Aiuto."
},
"greaterThanMin": {
"message": "deve essere maggiore o uguale a $1.",
"description": "aiuto per inserire un input esadecimale come decimale"
},
"hardware": {
"message": "hardware"
},
"hardwareWalletConnected": {
"message": "Portafoglio hardware connesso"
},
"hardwareWallets": {
"message": "Connetti portafoglio hardware"
},
"hardwareWalletsMsg": {
"message": "Selezione un portafoglio hardware che vuoi utilizzare con MetaMask"
},
"havingTroubleConnecting": {
"message": "Problemi di connessione?"
},
"here": {
"message": "qui",
"description": "per intendere -clicca qui- per maggiori informazioni (va con troubleTokenBalances)"
@ -337,6 +526,9 @@
"hereList": {
"message": "Questa è una lista!!!!"
},
"hexData": {
"message": "Dati Hex"
},
"hide": {
"message": "Nascondi"
},
@ -346,6 +538,9 @@
"hideTokenPrompt": {
"message": "Nascondi Token?"
},
"history": {
"message": "Storico"
},
"howToDeposit": {
"message": "Come vuoi depositare Ether?"
},
@ -372,9 +567,18 @@
"message": "Importato",
"description": "stato che conferma che un account è stato totalmente caricato nel portachiavi"
},
"importUsingSeed": {
"message": "Importa account con frase seed"
},
"info": {
"message": "Informazioni"
},
"infoHelp": {
"message": "Informazioni & Aiuto"
},
"initialTransactionConfirmed": {
"message": "La transazione iniziale è stata confermata dalla rete. Clicca OK per tornare indietro."
},
"insufficientFunds": {
"message": "Fondi non sufficienti."
},
@ -399,6 +603,9 @@
"invalidRPC": {
"message": "URI RPC invalido"
},
"invalidSeedPhrase": {
"message": "Frase seed non valida"
},
"jsonFail": {
"message": "Qualcosa è andato storto. Assicurati che il file JSON sia formattato correttamente."
},
@ -406,12 +613,24 @@
"message": "File JSON",
"description": "formato per importare un account"
},
"keepTrackTokens": {
"message": "Tieni traccia dei tokens che hai acquistato con il tuo account MetaMask."
},
"kovan": {
"message": "Rete di test Kovan"
},
"knowledgeDataBase": {
"message": "Visita la nostra Knowledge Base"
},
"max": {
"message": "Massimo"
},
"learnMore": {
"message": "Scopri di più"
},
"ledgerAccountRestriction": {
"message": "E' necessario utilizzare l'ultimo account prima di poterne aggiungere uno nuovo."
},
"lessThanMax": {
"message": "deve essere minore o uguale a $1.",
"description": "aiuto per inserire un input esadecimale come decimale"
@ -419,6 +638,9 @@
"likeToAddTokens": {
"message": "Vorresti aggiungere questi token?"
},
"links": {
"message": "Collegamenti"
},
"limit": {
"message": "Limite"
},
@ -446,17 +668,26 @@
"mainnet": {
"message": "Rete Ethereum Principale"
},
"menu": {
"message": "Menu"
},
"message": {
"message": "Messaggio"
},
"metamaskDescription": {
"message": "MetaMask è una cassaforte sicura per identità su Ethereum."
},
"metamaskSeedWords": {
"message": "Parole Seed di MetaMask"
},
"metamaskVersion": {
"message": "versione di MetaMask"
},
"min": {
"message": "Minimo"
},
"myAccounts": {
"message": "Account Miei"
"message": "Miei Account"
},
"mustSelectOne": {
"message": "Devi selezionare almeno un token."
@ -478,6 +709,9 @@
"networks": {
"message": "Reti"
},
"nevermind": {
"message": "Non importa"
},
"newAccount": {
"message": "Nuovo Account"
},
@ -491,6 +725,9 @@
"newPassword": {
"message": "Nuova Password (minimo 8 caratteri)"
},
"newPassword8Chars": {
"message": "Nuova Password (minimo 8 caratteri)"
},
"newRecipient": {
"message": "Nuovo Destinatario"
},
@ -498,7 +735,7 @@
"message": "Nuovo URL RPC"
},
"next": {
"message": "Prossimo"
"message": "Avanti"
},
"noAddressForName": {
"message": "Nessun indirizzo è stato impostato per questo nome."
@ -506,32 +743,75 @@
"noDeposits": {
"message": "Nessun deposito ricevuto"
},
"noConversionRateAvailable": {
"message": "Tasso di Conversione non Disponibile"
},
"noTransactionHistory": {
"message": "Nessuna cronologia delle transazioni."
},
"noTransactions": {
"message": "Nessuna Transazione"
},
"notFound": {
"message": "Non Trovata"
},
"notStarted": {
"message": "Non Iniziato"
},
"noWebcamFoundTitle": {
"message": "Webcam non trovata"
},
"noWebcamFound": {
"message": "La webcam del tuo computer non è stata trovata. Per favore riprovaci."
},
"oldUI": {
"message": "Vecchia interfaccia"
},
"oldUIMessage": {
"message": "Sei ritornato alla vecchia interfaccia. Puoi ritornare alla nuova interfaccia tramite l'opzione nel menu a discesa in alto a destra."
},
"onlySendToEtherAddress": {
"message": "Invia ETH solamente ad un account Ethereum."
},
"onlySendTokensToAccountAddress": {
"message": "Invia solamente $1 ad un account Ethereum.",
"description": "mostra il simbolo del token"
},
"openInTab": {
"message": "Apri in una scheda"
},
"or": {
"message": "o",
"description": "scelta tra creare o importare un nuovo account"
},
"orderOneHere": {
"message": "Compra un Trezor o un Ledger e tieni i tuoi soldi al sicuro"
},
"origin": {
"message": "Origine"
},
"outgoing": {
"message": "In Uscita"
},
"parameters": {
"message": "Parametri"
},
"password": {
"message": "Password"
},
"passwordCorrect": {
"message": "Assicurati che la password sia corretta."
},
"passwordsDontMatch": {
"message": "Le Password Non Corrispondonos"
},
"passwordMismatch": {
"message": "le password non corrispondono",
"description": "nella creazione della password, le due password all'interno dei campi non corrispondono"
},
"passwordNotLongEnough": {
"message": "Password non abbastanza lunga"
},
"passwordShort": {
"message": "password non sufficientemente lunga",
"description": "nella creazione della password, la password non è lunga abbastanza"
@ -543,12 +823,21 @@
"pasteSeed": {
"message": "Incolla la tua frase seed qui!"
},
"pending": {
"message": "in corso"
},
"personalAddressDetected": {
"message": "Rilevato indirizzo personale. Inserisci l'indirizzo del contratto del token."
},
"pleaseReviewTransaction": {
"message": "Ricontrolla la tua transazione."
},
"popularTokens": {
"message": "Tokens Popolari"
},
"prev": {
"message": "Precedente"
},
"privacyMsg": {
"message": "Politica sulla Privacy"
},
@ -565,6 +854,9 @@
"qrCode": {
"message": "Mostra Codice QR"
},
"queue": {
"message": "Coda"
},
"readdToken": {
"message": "Puoi aggiungere nuovamente questo token in futuro andando in “Aggiungi token” nel menu delle opzioni del tuo account."
},
@ -583,36 +875,84 @@
"refundAddress": {
"message": "Indirizzo di Rimborso"
},
"rejectAll": {
"message": "Reject All"
},
"rejectTxsN": {
"message": "Reject $1 transactions"
},
"rejectTxsDescription": {
"message": "You are about to batch reject $1 transactions."
},
"rejected": {
"message": "Respinta"
},
"reset": {
"message": "Reset"
},
"resetAccount": {
"message": "Resetta Account"
},
"resetAccountDescription": {
"message": "Resettare il tuo account cancellerà lo storico delle transazioni."
},
"restoreFromSeed": {
"message": "Ripristina da una frase seed"
},
"restoreVault": {
"message": "Ripristina Cassaforte"
},
"restoreAccountWithSeed": {
"message": "Ripristina Account con la Frase Seed"
},
"required": {
"message": "Richiesto"
},
"retryWithMoreGas": {
"message": "Riprova con un prezzo del Gas maggiore qui"
},
"restore": {
"message": "Ripristina"
},
"revealSeedWords": {
"message": "Rivela Frase Seed"
},
"revealSeedWordsTitle": {
"message": "Frase Seed"
},
"revealSeedWordsDescription": {
"message": "Se cambierai browser o computer, ti servirà questa frase seed per accedere ai tuoi account. Salvala in un posto sicuro e segreto."
},
"revealSeedWordsWarningTitle": {
"message": "NON CONDIVIDERE questa frase con nessuno!"
},
"revealSeedWordsWarning": {
"message": "Non ripristinare la tua frase seed in pubblico!. Queste parole possono essere usate per rubare il tuo account."
},
"revert": {
"message": "Annulla"
},
"remove": {
"message": "rimuovi"
},
"removeAccount": {
"message": "Rimuovi account"
},
"removeAccountDescription": {
"message": "Questo account sarà rimosso dal tuo portafoglio. Per favore assicurati che hai la frase seed originale o la chiave privata per questo account importato prima di continuare. Puoi nuovamente importare o creare un account dal menù a tendina. "
},
"readyToConnect": {
"message": "Pronto a Connetterti?"
},
"rinkeby": {
"message": "Rete di test Rinkeby"
},
"ropsten": {
"message": "Rete di test Ropsten"
},
"rpc": {
"message": "RPC Personalizzata"
},
"sampleAccountName": {
"message": "Es: Il mio nuovo account",
"description": "Aiuta l'utente a capire il concetto di aggiungere un nome leggibile al loro account"
@ -620,6 +960,9 @@
"save": {
"message": "Salva"
},
"saveAsCsvFile": {
"message": "Salva Come File CSV"
},
"saveAsFile": {
"message": "Salva come File",
"description": "Processo per esportare un account"
@ -627,9 +970,18 @@
"saveSeedAsFile": {
"message": "Salva la Frase Seed come File"
},
"scanInstructions": {
"message": "Posizione il codice QR davanti alla fotocamera"
},
"scanQrCode": {
"message": "Scansiona Codice QR"
},
"search": {
"message": "Cerca"
},
"searchResults": {
"message": "Risultati Ricerca"
},
"secretPhrase": {
"message": "Inserisci la tua frase segreta di dodici parole per ripristinare la cassaforte."
},
@ -642,6 +994,9 @@
"selectCurrency": {
"message": "Seleziona Moneta"
},
"selectLocale": {
"message": "Selezione Lingua"
},
"selectService": {
"message": "Seleziona Servizio"
},
@ -657,6 +1012,33 @@
"sendTokens": {
"message": "Invia Tokens"
},
"sentEther": {
"message": "ether inviati"
},
"sentTokens": {
"message": "tokens inviati"
},
"separateEachWord": {
"message": "Separa ogni parola con un solo spazio"
},
"searchTokens": {
"message": "Cerca Tokens"
},
"selectAnAddress": {
"message": "Seleziona un Indirizzo"
},
"selectAnAccount": {
"message": "Seleziona un Account"
},
"selectAnAccountHelp": {
"message": "Selezione l'account da visualizzare in MetaMask"
},
"selectHdPath": {
"message": "Seleziona Percorso HD"
},
"selectPathHelp": {
"message": "Se non vedi il tuo account Ledger esistente di seguito, prova a cambiare il percorso in \"Legacy (MEW / MyCrypto)\""
},
"sendTokensAnywhere": {
"message": "Invia Tokens a chiunque abbia un account Ethereum"
},
@ -672,9 +1054,21 @@
"showQRCode": {
"message": "Mostra Codie QR"
},
"showHexData": {
"message": "Mostra Dati Hex"
},
"showHexDataDescription": {
"message": "Seleziona per mostrare il campo dei dati hex nella schermata di invio"
},
"sign": {
"message": "Firma"
},
"signatureRequest": {
"message": "Firma Richiesta"
},
"signed": {
"message": "Firmata"
},
"signMessage": {
"message": "Firma Messaggio"
},
@ -690,6 +1084,15 @@
"spaceBetween": {
"message": "ci può essere solo uno spazio tra le parole"
},
"speedUp": {
"message": "velocizza"
},
"speedUpTitle": {
"message": "Velocizza Transazione"
},
"speedUpSubtitle": {
"message": "Aumenta il prezzo del gas per tentare di sovrascrivere e velocizzare la transazione"
},
"status": {
"message": "Stato"
},
@ -699,9 +1102,33 @@
"stateLogsDescription": {
"message": "I log di stato contengono i tuoi indirizzi pubblici e le transazioni effettuate."
},
"stateLogError": {
"message": "Errore nel recupero dei log di stato."
},
"step1HardwareWallet": {
"message": "1. Connetti Portafoglio Hardware"
},
"step1HardwareWalletMsg": {
"message": "Connetti il tuo portafoglio hardware al tuo computer."
},
"step2HardwareWallet": {
"message": "2. Seleziona un Account"
},
"step2HardwareWalletMsg": {
"message": "Selezione l'account che vuoi vedere. Puoi selezionarne solo uno alla volta."
},
"step3HardwareWallet": {
"message": "3. Inizia a usare dApps e molto altro ancora!"
},
"step3HardwareWalletMsg": {
"message": "Usa il tuo account hardware come utilizzeresti qualsiasi account Ethereum. Accedi alle dApps, invia Eth, compra e conserva token ERC20 e token non fungibili come CryptoKitties"
},
"submit": {
"message": "Invia"
},
"submitted": {
"message": "Inviata"
},
"supportCenter": {
"message": "Visita il nostro Centro di Supporto"
},
@ -724,6 +1151,9 @@
"message": "$1 a ETH via ShapeShift",
"description": "il sistema riempirà il tipo di deposito all'inizio del messaggio"
},
"token": {
"message": "Token"
},
"tokenAddress": {
"message": "Indirizzo Token"
},
@ -745,22 +1175,61 @@
"total": {
"message": "Totale"
},
"transaction": {
"message": "transazione"
},
"transactionConfirmed": {
"message": "Transazione confermata il $2."
},
"transactionCreated": {
"message": "Transazione di valore $1 creata il $2."
},
"transactionWithNonce": {
"message": "Transazione $1"
},
"transactionDropped": {
"message": "Transazione abbandonata il $2."
},
"transactionSubmitted": {
"message": "Transazione inviata il $2."
},
"transactionUpdated": {
"message": "Transazione aggiornata il $2."
},
"transactionUpdatedGas": {
"message": "Transazione aggiornata con un prezzo del gas di $1 il $2."
},
"transactions": {
"message": "transazioni"
},
"transactionError": {
"message": "Errore Transazione. Eccceziona generata nel codice del contratto."
},
"transactionMemo": {
"message": "Promemoria Transazione (opzionale)"
},
"transactionNumber": {
"message": "Numero Transazione"
},
"transfer": {
"message": "Trasferisci"
},
"transferFrom": {
"message": "Transfer From"
},
"transfers": {
"message": "Trasferimenti"
},
"trezorHardwareWallet": {
"message": "TREZOR Portafoglio Hardware"
},
"troubleTokenBalances": {
"message": "Abbiamo avuto un problema a caricare il bilancio dei tuoi token. Puoi vederlo ",
"description": "Seguito da un link (qui) per vedere il bilancio dei token"
},
"tryAgain": {
"message": "Prova di nuovo"
},
"twelveWords": {
"message": "Queste 12 parole sono l'unico modo per ripristinare i tuoi account MetaMask. \nSalvale in un posto sicuro e segreto."
},
@ -773,18 +1242,45 @@
"uiWelcomeMessage": {
"message": "Stai utilizzanto la nuova interfaccia di MetaMask. Guarda in giro, prova nuove funzionalità come inviare token, e facci sapere se hai dei problemi."
},
"unapproved": {
"message": "Non approvata"
},
"unavailable": {
"message": "Non Disponibile"
},
"units": {
"message": "unità"
},
"unknown": {
"message": "Sconosciuto"
},
"unknownFunction": {
"message": "Funzione Sconosciuta"
},
"unknownNetwork": {
"message": "Rete Privata Sconosciuta"
},
"unknownNetworkId": {
"message": "ID rete sconosciuto"
},
"unknownQrCode": {
"message": "Errore: Non siamo riusciti a identificare il codice QR"
},
"unknownCameraErrorTitle": {
"message": "Ooops! Qualcosa è andato storto...."
},
"unknownCameraError": {
"message": "C'è stato un errore nel tentativo di accedere alla fotocamera. Per favore prova di nuovo..."
},
"unlock": {
"message": "Sblocca"
},
"unlockMessage": {
"message": "Il web decentralizzato ti attende"
},
"updatedWithDate": {
"message": "Aggiornata $1"
},
"uriErrorMsg": {
"message": "Gli URI richiedono un prefisso HTTP/HTTPS."
},
@ -807,22 +1303,40 @@
"viewAccount": {
"message": "Vedi Account"
},
"viewOnEtherscan": {
"message": "Vedi su Etherscan"
},
"visitWebSite": {
"message": "Visita il nostro sito web"
},
"walletSeed": {
"message": "Seed del Portafoglio"
},
"warning": {
"message": "Attenzione"
},
"welcomeBack": {
"message": "Bentornato!"
},
"welcomeBeta": {
"message": "Benvenuto nella Beta di MetaMask"
},
"whatsThis": {
"message": "Cos'è questo?"
},
"yesLetsTry": {
"message": "Si, proviamo"
},
"youNeedToAllowCameraAccess": {
"message": "Devi consentire l'accesso alla fotocamera per usare questa funzionalità."
},
"yourSigRequested": {
"message": "E' richiesta la tua firma"
},
"youSign": {
"message": "Ti stai connettendo"
},
"yourPrivateSeedPhrase": {
"message": "La tua frase seed privata"
}
}

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "プライバシーモード"
},
"privacyModeDescription": {
"message": "ウェブサイトはあなたのアカウント情報を閲覧するためのアクセスを要求する必要があります。"
},
"exposeAccounts": {
"message": "アカウントを公開する"
},
"exposeDescription": {
"message": "アカウントを現在のウェブサイトに公開する。従来のdappsに役立ちます。"
},
"confirmExpose": {
"message": "現在のウェブサイトにアカウントを公開してもよろしいですか?"
},
"confirmClear": {
"message": "承認されたウェブサイトをクリアしてもよろしいですか?"
},
"clearApprovalDataSuccess": {
"message": "承認されたウェブサイトデータが正常に消去されました。"
},
"approvalData": {
"message": "承認データ"
},
"approvalDataDescription": {
"message": "承認されたウェブサイトのデータをクリアすると、すべてのサイトで承認を再度要求する必要があります"
},
"clearApprovalData": {
"message": "承認データのクリア"
},
"approve": {
"message": "承認する"
},
"reject": {
"message": "拒否"
},
"providerAPIRequest": {
"message": "Web3 APIリクエスト"
},
"reviewProviderRequest": {
"message": "このEthereum APIリクエストを確認してください。"
},
"providerRequestInfo": {
"message": "下記のドメインは、Ethereumブロックチェーンとやり取りできるようにEthereum APIへのアクセスをリクエストしようとしています。 Web3アクセスを承認する前に、正しいサイトにいることを常に確認してください。"
},
"accept": {
"message": "承認"
},

@ -1,4 +1,46 @@
{
"privacyMode": {
"message": "개인 정보 보호 모드"
},
"privacyModeDescription": {
"message": "웹 사이트는 계정 정보를 볼 수있는 액세스 권한을 요청해야합니다."
},
"exposeAccounts": {
"message": "계정 노출"
},
"exposeDescription": {
"message": "계정을 현재 웹 사이트에 노출하십시오. 기존 dapps에 유용합니다."
},
"confirmExpose": {
"message": "계정을 현재 웹 사이트에 공개 하시겠습니까?"
},
"confirmClear": {
"message": "승인 된 웹 사이트를 삭제 하시겠습니까?"
},
"clearApprovalDataSuccess": {
"message": "승인 된 웹 사이트 데이터가 성공적으로 삭제되었습니다."
},
"approvalData": {
"message": "승인 데이터"
},
"approvalDataDescription": {
"message": "승인 된 웹 사이트 데이터를 삭제하여 모든 사이트에서 승인을 다시 요청해야합니다."
},
"clearApprovalData": {
"message": "승인 데이터 삭제"
},
"reject": {
"message": "받지 않다"
},
"providerAPIRequest": {
"message": "Web3 API 요청"
},
"reviewProviderRequest": {
"message": "이 Ethereum API 요청을 검토하십시오."
},
"providerRequestInfo": {
"message": "아래 나열된 도메인은 Web3 API에 대한 액세스를 요청하여 Ethereum 블록 체인과 상호 작용할 수 있습니다. Ethereum 액세스를 승인하기 전에 항상 올바른 사이트에 있는지 다시 확인하십시오."
},
"accept": {
"message": "수락"
},
@ -14,9 +56,15 @@
"accountName": {
"message": "계정 이름"
},
"accountOptions": {
"message": "계정 옵션"
},
"accountSelectionRequired": {
"message": "계정을 선택하셔야 합니다!"
},
"activityLog": {
"message": "활동 로그"
},
"address": {
"message": "주소"
},
@ -29,6 +77,9 @@
"addTokens": {
"message": "토큰 추가"
},
"addSuggestedTokens": {
"message": "제안된 토큰 추가"
},
"addAcquiredTokens": {
"message": "메타마스크를 통해 획득한 토큰 추가"
},
@ -55,6 +106,9 @@
"attemptingConnect": {
"message": "블록체인에 접속을 시도하는 중입니다."
},
"attemptToCancel": {
"message": "취소 하시겠습니까?"
},
"attributions": {
"message": "속성"
},
@ -116,6 +170,15 @@
"cancel": {
"message": "취소"
},
"cancelAttempt": {
"message": "취소 시도"
},
"cancellationGasFee": {
"message": "취소 가스 수수료"
},
"cancelN": {
"message": "모든 $1 트랜잭션 취소"
},
"classicInterface": {
"message": "예전 인터페이스"
},
@ -229,7 +292,10 @@
"description": "거래 유형 (암호화폐)"
},
"currentConversion": {
"message": "선택된 단위"
"message": "현재 통화"
},
"currentLanguage": {
"message": "현재 언어"
},
"currentNetwork": {
"message": "현재 네트워크"
@ -349,6 +415,9 @@
"exchangeRate": {
"message": "환율"
},
"expandView": {
"message": "큰 화면으로 보기"
},
"exportPrivateKey": {
"message": "개인키 내보내기"
},
@ -466,6 +535,9 @@
"hideTokenPrompt": {
"message": "토큰 숨기기?"
},
"history": {
"message": "히스토리"
},
"howToDeposit": {
"message": "어떤 방법으로 이더를 입금하시겠습니까?"
},
@ -495,6 +567,9 @@
"importUsingSeed": {
"message": "계정 시드 구문으로 가져오기"
},
"info": {
"message": "정보"
},
"infoHelp": {
"message": "정보 및 도움말"
},
@ -548,7 +623,7 @@
"message": "최대"
},
"learnMore": {
"message": "더 배우기."
"message": "더 알아보기."
},
"ledgerAccountRestriction": {
"message": "새 계정을 추가하려면 최소 마지막 계정을 사용해야 합니다."
@ -599,6 +674,9 @@
"metamaskDescription": {
"message": "메타마스크는 이더리움을 위한 안전한 신분 저장소입니다."
},
"metamaskVersion": {
"message": "메타마스크 버전"
},
"metamaskSeedWords": {
"message": "메타마스크 시드 단어"
},
@ -619,7 +697,7 @@
"description": "사용자는 계정을 가져오기 위해서 파일을 추가후 계속 진행해야 합니다"
},
"needImportPassword": {
"message": "선택 된 파일에 대한 비밀번호를 입력해주세요.",
"message": "선택된 파일에 대한 비밀번호를 입력해주세요.",
"description": "계정을 가져오기 위해서 비밀번호와 파일이 필요합니다."
},
"negativeETH": {
@ -736,6 +814,9 @@
"pasteSeed": {
"message": "시드 구문을 이곳에 붙여넣어 주세요!"
},
"pending": {
"message": "펜딩 중"
},
"personalAddressDetected": {
"message": "개인 주소가 탐지됨. 토큰 컨트랙트 주소를 입력하세요."
},
@ -764,6 +845,9 @@
"qrCode": {
"message": "QR 코드 보기"
},
"queue": {
"message": "큐"
},
"readdToken": {
"message": "옵션 메뉴에서 “토큰 추가”를 눌러서 추후에 다시 이 토큰을 추가하실 수 있습니다."
},
@ -782,6 +866,15 @@
"refundAddress": {
"message": "환불받을 주소"
},
"rejectAll": {
"message": "모두 거부"
},
"rejectTxsN": {
"message": "$1 트랜잭션 거부"
},
"rejectTxsDescription": {
"message": "$1 트랜잭션을 거부합니다."
},
"rejected": {
"message": "거부됨"
},
@ -901,6 +994,9 @@
"selectCurrency": {
"message": "통화 선택"
},
"selectLocale": {
"message": "언어 선택"
},
"selectService": {
"message": "서비스 선택"
},
@ -916,6 +1012,12 @@
"sendTokens": {
"message": "토큰 전송"
},
"sentEther": {
"message": "전송된 이더"
},
"sentTokens": {
"message": "전송된 토큰"
},
"separateEachWord": {
"message": "각 단어는 공백 한칸으로 분리합니다"
},
@ -943,9 +1045,6 @@
"settings": {
"message": "설정"
},
"info": {
"message": "정보"
},
"shapeshiftBuy": {
"message": "Shapeshift를 통해서 구매하기"
},
@ -955,12 +1054,21 @@
"showQRCode": {
"message": "QR 코드 보기"
},
"showHexData": {
"message": "Hex 데이터 보기"
},
"showHexDataDescription": {
"message": "선택하면 전송화면의 hex 데이터 필드 값을 보여줍니다."
},
"sign": {
"message": "서명"
},
"signed": {
"message": "서명됨"
},
"signatureRequest": {
"message": "서명 요청"
},
"signMessage": {
"message": "메시지 서명"
},
@ -1058,6 +1166,9 @@
"total": {
"message": "합계"
},
"transaction": {
"message": "트랜잭션"
},
"transactions": {
"message": "트랜잭션"
},
@ -1104,6 +1215,9 @@
"unavailable": {
"message": "이용할 수 없음"
},
"units": {
"message": "단위"
},
"unknown": {
"message": "알 수 없음"
},
@ -1174,11 +1288,14 @@
"whatsThis": {
"message": "이것은 무엇인가요?"
},
"yesLetsTry": {
"message": "네, 시도해보겠습니다."
},
"yourSigRequested": {
"message": "서명을 요청 중입니다."
},
"youSign": {
"message": "서명 중입니다"
"message": "서명니다"
},
"yourPrivateSeedPhrase": {
"message": "개인 시드 구문"

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Privacy-modus"
},
"privacyModeDescription": {
"message": "Websites moeten toegang vragen om uw accountgegevens te bekijken."
},
"exposeAccounts": {
"message": "Expose Accounts"
},
"exposeDescription": {
"message": "Stel accounts vrij op de huidige website. Handig voor legacy dapps."
},
"confirmExpose": {
"message": "Weet u zeker dat u uw accounts wilt weergeven aan de huidige website?"
},
"confirmClear": {
"message": "Weet je zeker dat je goedgekeurde websites wilt wissen?"
},
"clearApprovalDataSuccess": {
"message": "Goedgekeurde websitegegevens zijn met succes gewist."
},
"approvalData": {
"message": "Goedkeuringsgegevens"
},
"approvalDataDescription": {
"message": "Goedgekeurde websitegegevens wissen zodat alle sites opnieuw goedkeuring moeten aanvragen."
},
"clearApprovalData": {
"message": "Gegevens over goedkeuring wissen"
},
"approve": {
"message": "Goedkeuren"
},
"reject": {
"message": "Afwijzen"
},
"providerAPIRequest": {
"message": "Web3 API-aanvraag"
},
"reviewProviderRequest": {
"message": "Bekijk deze Ethereum API-aanvraag."
},
"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."
},
"accept": {
"message": "Aanvaarden"
},
@ -444,7 +489,7 @@
"message": "back-up woorden hebben alleen kleine letters"
},
"mainnet": {
"message": "belangrijkste Ethereum-netwerk"
"message": "Main Netwerk"
},
"message": {
"message": "Bericht"

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Mode ng Privacy"
},
"privacyModeDescription": {
"message": "Dapat humiling ng access ang mga website upang tingnan ang impormasyon ng iyong account."
},
"exposeAccounts": {
"message": "Ilantad ang Mga Account"
},
"exposeDescription": {
"message": "Ilantad ang mga account sa kasalukuyang website. Kapaki-pakinabang para sa mga dapps ng legacy."
},
"confirmExpose": {
"message": "Sigurado ka bang gusto mong ilantad ang iyong mga account sa kasalukuyang website?"
},
"confirmClear": {
"message": "Sigurado ka bang gusto mong i-clear ang mga naaprubahang website?"
},
"clearApprovalDataSuccess": {
"message": "Matagumpay na na-clear ang data ng aprubadong website."
},
"approvalData": {
"message": "Data ng Pag-apruba"
},
"approvalDataDescription": {
"message": "I-clear ang naaprubahang data ng website upang ang lahat ng site ay dapat humiling muli ng pag-apruba"
},
"clearApprovalData": {
"message": "Tanggalin ang data ng pag-apruba"
},
"approve": {
"message": "Aprubahan"
},
"reject": {
"message": "Tanggihan"
},
"providerAPIRequest": {
"message": "Kahilingan sa Web3 API"
},
"reviewProviderRequest": {
"message": "Mangyaring suriin ang kahilingan sa Ethereum API na ito."
},
"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."
},
"accept": {
"message": "Tanggapin"
},

File diff suppressed because it is too large Load Diff

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Modo de privacidade"
},
"privacyModeDescription": {
"message": "Os sites devem solicitar acesso para visualizar as informações da sua conta."
},
"exposeAccounts": {
"message": "Expor contas"
},
"exposeDescription": {
"message": "Exponha contas ao site atual. Útil para dapps herdados."
},
"confirmExpose": {
"message": "Tem certeza de que deseja expor suas contas ao site atual?"
},
"confirmClear": {
"message": "Tem certeza de que deseja limpar sites aprovados?"
},
"clearApprovalDataSuccess": {
"message": "Dados aprovados do website foram limpos com sucesso."
},
"approvalData": {
"message": "Dados de aprovação"
},
"approvalDataDescription": {
"message": "Limpe os dados aprovados do website para que todos os sites solicitem aprovação novamente."
},
"clearApprovalData": {
"message": "Limpar dados de aprovação"
},
"approve": {
"message": "Aprovar"
},
"reject": {
"message": "Rejeitar"
},
"providerAPIRequest": {
"message": "Solicitação de API do Web3"
},
"reviewProviderRequest": {
"message": "Por favor, revise esta solicitação da API da Ethereum."
},
"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."
},
"accept": {
"message": "Aceitar"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Режим конфиденциальности"
},
"privacyModeDescription": {
"message": "Веб-сайты должны запрашивать доступ для просмотра информации об учетной записи."
},
"exposeAccounts": {
"message": "Открыть счета"
},
"exposeDescription": {
"message": "Выводить счета на текущий веб-сайт. Полезно для старых dapps."
},
"confirmExpose": {
"message": "Вы уверены, что хотите открыть свои аккаунты на текущем веб-сайте?"
},
"confirmClear": {
"message": "Вы уверены, что хотите очистить утвержденные веб-сайты?Tem certeza de que deseja limpar sites aprovados?"
},
"clearApprovalDataSuccess": {
"message": "Утвержденные данные веб-сайта успешно удалены."
},
"approvalData": {
"message": "Данные об утверждении"
},
"approvalDataDescription": {
"message": "Очистите утвержденные данные веб-сайта, чтобы все сайты снова запросили подтверждение."
},
"clearApprovalData": {
"message": "Четкие данные об утверждении"
},
"approve": {
"message": "Одобрить"
},
"reject": {
"message": "Отклонить"
},
"providerAPIRequest": {
"message": "Запрос API Web3"
},
"reviewProviderRequest": {
"message": "Просмотрите этот запрос API Ethereum."
},
"providerRequestInfo": {
"message": "Домен, указанный ниже, пытается запросить доступ к API-интерфейсу Ethereum, чтобы он мог взаимодействовать с блокчейном Ethereum. Всегда проверяйте, что вы находитесь на правильном сайте, прежде чем одобрять доступ к веб-сайту."
},
"accept": {
"message": "Принять"
},

File diff suppressed because it is too large Load Diff

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "โหมดความเปนสวนตว"
},
"privacyModeDescription": {
"message": "เวบไซตองขอเขาถงเพอดอมลบญชของคณ"
},
"exposeAccounts": {
"message": "เปดเผยบญช"
},
"exposeDescription": {
"message": "เปดเผยบญชไปยงเวบไซตจจน มประโยชนสำหรบ dapps แบบเดม"
},
"confirmExpose": {
"message": "คณแนใจหรอไมาตองการเปดเผยบญชของคณไปยงเวบไซตจจน"
},
"confirmClear": {
"message": "คณแนใจหรอไมาตองการลางเวบไซตานการอน"
},
"clearApprovalDataSuccess": {
"message": "อนอมลเวบไซตไดบอนแลว"
},
"approvalData": {
"message": "ขอมลการอน"
},
"approvalDataDescription": {
"message": "ลางขอมลเวบไซตไดบการอนเพอใหกไซตองขออนกครง"
},
"clearApprovalData": {
"message": "ลางขอมลการอน"
},
"approve": {
"message": "อน"
},
"reject": {
"message": "ปฏเสธ"
},
"providerAPIRequest": {
"message": "คำขอ Web3 API"
},
"reviewProviderRequest": {
"message": "โปรดอานคำขอ Ethereum API น"
},
"providerRequestInfo": {
"message": "โดเมนทแสดงดานลางกำลงพยายามขอเขาถง API ของ Ethereum เพอใหสามารถโตตอบกบบลอค Ethereum ได ตรวจสอบวาคณอยในไซตกตองกอนทจะอนการเขาถง Ethereum เสมอ"
},
"accept": {
"message": "ยอมรบ"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "தனிி"
},
"privacyModeDescription": {
"message": "உஙகள கணக தகவலிட வலதளஙகள அணகலர வ."
},
"exposeAccounts": {
"message": "கணககள அமபலபபடகள"
},
"exposeDescription": {
"message": "தறய இணயதளதி கணககள அமபலபபடகள. மரபண பயனபடத."
},
"confirmExpose": {
"message": "நிசயமக உஙகள கணககள தறய இணயததளதி அமபலபபடத விிகள?"
},
"confirmClear": {
"message": "அஙகரிகபபடட வலதளஙகளிசயமக நக விிகள?"
},
"clearApprovalDataSuccess": {
"message": "அஙகரிகபபடட வலதள தரவிகரமக அழிகபபடடத."
},
"approvalData": {
"message": "ஒபதல தரவ"
},
"approvalDataDescription": {
"message": "அஙகரிகபபடட வலதள தரவ அழிகவ, அன தளஙகள ஒபதலர வ."
},
"clearApprovalData": {
"message": "ஒபதல தரவ அழி"
},
"approve": {
"message": "ஒபதல"
},
"reject": {
"message": "நிகரி"
},
"providerAPIRequest": {
"message": "Web3 API கி"
},
"reviewProviderRequest": {
"message": "இநத வல 3 API கி மதியவ."
},
"providerRequestInfo": {
"message": "க படியலிடபபடள ட Web3 ஏபிஐ அணகலவதறயறிிறத, எனவ இத Ethereum blockchain உடனடரள மி. Web3 அண அஙகரிபதற சரின தளதி இரபத எப இர சரிகவ."
},
"accept": {
"message": "ஏறகவ"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Gizlilik modu"
},
"privacyModeDescription": {
"message": "Web siteleri, hesap bilgilerinizi görmek için erişim istemek zorundadır."
},
"exposeAccounts": {
"message": "Hesapları Açığa Çıkar"
},
"exposeDescription": {
"message": "Hesapları mevcut web sitesine gösterin. Eski dapps için kullanışlıdır."
},
"confirmExpose": {
"message": "Hesaplarınızı mevcut web sitesine taşımak istediğinizden emin misiniz?"
},
"confirmClear": {
"message": "Onaylanmış web sitelerini silmek istediğinizden emin misiniz?"
},
"clearApprovalDataSuccess": {
"message": "Onaylanan web sitesi verileri başarıyla temizlendi."
},
"approvalData": {
"message": "Onay Verileri"
},
"approvalDataDescription": {
"message": "Onaylanan web sitesi verilerini temizle, tüm sitelerin tekrar onay isteğinde bulunması gerekir."
},
"clearApprovalData": {
"message": "Onay verilerini temizle"
},
"approve": {
"message": "Onaylamak"
},
"reject": {
"message": "Reddetmek"
},
"providerAPIRequest": {
"message": "Web3 API İsteği"
},
"reviewProviderRequest": {
"message": "Lütfen bu Ethereum API isteğini inceleyin."
},
"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."
},
"accept": {
"message": "Kabul et"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "Chế độ riêng tư"
},
"privacyModeDescription": {
"message": "Trang web phải yêu cầu quyền truy cập để xem thông tin tài khoản của bạn."
},
"exposeAccounts": {
"message": "Hiển thị tài khoản"
},
"exposeDescription": {
"message": "Hiển thị tài khoản cho trang web hiện tại. Hữu ích cho các ứng dụng cũ."
},
"confirmExpose": {
"message": "Bạn có chắc chắn muốn hiển thị tài khoản của mình cho trang web hiện tại không?"
},
"confirmClear": {
"message": "Bạn có chắc chắn muốn xóa các trang web được phê duyệt không?"
},
"clearApprovalDataSuccess": {
"message": "Đã xóa thành công dữ liệu trang web được phê duyệt."
},
"approvalData": {
"message": "Dữ liệu phê duyệt"
},
"approvalDataDescription": {
"message": "Xóa dữ liệu trang web được phê duyệt để tất cả các trang web phải yêu cầu phê duyệt lại."
},
"clearApprovalData": {
"message": "Xóa dữ liệu phê duyệt"
},
"approve": {
"message": "Phê duyệt"
},
"reject": {
"message": "Từ chối"
},
"providerAPIRequest": {
"message": "Yêu cầu API Web3"
},
"reviewProviderRequest": {
"message": "Vui lòng xem lại yêu cầu API Ethereum này."
},
"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."
},
"accept": {
"message": "Chấp nhận"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "隐私模式"
},
"privacyModeDescription": {
"message": "网站必须请求访问权限才能查看您的帐户信息。"
},
"exposeAccounts": {
"message": "公开账户"
},
"exposeDescription": {
"message": "将帐户公开给当前网站。对传统dapps很有用。"
},
"confirmExpose": {
"message": "您确定要将帐户公开到当前网站吗?"
},
"confirmClear": {
"message": "您确定要清除已批准的网站吗?"
},
"clearApprovalDataSuccess": {
"message": "已批准的网站数据已成功清除。"
},
"approvalData": {
"message": "审批数据"
},
"approvalDataDescription": {
"message": "清除已批准的网站数据,以便所有网站都必须再次申请"
},
"clearApprovalData": {
"message": "清除批准数据"
},
"approve": {
"message": "批准"
},
"reject": {
"message": "拒绝"
},
"providerAPIRequest": {
"message": "Web3 API请求"
},
"reviewProviderRequest": {
"message": "请查看此Ethereum API请求。"
},
"providerRequestInfo": {
"message": "下面列出的域正在尝试请求访问Ethereum API,以便它可以与以太坊区块链进行交互。在批准Ethereum访问之前,请务必仔细检查您是否在正确的站点上。"
},
"accept": {
"message": "接受"
},

@ -1,4 +1,49 @@
{
"privacyMode": {
"message": "隱私模式"
},
"privacyModeDescription": {
"message": "網站必須請求訪問權限才能查看您的帳戶信息。"
},
"exposeAccounts": {
"message": "公開賬戶"
},
"exposeDescription": {
"message": "將帳戶公開給當前網站。對傳統dapps很有用。"
},
"confirmExpose": {
"message": "您確定要將帳戶公開到當前網站嗎?"
},
"confirmClear": {
"message": "您確定要清除已批准的網站嗎?"
},
"clearApprovalDataSuccess": {
"message": "已批准的網站數據已成功清除。"
},
"approvalData": {
"message": "審批數據"
},
"approvalDataDescription": {
"message": "清除已批准的網站數據,以便所有網站都必須再次申請"
},
"clearApprovalData": {
"message": "清除批准數據"
},
"approve": {
"message": "批准"
},
"reject": {
"message": "拒絕"
},
"providerAPIRequest": {
"message": "Web3 API請求"
},
"reviewProviderRequest": {
"message": "請查看此Ethereum API請求。"
},
"providerRequestInfo": {
"message": "下面列出的域正在嘗試請求訪問Ethereum API,以便它可以與以太坊區塊鏈進行交互。在批准Ethereum訪問之前,請務必仔細檢查您是否在正確的站點上。"
},
"accept": {
"message": "接受"
},

@ -7,6 +7,7 @@
</head>
<body>
<div id="app-content"></div>
<script src="./libs.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#38393A;}
</style>
<title>deposit-eth</title>
<desc>Created with Sketch.</desc>
<g id="deposit-eth" transform="translate(0.000000, 14.000000)">
<path id="Shape" class="st0" d="M19.9,16L7.5,8.7L19.9,26L32.3,8.7L19.9,16L19.9,16z M20.1-14L7.7,6.4l12.4,7.3l12.4-7.2L20.1-14z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 670 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<path d="M34.5 19C34.5 27.0081 28.0081 33.5 20 33.5C11.9919 33.5 5.5 27.0081 5.5 19C5.5 10.9919 11.9919 4.5 20 4.5C28.0081 4.5 34.5 10.9919 34.5 19Z" fill="#61BA00" stroke="#61BA00"/>
<path d="M13.2998 19.7195L16.813 23.3195L26.1998 14.0195" stroke="white" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d" x="0" y="0" width="40" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="2.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,5 +1,9 @@
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>MetaMask Loading</title>
<style>
#div-logo {
@ -31,5 +35,11 @@
<div id="div-logo">
<img id="logo" src="./images/loginglogo.svg">
</div>
<script type="text/javascript">
// redirect to 404 after one minute
setTimeout(() => {
location.href = './404.html'
}, 60000)
</script>
</body>
</html>

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "4.14.0",
"version": "5.0.2",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",
@ -77,4 +77,4 @@
"*"
]
}
}
}

@ -11,6 +11,7 @@
</head>
<body class="notification" style="height:600px;">
<div id="app-content"></div>
<script src="./libs.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -7,6 +7,7 @@
</head>
<body style="width:357px; height:600px;">
<div id="app-content"></div>
<script src="./libs.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -2,6 +2,9 @@
* @file The entry point for the web extension singleton process.
*/
// this needs to run before anything else
require('./lib/setupFetchDebugging')()
const urlUtil = require('url')
const endOfStream = require('end-of-stream')
const pump = require('pump')
@ -20,13 +23,13 @@ const createStreamSink = require('./lib/createStreamSink')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
const rawFirstTimeState = require('./first-time-state')
const setupRaven = require('./lib/setupRaven')
const setupSentry = require('./lib/setupSentry')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor')
const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code')
const getObjStructure = require('./lib/getObjStructure')
const ipfsContent = require('./lib/ipfsContent.js')
const setupEnsIpfsResolver = require('./lib/ens-ipfs/setup')
const {
ENVIRONMENT_TYPE_POPUP,
@ -47,7 +50,7 @@ global.METAMASK_NOTIFIER = notificationManager
// setup sentry error reporting
const release = platform.getVersion()
const raven = setupRaven({ release })
const sentry = setupSentry({ release })
// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Internet Explorer 6-11
@ -55,7 +58,6 @@ const isIE = !!document.documentMode
// Edge 20+
const isEdge = !isIE && !!window.StyleMedia
let ipfsHandle
let popupIsOpen = false
let notificationIsOpen = false
const openMetamaskTabsIDs = {}
@ -161,7 +163,6 @@ async function initialize () {
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.')
ipfsHandle = ipfsContent(initState.NetworkController.provider)
}
//
@ -194,14 +195,14 @@ async function loadStateFromPersistence () {
// we were able to recover (though it might be old)
versionedData = diskStoreState
const vaultStructure = getObjStructure(versionedData)
raven.captureMessage('MetaMask - Empty vault found - recovered from diskStore', {
sentry.captureMessage('MetaMask - Empty vault found - recovered from diskStore', {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
} else {
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState)
raven.captureMessage('MetaMask - Empty vault found - unable to recover')
sentry.captureMessage('MetaMask - Empty vault found - unable to recover')
}
}
@ -209,7 +210,7 @@ async function loadStateFromPersistence () {
migrator.on('error', (err) => {
// get vault structure without secrets
const vaultStructure = getObjStructure(versionedData)
raven.captureException(err, {
sentry.captureException(err, {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
@ -255,7 +256,8 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
showWatchAssetUi: showWatchAssetUi,
openPopup: openPopup,
closePopup: notificationManager.closePopup.bind(notificationManager),
// initial state
initState,
// initial locale code
@ -266,17 +268,15 @@ function setupController (initState, initLangCode) {
})
global.metamaskController = controller
controller.networkController.on('networkDidChange', () => {
ipfsHandle && ipfsHandle.remove()
ipfsHandle = ipfsContent(controller.networkController.providerStore.getState())
})
const provider = controller.provider
setupEnsIpfsResolver({ provider })
// report failed transactions to Sentry
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') return
const txMeta = controller.txController.txStateManager.getTx(txId)
try {
reportFailedTxToSentry({ raven, txMeta })
reportFailedTxToSentry({ sentry, txMeta })
} catch (e) {
console.error(e)
}
@ -448,7 +448,7 @@ function triggerUi () {
* Opens the browser popup for user confirmation of watchAsset
* then it waits until user interact with the UI
*/
function showWatchAssetUi () {
function openPopup () {
triggerUi()
return new Promise(
(resolve) => {

@ -7,10 +7,12 @@ const PongStream = require('ping-pong-stream/pong')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
const PortStream = require('extension-port-stream')
const TransformStream = require('stream').Transform
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
const inpageBundle = inpageContent + inpageSuffix
let isEnabled = false
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
@ -20,24 +22,27 @@ const inpageBundle = inpageContent + inpageSuffix
// MetaMask will be much faster loading and performant on Firefox.
if (shouldInjectWeb3()) {
setupInjection()
injectScript(inpageBundle)
setupStreams()
listenForProviderRequest()
checkPrivacyMode()
}
/**
* Creates a script tag that injects inpage.js
* Injects a script tag into the current document
*
* @param {string} content - Code to be executed in the current document
*/
function setupInjection () {
function injectScript (content) {
try {
// inject in-page script
var scriptTag = document.createElement('script')
scriptTag.textContent = inpageBundle
scriptTag.onload = function () { this.parentNode.removeChild(this) }
var container = document.head || document.documentElement
// append as first child
const container = document.head || document.documentElement
const scriptTag = document.createElement('script')
scriptTag.setAttribute('async', false)
scriptTag.textContent = content
container.insertBefore(scriptTag, container.children[0])
container.removeChild(scriptTag)
} catch (e) {
console.error('Metamask injection failed.', e)
console.error('MetaMask script injection failed', e)
}
}
@ -54,10 +59,22 @@ function setupStreams () {
const pluginPort = extension.runtime.connect({ name: 'contentscript' })
const pluginStream = new PortStream(pluginPort)
// Filter out selectedAddress until this origin is enabled
const approvalTransform = new TransformStream({
objectMode: true,
transform: (data, _, done) => {
if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !isEnabled) {
data.data.selectedAddress = undefined
}
done(null, { ...data })
},
})
// forward communication plugin->inpage
pump(
pageStream,
pluginStream,
approvalTransform,
pageStream,
(err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err)
)
@ -97,6 +114,73 @@ function setupStreams () {
mux.ignoreStream('publicConfig')
}
/**
* Establishes listeners for requests to fully-enable the provider from the dapp context
* and for full-provider approvals and rejections from the background script context. Dapps
* should not post messages directly and should instead call provider.enable(), which
* handles posting these messages internally.
*/
function listenForProviderRequest () {
window.addEventListener('message', ({ source, data }) => {
if (source !== window || !data || !data.type) { return }
switch (data.type) {
case 'ETHEREUM_ENABLE_PROVIDER':
extension.runtime.sendMessage({
action: 'init-provider-request',
force: data.force,
origin: source.location.hostname,
siteImage: getSiteIcon(source),
siteTitle: getSiteName(source),
})
break
case 'ETHEREUM_IS_APPROVED':
extension.runtime.sendMessage({
action: 'init-is-approved',
origin: source.location.hostname,
})
break
case 'METAMASK_IS_UNLOCKED':
extension.runtime.sendMessage({
action: 'init-is-unlocked',
})
break
}
})
extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked, selectedAddress }) => {
switch (action) {
case 'approve-provider-request':
isEnabled = true
window.postMessage({ type: 'ethereumprovider', selectedAddress }, '*')
break
case 'approve-legacy-provider-request':
isEnabled = true
window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*')
break
case 'reject-provider-request':
window.postMessage({ type: 'ethereumprovider', error: 'User rejected provider access' }, '*')
break
case 'answer-is-approved':
window.postMessage({ type: 'ethereumisapproved', isApproved, caching }, '*')
break
case 'answer-is-unlocked':
window.postMessage({ type: 'metamaskisunlocked', isUnlocked }, '*')
break
case 'metamask-set-locked':
isEnabled = false
window.postMessage({ type: 'metamasksetlocked' }, '*')
break
}
})
}
/**
* Checks if MetaMask is currently operating in "privacy mode", meaning
* dapps must call ethereum.enable in order to access user accounts
*/
function checkPrivacyMode () {
extension.runtime.sendMessage({ action: 'init-privacy-request' })
}
/**
* Error handler for page to plugin stream disconnections
@ -135,17 +219,22 @@ function doctypeCheck () {
}
/**
* Checks the current document extension
* Returns whether or not the extension (suffix) of the current document is prohibited
*
* @returns {boolean} {@code true} if the current extension is not 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.
*
* @returns {boolean} whether or not the extension of the current document is prohibited
*/
function suffixCheck () {
var prohibitedTypes = ['xml', 'pdf']
var currentUrl = window.location.href
var currentRegex
const prohibitedTypes = [
/\.xml$/,
/\.pdf$/,
]
const currentUrl = window.location.pathname
for (let i = 0; i < prohibitedTypes.length; i++) {
currentRegex = new RegExp(`\\.${prohibitedTypes[i]}$`)
if (currentRegex.test(currentUrl)) {
if (prohibitedTypes[i].test(currentUrl)) {
return false
}
}
@ -205,3 +294,31 @@ function redirectToPhishingWarning () {
href: window.location.href,
})}`
}
function getSiteName (window) {
const document = window.document
const siteName = document.querySelector('head > meta[property="og:site_name"]')
if (siteName) {
return siteName.content
}
return document.title
}
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
}

@ -83,8 +83,25 @@ class BlacklistController {
*
*/
async updatePhishingList () {
const response = await fetch('https://api.infura.io/v2/blacklist')
const phishing = await response.json()
// make request
let response
try {
response = await fetch('https://api.infura.io/v2/blacklist')
} catch (err) {
log.error(new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`))
return
}
// parse response
let rawResponse
let phishing
try {
const rawResponse = await response.text()
phishing = JSON.parse(rawResponse)
} catch (err) {
log.error(new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`))
return
}
// update current blacklist
this.store.updateState({ phishing })
this._setupPhishingDetector(phishing)
return phishing
@ -97,9 +114,9 @@ class BlacklistController {
*/
scheduleUpdates () {
if (this._phishingUpdateIntervalRef) return
this.updatePhishingList().catch(log.warn)
this.updatePhishingList()
this._phishingUpdateIntervalRef = setInterval(() => {
this.updatePhishingList().catch(log.warn)
this.updatePhishingList()
}, POLLING_INTERVAL)
}

@ -21,6 +21,7 @@ class CurrencyController {
* since midnight of January 1, 1970
* @property {number} conversionInterval The id of the interval created by the scheduleConversionInterval method.
* Used to clear an existing interval on subsequent calls of that method.
* @property {string} nativeCurrency The ticker/symbol of the native chain currency
*
*/
constructor (opts = {}) {
@ -28,6 +29,7 @@ class CurrencyController {
currentCurrency: 'usd',
conversionRate: 0,
conversionDate: 'N/A',
nativeCurrency: 'ETH',
}, opts.initState)
this.store = new ObservableStore(initState)
}
@ -36,6 +38,29 @@ class CurrencyController {
// PUBLIC METHODS
//
/**
* A getter for the nativeCurrency property
*
* @returns {string} A 2-4 character shorthand that describes the specific currency
*
*/
getNativeCurrency () {
return this.store.getState().nativeCurrency
}
/**
* A setter for the nativeCurrency property
*
* @param {string} nativeCurrency The new currency to set as the nativeCurrency in the store
*
*/
setNativeCurrency (nativeCurrency) {
this.store.updateState({
nativeCurrency,
ticker: nativeCurrency,
})
}
/**
* A getter for the currentCurrency property
*
@ -104,17 +129,60 @@ class CurrencyController {
*
*/
async updateConversionRate () {
let currentCurrency
let currentCurrency, nativeCurrency
try {
currentCurrency = this.getCurrentCurrency()
const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`)
const parsedResponse = await response.json()
this.setConversionRate(Number(parsedResponse.bid))
this.setConversionDate(Number(parsedResponse.timestamp))
nativeCurrency = this.getNativeCurrency()
// select api
let apiUrl
if (nativeCurrency === 'ETH') {
// ETH
apiUrl = `https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`
} else {
// ETC
apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}`
}
// attempt request
let response
try {
response = await fetch(apiUrl)
} catch (err) {
log.error(new Error(`CurrencyController - Failed to request currency from Infura:\n${err.stack}`))
return
}
// parse response
let rawResponse
let parsedResponse
try {
rawResponse = await response.text()
parsedResponse = JSON.parse(rawResponse)
} catch (err) {
log.error(new Error(`CurrencyController - Failed to parse response "${rawResponse}"`))
return
}
// set conversion rate
if (nativeCurrency === 'ETH') {
// ETH
this.setConversionRate(Number(parsedResponse.bid))
this.setConversionDate(Number(parsedResponse.timestamp))
} else {
// ETC
if (parsedResponse[currentCurrency.toUpperCase()]) {
this.setConversionRate(Number(parsedResponse[currentCurrency.toUpperCase()]))
this.setConversionDate(parseInt((new Date()).getTime() / 1000))
} else {
this.setConversionRate(0)
this.setConversionDate('N/A')
}
}
} catch (err) {
log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err)
// reset current conversion rate
log.warn(`MetaMask - Failed to query currency conversion:`, nativeCurrency, currentCurrency, err)
this.setConversionRate(0)
this.setConversionDate('N/A')
// throw error
log.error(new Error(`CurrencyController - Failed to query rate for currency "${currentCurrency}":\n${err.stack}`))
return
}
}

@ -1,4 +1,5 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref')
const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty')
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
@ -16,6 +17,7 @@ function createInfuraClient ({ network }) {
const blockTracker = new BlockTracker({ provider: infuraProvider })
const networkMiddleware = mergeMiddleware([
createNetworkAndChainIdMiddleware({ network }),
createBlockCacheMiddleware({ blockTracker }),
createInflightMiddleware(),
createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }),
@ -25,3 +27,34 @@ function createInfuraClient ({ network }) {
])
return { networkMiddleware, blockTracker }
}
function createNetworkAndChainIdMiddleware({ network }) {
let chainId
let netId
switch (network) {
case 'mainnet':
netId = '1'
chainId = '0x01'
break
case 'ropsten':
netId = '3'
chainId = '0x03'
break
case 'rinkeby':
netId = '4'
chainId = '0x04'
break
case 'kovan':
netId = '42'
chainId = '0x2a'
break
default:
throw new Error(`createInfuraClient - unknown network "${network}"`)
}
return createScaffoldMiddleware({
eth_chainId: chainId,
net_version: netId,
})
}

@ -11,6 +11,7 @@ function createMetamaskMiddleware ({
processTransaction,
processEthSignMessage,
processTypedMessage,
processTypedMessageV3,
processPersonalMessage,
getPendingNonce,
}) {
@ -25,6 +26,7 @@ function createMetamaskMiddleware ({
processTransaction,
processEthSignMessage,
processTypedMessage,
processTypedMessageV3,
processPersonalMessage,
}),
createPendingNonceMiddleware({ getPendingNonce }),

@ -11,6 +11,8 @@ const createInfuraClient = require('./createInfuraClient')
const createJsonRpcClient = require('./createJsonRpcClient')
const createLocalhostClient = require('./createLocalhostClient')
const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy')
const extend = require('extend')
const networks = { networkList: {} }
const {
ROPSTEN,
@ -29,6 +31,10 @@ const defaultProviderConfig = {
type: testMode ? RINKEBY : MAINNET,
}
const defaultNetworkConfig = {
ticker: 'ETH',
}
module.exports = class NetworkController extends EventEmitter {
constructor (opts = {}) {
@ -39,7 +45,8 @@ module.exports = class NetworkController extends EventEmitter {
// create stores
this.providerStore = new ObservableStore(providerConfig)
this.networkStore = new ObservableStore('loading')
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
this.networkConfig = new ObservableStore(defaultNetworkConfig)
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig })
this.on('networkDidChange', this.lookupNetwork)
// provider and block tracker
this._provider = null
@ -51,8 +58,8 @@ module.exports = class NetworkController extends EventEmitter {
initializeProvider (providerParams) {
this._baseProviderParams = providerParams
const { type, rpcTarget } = this.providerStore.getState()
this._configureProvider({ type, rpcTarget })
const { type, rpcTarget, chainId, ticker, nickname } = this.providerStore.getState()
this._configureProvider({ type, rpcTarget, chainId, ticker, nickname })
this.lookupNetwork()
}
@ -72,7 +79,20 @@ module.exports = class NetworkController extends EventEmitter {
return this.networkStore.getState()
}
setNetworkState (network) {
getNetworkConfig () {
return this.networkConfig.getState()
}
setNetworkState (network, type) {
if (network === 'loading') {
return this.networkStore.putState(network)
}
// type must be defined
if (!type) {
return
}
network = networks.networkList[type] && networks.networkList[type].chainId ? networks.networkList[type].chainId : network
return this.networkStore.putState(network)
}
@ -85,18 +105,22 @@ module.exports = class NetworkController extends EventEmitter {
if (!this._provider) {
return log.warn('NetworkController - lookupNetwork aborted due to missing provider')
}
var { type } = this.providerStore.getState()
const ethQuery = new EthQuery(this._provider)
ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network)
this.setNetworkState(network)
this.setNetworkState(network, type)
})
}
setRpcTarget (rpcTarget) {
setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') {
const providerConfig = {
type: 'rpc',
rpcTarget,
chainId,
ticker,
nickname,
}
this.providerConfig = providerConfig
}
@ -132,7 +156,7 @@ module.exports = class NetworkController extends EventEmitter {
}
_configureProvider (opts) {
const { type, rpcTarget } = opts
const { type, rpcTarget, chainId, ticker, nickname } = opts
// infura type-based endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type)
if (isInfura) {
@ -142,7 +166,7 @@ module.exports = class NetworkController extends EventEmitter {
this._configureLocalhostProvider()
// url-based rpc endpoints
} else if (type === 'rpc') {
this._configureStandardProvider({ rpcUrl: rpcTarget })
this._configureStandardProvider({ rpcUrl: rpcTarget, chainId, ticker, nickname })
} else {
throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`)
}
@ -152,6 +176,11 @@ module.exports = class NetworkController extends EventEmitter {
log.info('NetworkController - configureInfuraProvider', type)
const networkClient = createInfuraClient({ network: type })
this._setNetworkClient(networkClient)
// setup networkConfig
var settings = {
ticker: 'ETH',
}
this.networkConfig.putState(settings)
}
_configureLocalhostProvider () {
@ -160,9 +189,22 @@ module.exports = class NetworkController extends EventEmitter {
this._setNetworkClient(networkClient)
}
_configureStandardProvider ({ rpcUrl }) {
_configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) {
log.info('NetworkController - configureStandardProvider', rpcUrl)
const networkClient = createJsonRpcClient({ rpcUrl })
// hack to add a 'rpc' network with chainId
networks.networkList['rpc'] = {
chainId: chainId,
rpcUrl,
ticker: ticker || 'ETH',
nickname,
}
// setup networkConfig
var settings = {
network: chainId,
}
settings = extend(settings, networks.networkList['rpc'])
this.networkConfig.putState(settings)
this._setNetworkClient(networkClient)
}

@ -25,7 +25,7 @@ class PreferencesController {
*/
constructor (opts = {}) {
const initState = extend({
frequentRpcList: [],
frequentRpcListDetail: [],
currentAccountTab: 'history',
accountTokens: {},
assetImages: {},
@ -38,12 +38,15 @@ class PreferencesController {
lostIdentities: {},
seedWords: null,
forgottenPassword: false,
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
}, opts.initState)
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
this.showWatchAssetUi = opts.showWatchAssetUi
this.openPopup = opts.openPopup
this._subscribeProviderType()
}
// PUBLIC METHODS
@ -101,7 +104,7 @@ class PreferencesController {
* @param {Function} - end
*/
async requestWatchAsset (req, res, next, end) {
if (req.method === 'metamask_watchAsset') {
if (req.method === 'metamask_watchAsset' || req.method === 'wallet_watchAsset') {
const { type, options } = req.params
switch (type) {
case 'ERC20':
@ -371,22 +374,6 @@ class PreferencesController {
return Promise.resolve(label)
}
/**
* Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list.
*
* @param {string} _url The the new rpc url to add to the updated list
* @param {bool} remove Remove selected url
* @returns {Promise<void>} Promise resolves with undefined
*
*/
updateFrequentRpcList (_url, remove = false) {
return this.addToFrequentRpcList(_url, remove)
.then((rpcList) => {
this.store.updateState({ frequentRpcList: rpcList })
return Promise.resolve()
})
}
/**
* Setter for the `currentAccountTab` property
*
@ -402,35 +389,53 @@ class PreferencesController {
}
/**
* Returns an updated rpcList based on the passed url and the current list.
* The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
* end of the list. The current list is modified and returned as a promise.
* Adds custom RPC url to state.
*
* @param {string} _url The rpc url to add to the frequentRpcList.
* @param {bool} remove Remove selected url
* @returns {Promise<array>} The updated frequentRpcList.
* @param {string} url The RPC url to add to frequentRpcList.
* @param {number} chainId Optional chainId of the selected network.
* @param {string} ticker Optional ticker symbol of the selected network.
* @param {string} nickname Optional nickname of the selected network.
* @returns {Promise<array>} Promise resolving to updated frequentRpcList.
*
*/
addToFrequentRpcList (_url, remove = false) {
const rpcList = this.getFrequentRpcList()
const index = rpcList.findIndex((element) => { return element === _url })
addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') {
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
if (index !== -1) {
rpcList.splice(index, 1)
}
if (!remove && _url !== 'http://localhost:8545') {
rpcList.push(_url)
if (url !== 'http://localhost:8545') {
rpcList.push({ rpcUrl: url, chainId, ticker, nickname })
}
this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
}
/**
* Removes custom RPC url from state.
*
* @param {string} url The RPC url to remove from frequentRpcList.
* @returns {Promise<array>} Promise resolving to updated frequentRpcList.
*
*/
removeFromFrequentRpcList (url) {
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
if (index !== -1) {
rpcList.splice(index, 1)
}
this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
}
/**
* Getter for the `frequentRpcList` property.
* Getter for the `frequentRpcListDetail` property.
*
* @returns {array<string>} An array of one or two rpc urls.
* @returns {array<array>} An array of rpc urls.
*
*/
getFrequentRpcList () {
return this.store.getState().frequentRpcList
getFrequentRpcListDetail () {
return this.store.getState().frequentRpcListDetail
}
/**
@ -463,6 +468,33 @@ class PreferencesController {
getFeatureFlags () {
return this.store.getState().featureFlags
}
/**
* Updates the `preferences` property, which is an object. These are user-controlled features
* found in the settings page.
* @param {string} preference The preference to enable or disable.
* @param {boolean} value Indicates whether or not the preference should be enabled or disabled.
* @returns {Promise<object>} Promises a new object; the updated preferences object.
*/
setPreference (preference, value) {
const currentPreferences = this.getPreferences()
const updatedPreferences = {
...currentPreferences,
[preference]: value,
}
this.store.updateState({ preferences: updatedPreferences })
return Promise.resolve(updatedPreferences)
}
/**
* A getter for the `preferences` property
* @returns {object} A key-boolean map of user-selected preferences.
*/
getPreferences () {
return this.store.getState().preferences
}
//
// PRIVATE METHODS
//
@ -535,7 +567,7 @@ class PreferencesController {
}
const tokenOpts = { rawAddress, decimals, symbol, image }
this.addSuggestedERC20Asset(tokenOpts)
return this.showWatchAssetUi().then(() => {
return this.openPopup().then(() => {
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
return tokenAddresses.length > 0
})
@ -551,8 +583,8 @@ class PreferencesController {
*/
_validateERC20AssetParams (opts) {
const { rawAddress, symbol, decimals } = opts
if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
if (!rawAddress || !symbol || typeof decimals === 'undefined') throw new Error(`Cannot suggest token without address, symbol, and decimals`)
if (!(symbol.length < 7)) throw new Error(`Invalid symbol ${symbol} more than six characters`)
const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)

@ -0,0 +1,158 @@
const ObservableStore = require('obs-store')
/**
* A controller that services user-approved requests for a full Ethereum provider API
*/
class ProviderApprovalController {
/**
* Determines if caching is enabled
*/
caching = true
/**
* Creates a ProviderApprovalController
*
* @param {Object} [config] - Options to configure controller
*/
constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) {
this.approvedOrigins = {}
this.closePopup = closePopup
this.keyringController = keyringController
this.openPopup = openPopup
this.platform = platform
this.preferencesController = preferencesController
this.publicConfigStore = publicConfigStore
this.store = new ObservableStore()
if (platform && platform.addMessageListener) {
platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }) => {
switch (action) {
case 'init-provider-request':
this._handleProviderRequest(origin, siteTitle, siteImage, force)
break
case 'init-is-approved':
this._handleIsApproved(origin)
break
case 'init-is-unlocked':
this._handleIsUnlocked()
break
case 'init-privacy-request':
this._handlePrivacyRequest()
break
}
})
}
}
/**
* Called when a tab requests access to a full Ethereum provider API
*
* @param {string} origin - Origin of the window requesting full provider access
* @param {string} siteTitle - The title of the document requesting full provider access
* @param {string} siteImage - The icon of the window requesting full provider access
*/
_handleProviderRequest (origin, siteTitle, siteImage, force) {
this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] })
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) {
this.approveProviderRequest(origin)
return
}
this.openPopup && this.openPopup()
}
/**
* Called by a tab to determine if an origin has been approved in the past
*
* @param {string} origin - Origin of the window
*/
_handleIsApproved (origin) {
this.platform && this.platform.sendMessage({
action: 'answer-is-approved',
isApproved: this.approvedOrigins[origin] && this.caching,
caching: this.caching,
}, { active: true })
}
/**
* Called by a tab to determine if MetaMask is currently locked or unlocked
*/
_handleIsUnlocked () {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { active: true })
}
/**
* Called to check privacy mode; if privacy mode is off, this will automatically enable the provider (legacy behavior)
*/
_handlePrivacyRequest () {
const privacyMode = this.preferencesController.getFeatureFlags().privacyMode
if (!privacyMode) {
this.platform && this.platform.sendMessage({
action: 'approve-legacy-provider-request',
selectedAddress: this.publicConfigStore.getState().selectedAddress,
}, { active: true })
this.publicConfigStore.emit('update', this.publicConfigStore.getState())
}
}
/**
* Called when a user approves access to a full Ethereum provider API
*
* @param {string} origin - Origin of the target window to approve provider access
*/
approveProviderRequest (origin) {
this.closePopup && this.closePopup()
const requests = this.store.getState().providerRequests || []
this.platform && this.platform.sendMessage({
action: 'approve-provider-request',
selectedAddress: this.publicConfigStore.getState().selectedAddress,
}, { active: true })
this.publicConfigStore.emit('update', this.publicConfigStore.getState())
const providerRequests = requests.filter(request => request.origin !== origin)
this.store.updateState({ providerRequests })
this.approvedOrigins[origin] = true
}
/**
* Called when a tab rejects access to a full Ethereum provider API
*
* @param {string} origin - Origin of the target window to reject provider access
*/
rejectProviderRequest (origin) {
this.closePopup && this.closePopup()
const requests = this.store.getState().providerRequests || []
this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { active: true })
const providerRequests = requests.filter(request => request.origin !== origin)
this.store.updateState({ providerRequests })
delete this.approvedOrigins[origin]
}
/**
* Clears any cached approvals for user-approved origins
*/
clearApprovedOrigins () {
this.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) {
const privacyMode = this.preferencesController.getFeatureFlags().privacyMode
return !privacyMode || this.approvedOrigins[origin]
}
/**
* Tells all tabs that MetaMask is now locked. This is primarily used to set
* internal flags in the contentscript and inpage script.
*/
setLocked () {
this.platform.sendMessage({ action: 'metamask-set-locked' })
}
}
module.exports = ProviderApprovalController

@ -1,5 +1,5 @@
const ObservableStore = require('obs-store')
const { warn } = require('loglevel')
const log = require('loglevel')
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
@ -26,8 +26,11 @@ class TokenRatesController {
async updateExchangeRates () {
if (!this.isActive) { return }
const contractExchangeRates = {}
for (const i in this._tokens) {
const address = this._tokens[i].address
// copy array to ensure its not modified during iteration
const tokens = this._tokens.slice()
for (const token of tokens) {
if (!token) return log.error(`TokenRatesController - invalid tokens state:\n${JSON.stringify(tokens, null, 2)}`)
const address = token.address
contractExchangeRates[address] = await this.fetchExchangeRate(address)
}
this.store.putState({ contractExchangeRates })
@ -44,7 +47,7 @@ class TokenRatesController {
const json = await response.json()
return json && json.length ? json[0].averagePrice : 0
} catch (error) {
warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
log.warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
return 0
}
}

@ -366,7 +366,40 @@ class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusSubmitted(txId)
}
confirmTransaction (txId) {
/**
* Sets the status of the transaction to confirmed and sets the status of nonce duplicates as
* dropped if the txParams have data it will fetch the txReceipt
* @param {number} txId - The tx's ID
* @returns {Promise<void>}
*/
async confirmTransaction (txId) {
// get the txReceipt before marking the transaction confirmed
// to ensure the receipt is gotten before the ui revives the tx
const txMeta = this.txStateManager.getTx(txId)
if (!txMeta) {
return
}
try {
const txReceipt = await this.query.getTransactionReceipt(txMeta.hash)
// It seems that sometimes the numerical values being returned from
// this.query.getTransactionReceipt are BN instances and not strings.
const gasUsed = typeof txReceipt.gasUsed !== 'string'
? txReceipt.gasUsed.toString(16)
: txReceipt.gasUsed
txMeta.txReceipt = {
...txReceipt,
gasUsed,
}
this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt')
} catch (err) {
log.error(err)
}
this.txStateManager.setTxStatusConfirmed(txId)
this._markNonceDuplicatesDropped(txId)
}

@ -7,6 +7,8 @@ const {
const { addHexPrefix } = require('ethereumjs-util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/constants/error-keys'
/**
tx-gas-utils are gas utility methods for Transaction manager
its passed ethquery
@ -32,6 +34,7 @@ class TxGasUtil {
} catch (err) {
txMeta.simulationFails = {
reason: err.message,
errorKey: err.errorKey,
}
return txMeta
}
@ -56,24 +59,38 @@ class TxGasUtil {
return txParams.gas
}
// if recipient has no code, gas is 21k max:
const recipient = txParams.to
const hasRecipient = Boolean(recipient)
let code
if (recipient) code = await this.query.getCode(recipient)
if (hasRecipient && (!code || code === '0x')) {
txParams.gas = SIMPLE_GAS_COST
txMeta.simpleSend = true // Prevents buffer addition
return SIMPLE_GAS_COST
// see if we can set the gas based on the recipient
if (hasRecipient) {
const code = await this.query.getCode(recipient)
// For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0'
const codeIsEmpty = !code || code === '0x' || code === '0x0'
if (codeIsEmpty) {
// if there's data in the params, but there's no contract code, it's not a valid transaction
if (txParams.data) {
const err = new Error('TxGasUtil - Trying to call a function on a non-contract address')
// set error key so ui can display localized error message
err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY
throw err
}
// This is a standard ether simple send, gas requirement is exactly 21k
txParams.gas = SIMPLE_GAS_COST
// prevents buffer addition
txMeta.simpleSend = true
return SIMPLE_GAS_COST
}
}
// if not, fall back to block gasLimit
// fallback to block gasLimit
const blockGasLimitBN = hexToBn(blockGasLimitHex)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
txParams.gas = bnToHex(saferGasLimitBN)
// run tx
// estimate tx gas requirements
return await this.query.estimateGas(txParams)
}

@ -400,6 +400,11 @@ class TransactionStateManager extends EventEmitter {
*/
_setTxStatus (txId, status) {
const txMeta = this.getTx(txId)
if (!txMeta) {
return
}
txMeta.status = status
setTimeout(() => {
try {

@ -6,14 +6,36 @@ const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('metamask-inpage-provider')
let isEnabled = false
let warned = false
let providerHandle
let isApprovedHandle
let isUnlockedHandle
restoreContextAfterImports()
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
console.warn('ATTENTION: In an effort to improve user privacy, MetaMask will ' +
'stop exposing user accounts to dapps by default beginning November 2nd, 2018. ' +
'Dapps should call provider.enable() in order to view and use accounts. Please see ' +
'https://bit.ly/2QQHXvF for complete information and up-to-date example code.')
console.warn('ATTENTION: In an effort to improve user privacy, MetaMask ' +
'stopped exposing user accounts to dapps if "privacy mode" is enabled on ' +
'November 2nd, 2018. Dapps should now call provider.enable() in order to view and use ' +
'accounts. Please see https://bit.ly/2QQHXvF for complete information and up-to-date ' +
'example code.')
/**
* Adds a postMessage listener for a specific message type
*
* @param {string} messageType - postMessage type to listen for
* @param {Function} handler - event handler
* @param {boolean} remove - removes this handler after being triggered
*/
function onMessage(messageType, handler, remove) {
window.addEventListener('message', function ({ data }) {
if (!data || data.type !== messageType) { return }
remove && window.removeEventListener('message', handler)
handler.apply(window, arguments)
})
}
//
// setup plugin communication
@ -28,23 +50,98 @@ var metamaskStream = new LocalMessageDuplexStream({
// compose the inpage provider
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
// Augment the provider with its enable method
inpageProvider.enable = function (options = {}) {
// set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100)
// set up a listener for when MetaMask is locked
onMessage('metamasksetlocked', () => { isEnabled = false })
// set up a listener for privacy mode responses
onMessage('ethereumproviderlegacy', ({ data: { selectedAddress } }) => {
isEnabled = true
inpageProvider.publicConfigStore.updateState({ selectedAddress })
}, true)
// augment the provider with its enable method
inpageProvider.enable = function ({ force } = {}) {
return new Promise((resolve, reject) => {
if (options.mockRejection) {
reject('User rejected account access')
} else {
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
if (error) {
reject(error)
} else {
resolve(response.result)
}
})
providerHandle = ({ data: { error, selectedAddress } }) => {
if (typeof error !== 'undefined') {
reject(error)
} else {
window.removeEventListener('message', providerHandle)
inpageProvider.publicConfigStore.updateState({ selectedAddress })
// wait for the background to update with an account
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
if (error) {
reject(error)
} else {
isEnabled = true
resolve(response.result)
}
})
}
}
onMessage('ethereumprovider', providerHandle, true)
window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*')
})
}
// add metamask-specific convenience methods
inpageProvider._metamask = new Proxy({
/**
* Determines if this domain is currently enabled
*
* @returns {boolean} - true if this domain is currently enabled
*/
isEnabled: function () {
return isEnabled
},
/**
* Determines if this domain has been previously approved
*
* @returns {Promise<boolean>} - Promise resolving to true if this domain has been previously approved
*/
isApproved: function() {
return new Promise((resolve) => {
isApprovedHandle = ({ data: { caching, isApproved } }) => {
if (caching) {
resolve(!!isApproved)
} else {
resolve(false)
}
}
onMessage('ethereumisapproved', isApprovedHandle, true)
window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*')
})
},
/**
* Determines if MetaMask is unlocked by the user
*
* @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked
*/
isUnlocked: function () {
return new Promise((resolve) => {
isUnlockedHandle = ({ data: { isUnlocked } }) => {
resolve(!!isUnlocked)
}
onMessage('metamaskisunlocked', isUnlockedHandle, true)
window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*')
})
},
}, {
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 with drizzle
const proxiedInpageProvider = new Proxy(inpageProvider, {
@ -55,6 +152,19 @@ const proxiedInpageProvider = new Proxy(inpageProvider, {
window.ethereum = proxiedInpageProvider
// detect eth_requestAccounts and pipe to enable for now
function detectAccountRequest(method) {
const originalMethod = inpageProvider[method]
inpageProvider[method] = function ({ method }) {
if (method === 'eth_requestAccounts') {
return window.ethereum.enable()
}
return originalMethod.apply(this, arguments)
}
}
detectAccountRequest('send')
detectAccountRequest('sendAsync')
//
// setup web3
//

@ -0,0 +1,54 @@
const namehash = require('eth-ens-namehash')
const multihash = require('multihashes')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')
const registrarAbi = require('./contracts/registrar')
const resolverAbi = require('./contracts/resolver')
module.exports = resolveEnsToIpfsContentId
async function resolveEnsToIpfsContentId ({ provider, name }) {
const eth = new Eth(provider)
const hash = namehash.hash(name)
const contract = new EthContract(eth)
// lookup registrar
const chainId = Number.parseInt(await eth.net_version(), 10)
const registrarAddress = getRegistrarForChainId(chainId)
if (!registrarAddress) {
throw new Error(`EnsIpfsResolver - no known ens-ipfs registrar for chainId "${chainId}"`)
}
const Registrar = contract(registrarAbi).at(registrarAddress)
// lookup resolver
const resolverLookupResult = await Registrar.resolver(hash)
const resolverAddress = resolverLookupResult[0]
if (hexValueIsEmpty(resolverAddress)) {
throw new Error(`EnsIpfsResolver - no resolver found for name "${name}"`)
}
const Resolver = contract(resolverAbi).at(resolverAddress)
// lookup content id
const contentLookupResult = await Resolver.content(hash)
const contentHash = contentLookupResult[0]
if (hexValueIsEmpty(contentHash)) {
throw new Error(`EnsIpfsResolver - no content ID found for name "${name}"`)
}
const nonPrefixedHex = contentHash.slice(2)
const buffer = multihash.fromHexString(nonPrefixedHex)
const contentId = multihash.toB58String(multihash.encode(buffer, 'sha2-256'))
return contentId
}
function hexValueIsEmpty(value) {
return [undefined, null, '0x', '0x0', '0x0000000000000000000000000000000000000000000000000000000000000000'].includes(value)
}
function getRegistrarForChainId (chainId) {
switch (chainId) {
// mainnet
case 1:
return '0x314159265dd8dbb310642f98f50c066173c1259b'
// ropsten
case 3:
return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
}
}

@ -0,0 +1,63 @@
const urlUtil = require('url')
const extension = require('extensionizer')
const resolveEnsToIpfsContentId = require('./resolver.js')
const supportedTopLevelDomains = ['eth']
module.exports = setupEnsIpfsResolver
function setupEnsIpfsResolver({ provider }) {
// install listener
const urlPatterns = supportedTopLevelDomains.map(tld => `*://*.${tld}/*`)
extension.webRequest.onErrorOccurred.addListener(webRequestDidFail, { urls: urlPatterns })
// return api object
return {
// uninstall listener
remove () {
extension.webRequest.onErrorOccurred.removeListener(webRequestDidFail)
},
}
async function webRequestDidFail (details) {
const { tabId, url } = details
// ignore requests that are not associated with tabs
if (tabId === -1) return
// parse ens name
const urlData = urlUtil.parse(url)
const { hostname: name, path, search } = urlData
const domainParts = name.split('.')
const topLevelDomain = domainParts[domainParts.length - 1]
// if unsupported TLD, abort
if (!supportedTopLevelDomains.includes(topLevelDomain)) return
// otherwise attempt resolve
attemptResolve({ tabId, name, path, search })
}
async function attemptResolve({ tabId, name, path, search }) {
extension.tabs.update(tabId, { url: `loading.html` })
try {
const ipfsContentId = await resolveEnsToIpfsContentId({ provider, name })
let url = `https://gateway.ipfs.io/ipfs/${ipfsContentId}${path}${search || ''}`
try {
// check if ipfs gateway has result
const response = await fetch(url, { method: 'HEAD' })
// if failure, redirect to 404 page
if (response.status !== 200) {
extension.tabs.update(tabId, { url: '404.html' })
return
}
// otherwise redirect to the correct page
extension.tabs.update(tabId, { url })
} catch (err) {
console.warn(err)
// if HEAD fetch failed, redirect so user can see relevant error page
extension.tabs.update(tabId, { url })
}
} catch (err) {
console.warn(err)
extension.tabs.update(tabId, { url: `error.html?name=${name}` })
}
}
}

@ -1,46 +0,0 @@
const extension = require('extensionizer')
const resolver = require('./resolver.js')
module.exports = function (provider) {
function ipfsContent (details) {
const name = details.url.substring(7, details.url.length - 1)
let clearTime = null
if (/^.+\.eth$/.test(name) === false) return
extension.tabs.query({active: true}, tab => {
extension.tabs.update(tab.id, { url: 'loading.html' })
clearTime = setTimeout(() => {
return extension.tabs.update(tab.id, { url: '404.html' })
}, 60000)
resolver.resolve(name, provider).then(ipfsHash => {
clearTimeout(clearTime)
let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash
return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => {
if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' })
extension.tabs.update(tab.id, { url: url })
})
.catch(err => {
url = 'https://ipfs.infura.io/ipfs/' + ipfsHash
extension.tabs.update(tab.id, {url: url})
return err
})
})
.catch(err => {
clearTimeout(clearTime)
const url = err === 'unsupport' ? 'unsupport' : 'error'
extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`})
})
})
return { cancel: true }
}
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/'], types: ['main_frame']})
return {
remove () {
extension.webRequest.onErrorOccurred.removeListener(ipfsContent)
},
}
}

@ -7,10 +7,10 @@ module.exports = reportFailedTxToSentry
// for sending to sentry
//
function reportFailedTxToSentry ({ raven, txMeta }) {
function reportFailedTxToSentry ({ sentry, txMeta }) {
const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message)
raven.captureMessage(errorMessage, {
sentry.captureMessage(errorMessage, {
// "extra" key is required by Sentry
extra: txMeta,
extra: { txMeta },
})
}

@ -1,71 +0,0 @@
const namehash = require('eth-ens-namehash')
const multihash = require('multihashes')
const HttpProvider = require('ethjs-provider-http')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')
const registrarAbi = require('./contracts/registrar')
const resolverAbi = require('./contracts/resolver')
function ens (name, provider) {
const eth = new Eth(new HttpProvider(getProvider(provider.type)))
const hash = namehash.hash(name)
const contract = new EthContract(eth)
const Registrar = contract(registrarAbi).at(getRegistrar(provider.type))
return new Promise((resolve, reject) => {
if (provider.type === 'mainnet' || provider.type === 'ropsten') {
Registrar.resolver(hash).then((address) => {
if (address === '0x0000000000000000000000000000000000000000') {
reject(null)
} else {
const Resolver = contract(resolverAbi).at(address['0'])
return Resolver.content(hash)
}
}).then((contentHash) => {
if (contentHash['0'] === '0x0000000000000000000000000000000000000000000000000000000000000000') reject(null)
if (contentHash.ret !== '0x') {
const hex = contentHash['0'].substring(2)
const buf = multihash.fromHexString(hex)
resolve(multihash.toB58String(multihash.encode(buf, 'sha2-256')))
} else {
reject(null)
}
})
} else {
return reject('unsupport')
}
})
}
function getProvider (type) {
switch (type) {
case 'mainnet':
return 'https://mainnet.infura.io/'
case 'ropsten':
return 'https://ropsten.infura.io/'
default:
return 'http://localhost:8545/'
}
}
function getRegistrar (type) {
switch (type) {
case 'mainnet':
return '0x314159265dd8dbb310642f98f50c066173c1259b'
case 'ropsten':
return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
default:
return '0x0000000000000000000000000000000000000000'
}
}
module.exports.resolve = function (name, provider) {
const path = name.split('.')
const topLevelDomain = path[path.length - 1]
if (topLevelDomain === 'eth' || topLevelDomain === 'test') {
return ens(name, provider)
} else {
return new Promise((resolve, reject) => {
reject(null)
})
}
}

@ -0,0 +1,36 @@
module.exports = setupFetchDebugging
//
// This is a utility to help resolve cases where `window.fetch` throws a
// `TypeError: Failed to Fetch` without any stack or context for the request
// https://github.com/getsentry/sentry-javascript/pull/1293
//
function setupFetchDebugging() {
if (!global.fetch) return
const originalFetch = global.fetch
global.fetch = wrappedFetch
async function wrappedFetch(...args) {
const initialStack = getCurrentStack()
try {
return await originalFetch.call(window, ...args)
} catch (err) {
if (!err.stack) {
console.warn('FetchDebugger - fetch encountered an Error without a stack', err)
console.warn('FetchDebugger - overriding stack to point of original call')
err.stack = initialStack
}
throw err
}
}
}
function getCurrentStack() {
try {
throw new Error('Fake error for generating stack trace')
} catch (err) {
return err.stack
}
}

@ -1,58 +1,55 @@
const Raven = require('raven-js')
const Sentry = require('@sentry/browser')
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const extractEthjsErrorMessage = require('./extractEthjsErrorMessage')
const PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
module.exports = setupRaven
module.exports = setupSentry
// Setup raven / sentry remote error reporting
function setupRaven (opts) {
const { release } = opts
let ravenTarget
// Setup sentry remote error reporting
function setupSentry (opts) {
const { release, getState } = opts
let sentryTarget
// detect brave
const isBrave = Boolean(window.chrome.ipcRenderer)
if (METAMASK_DEBUG) {
console.log('Setting up Sentry Remote Error Reporting: DEV')
ravenTarget = DEV
console.log('Setting up Sentry Remote Error Reporting: SENTRY_DSN_DEV')
sentryTarget = SENTRY_DSN_DEV
} else {
console.log('Setting up Sentry Remote Error Reporting: PROD')
ravenTarget = PROD
console.log('Setting up Sentry Remote Error Reporting: SENTRY_DSN_PROD')
sentryTarget = SENTRY_DSN_PROD
}
const client = Raven.config(ravenTarget, {
Sentry.init({
dsn: sentryTarget,
debug: METAMASK_DEBUG,
release,
transport: function (opts) {
opts.data.extra.isBrave = isBrave
const report = opts.data
beforeSend: (report) => rewriteReport(report),
})
try {
// handle error-like non-error exceptions
rewriteErrorLikeExceptions(report)
// simplify certain complex error messages (e.g. Ethjs)
simplifyErrorMessages(report)
// modify report urls
rewriteReportUrls(report)
} catch (err) {
console.warn(err)
}
// make request normally
client._makeRequest(opts)
},
Sentry.configureScope(scope => {
scope.setExtra('isBrave', isBrave)
})
client.install()
return Raven
}
function rewriteReport(report) {
try {
// simplify certain complex error messages (e.g. Ethjs)
simplifyErrorMessages(report)
// modify report urls
rewriteReportUrls(report)
// append app state
if (getState) {
const appState = getState()
report.extra.appState = appState
}
} catch (err) {
console.warn(err)
}
return report
}
function rewriteErrorLikeExceptions (report) {
// handle errors that lost their error-ness in serialization (e.g. dnode)
rewriteErrorMessages(report, (errorMessage) => {
if (!errorMessage.includes('Non-Error exception captured with keys:')) return errorMessage
if (!(report.extra && report.extra.__serialized__ && report.extra.__serialized__.message)) return errorMessage
return `Non-Error Exception: ${report.extra.__serialized__.message}`
})
return Sentry
}
function simplifyErrorMessages (report) {

@ -37,6 +37,7 @@ const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
const DetectTokensController = require('./controllers/detect-tokens')
const ProviderApprovalController = require('./controllers/provider-approval')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
@ -89,7 +90,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
showWatchAssetUi: opts.showWatchAssetUi,
openPopup: opts.openPopup,
network: this.networkController,
})
@ -129,6 +130,7 @@ module.exports = class MetamaskController extends EventEmitter {
provider: this.provider,
blockTracker: this.blockTracker,
})
// start and stop polling for balances based on activeControllerConnections
this.on('controllerConnectionChanged', (activeControllerConnections) => {
if (activeControllerConnections > 0) {
@ -138,6 +140,11 @@ module.exports = class MetamaskController extends EventEmitter {
}
})
// ensure accountTracker updates balances after network change
this.networkController.on('networkDidChange', () => {
this.accountTracker._updateAccounts()
})
// key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
this.keyringController = new KeyringController({
@ -191,6 +198,8 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.networkController.on('networkDidChange', () => {
this.balancesController.updateAllBalances()
var currentCurrency = this.currencyController.getCurrentCurrency()
this.setCurrentCurrency(currentCurrency, function() {})
})
this.balancesController.updateAllBalances()
@ -211,6 +220,15 @@ module.exports = class MetamaskController extends EventEmitter {
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
this.publicConfigStore = this.initPublicConfigStore()
this.providerApprovalController = new ProviderApprovalController({
closePopup: opts.closePopup,
keyringController: this.keyringController,
openPopup: opts.openPopup,
platform: opts.platform,
preferencesController: this.preferencesController,
publicConfigStore: this.publicConfigStore,
})
this.store.updateStructure({
TransactionController: this.txController.store,
KeyringController: this.keyringController.store,
@ -240,6 +258,7 @@ module.exports = class MetamaskController extends EventEmitter {
NoticeController: this.noticeController.memStore,
ShapeshiftController: this.shapeshiftController.store,
InfuraController: this.infuraController.store,
ProviderApprovalController: this.providerApprovalController.store,
})
this.memStore.subscribe(this.sendUpdate.bind(this))
}
@ -255,7 +274,11 @@ module.exports = class MetamaskController extends EventEmitter {
},
version,
// account mgmt
getAccounts: async () => {
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
@ -269,6 +292,8 @@ module.exports = class MetamaskController extends EventEmitter {
processTransaction: this.newUnapprovedTransaction.bind(this),
// msg signing
processEthSignMessage: this.newUnsignedMessage.bind(this),
processTypedMessage: this.newUnsignedTypedMessage.bind(this),
processTypedMessageV3: this.newUnsignedTypedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
getPendingNonce: this.getPendingNonce.bind(this),
}
@ -339,6 +364,7 @@ module.exports = class MetamaskController extends EventEmitter {
const noticeController = this.noticeController
const addressBookController = this.addressBookController
const networkController = this.networkController
const providerApprovalController = this.providerApprovalController
return {
// etc
@ -387,6 +413,7 @@ module.exports = class MetamaskController extends EventEmitter {
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
setPreference: nodeify(preferencesController.setPreference, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
@ -395,7 +422,7 @@ module.exports = class MetamaskController extends EventEmitter {
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
// KeyringController
setLocked: nodeify(keyringController.setLocked, keyringController),
setLocked: nodeify(this.setLocked, this),
createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this),
createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this),
addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController),
@ -426,6 +453,10 @@ module.exports = class MetamaskController extends EventEmitter {
// notices
checkNotices: noticeController.updateNoticesList.bind(noticeController),
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController),
clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController),
rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController),
}
}
@ -971,8 +1002,8 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @param {Function} cb - The callback function, called with the signature.
*/
newUnsignedTypedMessage (msgParams, req) {
const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req)
newUnsignedTypedMessage (msgParams, req, version) {
const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req, version)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
return promise
@ -1266,10 +1297,6 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(subscriptionManager.middleware)
// watch asset
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
// sign typed data middleware
engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this))
engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this))
engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3', true).bind(this))
// forward to metamask primary provider
engine.push(createProviderMiddleware({ provider }))
@ -1405,10 +1432,13 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - A callback function returning currency info.
*/
setCurrentCurrency (currencyCode, cb) {
const { ticker } = this.networkController.getNetworkConfig()
try {
this.currencyController.setNativeCurrency(ticker)
this.currencyController.setCurrentCurrency(currencyCode)
this.currencyController.updateConversionRate()
const data = {
nativeCurrency: ticker || 'ETH',
conversionRate: this.currencyController.getConversionRate(),
currentCurrency: this.currencyController.getCurrentCurrency(),
conversionDate: this.currencyController.getConversionDate(),
@ -1447,11 +1477,14 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A method for selecting a custom URL for an ethereum RPC provider.
* @param {string} rpcTarget - A URL for a valid Ethereum RPC API.
* @param {number} chainId - The chainId of the selected network.
* @param {string} ticker - The ticker symbol of the selected network.
* @param {string} nickname - Optional nickname of the selected network.
* @returns {Promise<String>} - The RPC Target URL confirmed.
*/
async setCustomRpc (rpcTarget) {
this.networkController.setRpcTarget(rpcTarget)
await this.preferencesController.updateFrequentRpcList(rpcTarget)
async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') {
this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname)
await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname)
return rpcTarget
}
@ -1460,7 +1493,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {string} rpcTarget - A RPC URL to delete.
*/
async delCustomRpc (rpcTarget) {
await this.preferencesController.updateFrequentRpcList(rpcTarget, true)
await this.preferencesController.removeFromFrequentRpcList(rpcTarget)
}
/**
@ -1535,27 +1568,6 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} - next
* @param {Function} - end
*/
createTypedDataMiddleware (methodName, version, reverse) {
return async (req, res, next, end) => {
const { method, params } = req
if (method === methodName) {
const promise = this.typedMessageManager.addUnapprovedMessageAsync({
data: reverse ? params[1] : params[0],
from: reverse ? params[0] : params[1],
}, req, version)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
try {
res.result = await promise
end()
} catch (error) {
end(error)
}
} else {
next()
}
}
}
/**
* Adds a domain to the {@link BlacklistController} whitelist
@ -1564,4 +1576,12 @@ module.exports = class MetamaskController extends EventEmitter {
whitelistPhishingDomain (hostname) {
return this.blacklistController.whitelistDomain(hostname)
}
/**
* Locks MetaMask
*/
setLocked() {
this.providerApprovalController.setLocked()
return this.keyringController.setLocked()
}
}

@ -57,6 +57,18 @@ class ExtensionPlatform {
}
}
addMessageListener (cb) {
extension.runtime.onMessage.addListener(cb)
}
sendMessage (message, query = {}) {
extension.tabs.query(query, tabs => {
tabs.forEach(tab => {
extension.tabs.sendMessage(tab.id, message)
})
})
}
_showConfirmedTransaction (txMeta) {
this._subscribeToNotificationClicked()

@ -9,7 +9,7 @@ const extension = require('extensionizer')
const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
const setupRaven = require('./lib/setupRaven')
const setupSentry = require('./lib/setupSentry')
const log = require('loglevel')
start().catch(log.error)
@ -21,7 +21,17 @@ async function start () {
// setup sentry error reporting
const release = global.platform.getVersion()
setupRaven({ release })
setupSentry({ release, getState })
// provide app state to append to error logs
function getState() {
// get app state
const state = window.getCleanAppState()
// remove unnecessary data
delete state.localeMessages
delete state.metamask.recentBlocks
// return state to be added to request
return state
}
// inject css
// const css = MetaMaskUiCss()

@ -107,7 +107,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

@ -150,7 +150,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

@ -108,7 +108,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

@ -37,7 +37,10 @@
"shapeShiftTxList": [],
"lostAccounts": [],
"tokens": [],
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

@ -109,7 +109,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

@ -102,7 +102,10 @@
"shapeShiftTxList": [{"depositAddress":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke","depositType":"BTC","key":"shapeshift","response":{"status":"no_deposits","address":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke"},"time":1522377459106}],
"lostAccounts": [],
"send": {},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

@ -30,7 +30,7 @@ if (specifiedLocale) {
}
function verifyLocale (localeMeta) {
function verifyLocale ({ localeMeta }) {
const localeCode = localeMeta.code
const localeName = localeMeta.name
let targetLocale, englishLocale

@ -26,6 +26,16 @@ const pify = require('pify')
const gulpMultiProcess = require('gulp-multi-process')
const endOfStream = pify(require('end-of-stream'))
const packageJSON = require('./package.json')
const dependencies = Object.keys(packageJSON && packageJSON.dependencies || {})
const materialUIDependencies = ['@material-ui/core']
const reactDepenendencies = dependencies.filter(dep => dep.match(/react/))
const uiDependenciesToBundle = [
...materialUIDependencies,
...reactDepenendencies,
]
function gulpParallel (...args) {
return function spawnGulpChildProcess (cb) {
return gulpMultiProcess(args, cb, true)
@ -279,11 +289,32 @@ const buildJsFiles = [
]
// bundle tasks
createTasksForBuildJsUIDeps({ dependenciesToBundle: uiDependenciesToBundle, filename: 'libs' })
createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:extension:js', devMode: true })
createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' })
createTasksForBuildJsMascara({ taskPrefix: 'build:mascara:js' })
createTasksForBuildJsMascara({ taskPrefix: 'dev:mascara:js', devMode: true })
function createTasksForBuildJsUIDeps ({ dependenciesToBundle, filename }) {
const destinations = browserPlatforms.map(platform => `./dist/${platform}`)
const bundleTaskOpts = Object.assign({
buildSourceMaps: true,
sourceMapDir: '../sourcemaps',
minifyBuild: true,
devMode: false,
})
gulp.task('build:extension:js:uideps', bundleTask(Object.assign({
label: filename,
filename: `${filename}.js`,
destinations,
buildLib: true,
dependenciesToBundle: uiDependenciesToBundle,
}, bundleTaskOpts)))
}
function createTasksForBuildJsExtension ({ buildJsFiles, taskPrefix, devMode, bundleTaskOpts = {} }) {
// inpage must be built before all other scripts:
const rootDir = './app/scripts'
@ -326,6 +357,7 @@ function createTasksForBuildJs ({ rootDir, taskPrefix, bundleTaskOpts, destinati
label: jsFile,
filename: `${jsFile}.js`,
filepath: `${rootDir}/${jsFile}.js`,
externalDependencies: jsFile === 'ui' && !bundleTaskOpts.devMode && uiDependenciesToBundle,
destinations,
}, bundleTaskOpts)))
})
@ -402,6 +434,7 @@ gulp.task('build',
'clean',
'build:scss',
gulpParallel(
'build:extension:js:uideps',
'build:extension:js',
'build:mascara:js',
'copy'
@ -450,19 +483,32 @@ function zipTask (target) {
function generateBundler (opts, performBundle) {
const browserifyOpts = assign({}, watchify.args, {
entries: [opts.filepath],
plugin: 'browserify-derequire',
debug: opts.buildSourceMaps,
fullPaths: opts.buildWithFullPaths,
})
if (!opts.buildLib) {
browserifyOpts['entries'] = [opts.filepath]
}
let bundler = browserify(browserifyOpts)
if (opts.buildLib) {
bundler = bundler.require(opts.dependenciesToBundle)
}
if (opts.externalDependencies) {
bundler = bundler.external(opts.externalDependencies)
}
// inject variables into bundle
bundler.transform(envify({
METAMASK_DEBUG: opts.devMode,
NODE_ENV: opts.devMode ? 'development' : 'production',
}))
}), {
global: true,
})
if (opts.watch) {
bundler = watchify(bundler)

@ -33,6 +33,7 @@ const BuyView = require('./components/buy-button-subview')
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const ProviderApproval = require('./provider-approval')
module.exports = connect(mapStateToProps)(App)
@ -49,6 +50,7 @@ function mapStateToProps (state) {
noActiveNotices,
seedWords,
featureFlags,
providerRequests,
} = state.metamask
const selected = address || Object.keys(accounts)[0]
@ -73,8 +75,9 @@ function mapStateToProps (state) {
forgottenPassword: state.appState.forgottenPassword,
nextUnreadNotice: state.metamask.nextUnreadNotice,
lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
featureFlags,
providerRequests,
suggestedTokens: state.metamask.suggestedTokens,
// state needed to get account dropdown temporarily rendering from app bar
@ -147,7 +150,7 @@ App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork,
App.prototype.renderPrimary = function () {
log.debug('rendering primary')
var props = this.props
const {isMascara, isOnboarding} = props
const {isMascara, isOnboarding, providerRequests} = props
if (isMascara && isOnboarding) {
return h(MascaraFirstTime)
@ -215,6 +218,11 @@ App.prototype.renderPrimary = function () {
return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
}
if (providerRequests && providerRequests.length > 0) {
log.debug('rendering provider API approval screen')
return h(ProviderApproval, { origin: providerRequests[0].origin })
}
// show current view
switch (props.currentView.name) {

@ -17,7 +17,7 @@ module.exports = class AppBar extends Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
frequentRpcList: PropTypes.array.isRequired,
frequentRpcListDetail: PropTypes.array.isRequired,
isMascara: PropTypes.bool.isRequired,
isOnboarding: PropTypes.bool.isRequired,
identities: PropTypes.any.isRequired,
@ -196,7 +196,7 @@ module.exports = class AppBar extends Component {
renderNetworkDropdown () {
const {
dispatch,
frequentRpcList: rpcList,
frequentRpcListDetail: rpcList,
provider,
} = this.props
const {
@ -321,8 +321,8 @@ module.exports = class AppBar extends Component {
])
}
renderCustomOption ({ rpcTarget, type }) {
const {dispatch} = this.props
renderCustomOption ({ rpcTarget, type, ticker }) {
const {dispatch, network} = this.props
if (type !== 'rpc') {
return null
@ -340,7 +340,7 @@ module.exports = class AppBar extends Component {
default:
return h(DropdownMenuItem, {
key: rpcTarget,
onClick: () => dispatch(actions.setRpcTarget(rpcTarget)),
onClick: () => dispatch(actions.setRpcTarget(rpcTarget, network, ticker)),
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
@ -354,7 +354,8 @@ module.exports = class AppBar extends Component {
const {dispatch} = this.props
const reversedRpcList = rpcList.slice().reverse()
return reversedRpcList.map((rpc) => {
return reversedRpcList.map((entry) => {
const rpc = entry.rpcUrl
const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) {
@ -363,7 +364,7 @@ module.exports = class AppBar extends Component {
return h(DropdownMenuItem, {
key: `common${rpc}`,
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
onClick: () => dispatch(actions.setRpcTarget(rpc)),
onClick: () => dispatch(actions.setRpcTarget(rpc, entry.chainId, entry.ticker)),
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,

@ -1,12 +1,18 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
const FiatValue = require('./fiat-value.js')
module.exports = EthBalanceComponent
module.exports = connect(mapStateToProps)(EthBalanceComponent)
function mapStateToProps (state) {
return {
ticker: state.metamask.ticker,
}
}
inherits(EthBalanceComponent, Component)
function EthBalanceComponent () {
@ -16,9 +22,10 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
const { ticker } = props
var style = props.style
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
value = value ? formatBalance(value, 6, needsParse) : '...'
value = value ? formatBalance(value, 6, needsParse, ticker) : '...'
var width = props.width
return (

@ -1,12 +1,18 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
const FiatValue = require('./fiat-value.js')
module.exports = EthBalanceComponent
module.exports = connect(mapStateToProps)(EthBalanceComponent)
function mapStateToProps (state) {
return {
ticker: state.metamask.ticker,
}
}
inherits(EthBalanceComponent, Component)
function EthBalanceComponent () {
@ -16,9 +22,9 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
const { style, width } = props
const { ticker, style, width } = props
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
value = value ? formatBalance(value, 6, needsParse) : '...'
value = value ? formatBalance(value, 6, needsParse, ticker) : '...'
return (

@ -1,4 +1,5 @@
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const actions = require('../../../ui/app/actions')
@ -19,7 +20,9 @@ const BNInput = require('./bn-as-decimal-input')
const MIN_GAS_PRICE_BN = new BN('0')
const MIN_GAS_LIMIT_BN = new BN('21000')
module.exports = PendingTx
module.exports = connect()(PendingTx)
inherits(PendingTx, Component)
function PendingTx () {
Component.call(this)
@ -445,7 +448,8 @@ PendingTx.prototype.onSubmit = function (event) {
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) {
const validGasParams = this.verifyGasParams()
if (valid && validGasParams) {
this.props.sendTransaction(txMeta, event)
} else {
this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
@ -488,8 +492,12 @@ PendingTx.prototype.verifyGasParams = function () {
)
}
PendingTx.prototype._notZeroOrEmptyString = function (obj) {
return obj !== '' && obj !== '0x0'
PendingTx.prototype._notZeroOrEmptyString = function (value) {
// allow undefined values
if (value === undefined) return true
// Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
const valueIsEmpty = !value || value === '0x' || value === '0x0'
return !valueIsEmpty
}
PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {

@ -68,7 +68,7 @@ ConfigScreen.prototype.render = function () {
currentProviderDisplay(metamaskState),
h('div', { style: {display: 'flex'} }, [
h('div', { style: {display: 'block'} }, [
h('input#new_rpc', {
placeholder: 'New RPC URL',
style: {
@ -81,7 +81,70 @@ ConfigScreen.prototype.render = function () {
if (event.key === 'Enter') {
var element = event.target
var newRpc = element.value
rpcValidation(newRpc, state)
var chainid = document.querySelector('input#chainid')
var ticker = document.querySelector('input#ticker')
var nickname = document.querySelector('input#nickname')
rpcValidation(newRpc, chainid.value, ticker.value, nickname.value, state)
}
},
}),
h('br'),
h('input#chainid', {
placeholder: 'ChainId (optional)',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onKeyPress (event) {
if (event.key === 'Enter') {
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
var chainid = document.querySelector('input#chainid')
var ticker = document.querySelector('input#ticker')
var nickname = document.querySelector('input#nickname')
rpcValidation(newRpc, chainid.value, ticker.value, nickname.value, state)
}
},
}),
h('br'),
h('input#ticker', {
placeholder: 'Symbol (optional)',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onKeyPress (event) {
if (event.key === 'Enter') {
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
var chainid = document.querySelector('input#chainid')
var ticker = document.querySelector('input#ticker')
var nickname = document.querySelector('input#nickname')
rpcValidation(newRpc, chainid.value, ticker.value, nickname.value, state)
}
},
}),
h('br'),
h('input#nickname', {
placeholder: 'Nickname (optional)',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onKeyPress (event) {
if (event.key === 'Enter') {
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
var chainid = document.querySelector('input#chainid')
var ticker = document.querySelector('input#ticker')
var nickname = document.querySelector('input#nickname')
rpcValidation(newRpc, chainid.value, ticker.value, nickname.value, state)
}
},
}),
@ -93,7 +156,10 @@ ConfigScreen.prototype.render = function () {
event.preventDefault()
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
rpcValidation(newRpc, state)
var chainid = document.querySelector('input#chainid')
var ticker = document.querySelector('input#ticker')
var nickname = document.querySelector('input#nickname')
rpcValidation(newRpc, chainid.value, ticker.value, nickname.value, state)
},
}, 'Save'),
]),
@ -134,6 +200,62 @@ ConfigScreen.prototype.render = function () {
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('p', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
},
}, 'Clear privacy data so all websites must request access to view account information again.'),
h('br'),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
event.preventDefault()
state.dispatch(actions.clearApprovedOrigins())
},
}, 'Clear privacy data'),
]),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('p', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
},
}, metamaskState.featureFlags.privacyMode ?
'Websites will be able to view your account information.' :
'Websites must request access to view your account information.'
),
h('br'),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
event.preventDefault()
state.dispatch(actions.setFeatureFlag('privacyMode', !metamaskState.featureFlags.privacyMode))
},
}, metamaskState.featureFlags.privacyMode ?
'Disable privacy mode' :
'Enable privacy mode'
),
]),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
@ -189,9 +311,9 @@ ConfigScreen.prototype.render = function () {
)
}
function rpcValidation (newRpc, state) {
function rpcValidation (newRpc, chainid, ticker = 'ETH', nickname = '', state) {
if (validUrl.isWebUri(newRpc)) {
state.dispatch(actions.setRpcTarget(newRpc))
state.dispatch(actions.setRpcTarget(newRpc, chainid, ticker, nickname))
} else {
var appendedRpc = `http://${newRpc}`
if (validUrl.isWebUri(appendedRpc)) {

@ -0,0 +1,64 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { approveProviderRequest, rejectProviderRequest } from '../../ui/app/actions'
import { connect } from 'react-redux'
class ProviderApproval extends Component {
render () {
const { approveProviderRequest, origin, rejectProviderRequest } = this.props
return (
<div className="flex-column flex-grow">
<style dangerouslySetInnerHTML={{__html: `
.provider_approval_actions {
display: flex;
justify-content: flex-end;
margin: 14px 25px;
}
.provider_approval_actions button {
margin-left: 10px;
text-transform: uppercase;
}
.provider_approval_content {
padding: 0 25px;
}
.provider_approval_origin {
font-weight: bold;
margin: 14px 0;
}
`}} />
<div className="section-title flex-row flex-center">
<i
className="fa fa-arrow-left fa-lg cursor-pointer"
onClick={() => { rejectProviderRequest(origin) }} />
<h2 className="page-subtitle">Web3 API Request</h2>
</div>
<div className="provider_approval_content">
{"The domain listed below is requesting access to the Ethereum blockchain and to view your current account. Always double check that you're on the correct site before approving access."}
<div className="provider_approval_origin">{origin}</div>
</div>
<div className="provider_approval_actions">
<button
className="btn-green"
onClick={() => { approveProviderRequest(origin) }}>APPROVE</button>
<button
className="cancel btn-red"
onClick={() => { rejectProviderRequest(origin) }}>REJECT</button>
</div>
</div>
)
}
}
ProviderApproval.propTypes = {
approveProviderRequest: PropTypes.func,
origin: PropTypes.string,
rejectProviderRequest: PropTypes.func,
}
function mapDispatchToProps (dispatch) {
return {
approveProviderRequest: origin => dispatch(approveProviderRequest(origin)),
rejectProviderRequest: origin => dispatch(rejectProviderRequest(origin)),
}
}
module.exports = connect(null, mapDispatchToProps)(ProviderApproval)

@ -102,7 +102,7 @@ function parseBalance (balance) {
// Takes wei hex, returns an object with three properties.
// Its "formatted" property is what we generally use to render values.
function formatBalance (balance, decimalsToKeep, needsParse = true) {
function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ETH') {
var parsed = needsParse ? parseBalance(balance) : balance.split('.')
var beforeDecimal = parsed[0]
var afterDecimal = parsed[1]
@ -112,14 +112,14 @@ function formatBalance (balance, decimalsToKeep, needsParse = true) {
if (afterDecimal !== '0') {
var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits
if (sigFigs) { afterDecimal = sigFigs[0] }
formatted = '0.' + afterDecimal + ' ETH'
formatted = '0.' + afterDecimal + ` ${ticker}`
}
} else {
formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH'
formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${ticker}`
}
} else {
afterDecimal += Array(decimalsToKeep).join('0')
formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH'
formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${ticker}`
}
return formatted
}

15290
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -82,6 +82,7 @@
},
"dependencies": {
"@material-ui/core": "1.0.0",
"@sentry/browser": "^4.1.1",
"@zxing/library": "^0.8.0",
"abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0",
@ -97,7 +98,7 @@
"browserify-derequire": "^0.9.4",
"browserify-unibabel": "^3.0.0",
"classnames": "^2.2.5",
"clone": "^2.1.1",
"clone": "^2.1.2",
"copy-to-clipboard": "^3.0.8",
"css-loader": "^0.28.11",
"currency-formatter": "^1.4.2",
@ -112,7 +113,7 @@
"ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^4.0.3",
"eth-block-tracker": "^4.1.0",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
"eth-ens-namehash": "^2.0.8",
"eth-hd-keyring": "^1.2.2",
@ -124,7 +125,7 @@
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^2.0.2",
"eth-token-tracker": "^1.1.4",
"eth-token-tracker": "^1.1.5",
"eth-trezor-keyring": "^0.1.0",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "^1.3.0",
@ -143,7 +144,7 @@
"fast-levenshtein": "^2.0.6",
"file-loader": "^1.1.11",
"fuse.js": "^3.2.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp": "github:gulpjs/gulp#v4.0.0",
"gulp-autoprefixer": "^5.0.0",
"gulp-debug": "^3.2.0",
"gulp-eslint": "^4.0.0",
@ -186,7 +187,6 @@
"pumpify": "^1.3.4",
"qrcode-npm": "0.0.3",
"ramda": "^0.24.1",
"raven-js": "^3.24.2",
"react": "^15.6.2",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.2",
@ -261,7 +261,7 @@
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^3.1.3",
"eth-json-rpc-middleware": "^3.1.6",
"eth-keyring-controller": "^3.3.1",
"fetch-mock": "^6.5.2",
"file-loader": "^1.1.11",

@ -111,7 +111,9 @@
"0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d": 0.00039345803819379796,
"0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5": 0.00008189274407698049
},
"ticker": "ETH",
"currentCurrency": "usd",
"nativeCurrency": "ETH",
"conversionRate": 556.12,
"addressBook": [
{
@ -1248,4 +1250,4 @@
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
}
}
}
}

File diff suppressed because one or more lines are too long

@ -19,6 +19,7 @@ const {
openNewPage,
verboseReportOnFailure,
waitUntilXWindowHandles,
switchToWindowWithTitle,
} = require('./helpers')
describe('MetaMask', function () {
@ -266,17 +267,31 @@ describe('MetaMask', function () {
})
describe('Drizzle', () => {
it('should be able to detect our eth address', async () => {
let windowHandles
let extension
let popup
let dapp
it('should be able to connect the account', async () => {
await openNewPage(driver, 'http://127.0.0.1:3000/')
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 2)
const windowHandles = await driver.getAllWindowHandles()
const dapp = windowHandles[1]
await waitUntilXWindowHandles(driver, 3)
windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click()
})
it('should be able to detect our eth address', async () => {
// Check if address exposed
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const addressElement = await findElement(driver, By.css(`.pure-u-1-1 h4`))
const addressText = await addressElement.getText()

@ -286,7 +286,7 @@ describe('Using MetaMask with an existing account', function () {
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
@ -319,7 +319,7 @@ describe('Using MetaMask with an existing account', function () {
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '-1 ETH')
assert.ok(/-1\s*ETH/.test(await txValues[0].getText()))
})
})

@ -14,6 +14,7 @@ module.exports = {
loadExtension,
openNewPage,
switchToWindowWithTitle,
switchToWindowWithUrlThatMatches,
verboseReportOnFailure,
waitUntilXWindowHandles,
}
@ -130,3 +131,19 @@ async function assertElementNotPresent (webdriver, driver, by) {
}
assert.ok(!dataTab, 'Found element that should not be present')
}
async function switchToWindowWithUrlThatMatches (driver, regexp, windowHandles) {
if (!windowHandles) {
windowHandles = await driver.getAllWindowHandles()
} else if (windowHandles.length === 0) {
throw new Error('No window that matches: ' + regexp)
}
const firstHandle = windowHandles[0]
await driver.switchTo().window(firstHandle)
const windowUrl = await driver.getCurrentUrl()
if (windowUrl.match(regexp)) {
return firstHandle
} else {
return await switchToWindowWithUrlThatMatches(driver, regexp, windowHandles.slice(1))
}
}

@ -0,0 +1,360 @@
const path = require('path')
const assert = require('assert')
const webdriver = require('selenium-webdriver')
const { By, until } = webdriver
const {
delay,
buildChromeWebDriver,
buildFirefoxWebdriver,
installWebExt,
getExtensionIdChrome,
getExtensionIdFirefox,
} = require('../func')
const {
checkBrowserForConsoleErrors,
closeAllWindowHandlesExcept,
findElement,
findElements,
loadExtension,
verboseReportOnFailure,
} = require('./helpers')
describe('MetaMask', function () {
let extensionId
let driver
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const tinyDelayMs = 200
const regularDelayMs = tinyDelayMs * 2
const largeDelayMs = regularDelayMs * 2
this.timeout(0)
this.bail(true)
before(async function () {
switch (process.env.SELENIUM_BROWSER) {
case 'chrome': {
const extPath = path.resolve('dist/chrome')
driver = buildChromeWebDriver(extPath, { responsive: true })
extensionId = await getExtensionIdChrome(driver)
await driver.get(`chrome-extension://${extensionId}/popup.html`)
break
}
case 'firefox': {
const extPath = path.resolve('dist/firefox')
driver = buildFirefoxWebdriver({ responsive: true })
await installWebExt(driver, extPath)
await delay(700)
extensionId = await getExtensionIdFirefox(driver)
await driver.get(`moz-extension://${extensionId}/popup.html`)
}
}
})
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('New UI setup', async function () {
it('switches to first tab', async function () {
await delay(tinyDelayMs)
const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab)
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
try {
const overlay = await findElement(driver, By.css('.full-flex-height'))
await driver.wait(until.stalenessOf(overlay))
} catch (e) {}
let button
try {
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
} catch (e) {
await loadExtension(driver, extensionId)
await delay(largeDelayMs)
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
}
await button.click()
await delay(regularDelayMs)
// Close all other tabs
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
await driver.switchTo().window(tab0)
await delay(tinyDelayMs)
let selectedUrl = await driver.getCurrentUrl()
await delay(tinyDelayMs)
if (tab0 && selectedUrl.match(/popup.html/)) {
await closeAllWindowHandlesExcept(driver, tab0)
} else if (tab1) {
await driver.switchTo().window(tab1)
selectedUrl = await driver.getCurrentUrl()
await delay(tinyDelayMs)
if (selectedUrl.match(/popup.html/)) {
await closeAllWindowHandlesExcept(driver, tab1)
} else if (tab2) {
await driver.switchTo().window(tab2)
selectedUrl = await driver.getCurrentUrl()
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
}
} else {
throw new Error('popup.html not found')
}
await delay(regularDelayMs)
const [appTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(appTab)
await delay(tinyDelayMs)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
await continueBtn.click()
await delay(regularDelayMs)
})
})
describe('Going through the first time flow', () => {
it('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.create-password #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.create-password #confirm-password'))
const button = await findElement(driver, By.css('.create-password button'))
await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple')
await button.click()
await delay(regularDelayMs)
})
it('clicks through the unique image screen', async () => {
const nextScreen = await findElement(driver, By.css('.unique-image button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the ToS', async () => {
// terms of use
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('.tou button'))
driver.wait(until.elementIsEnabled(acceptTos))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase
it('reveals the seed phrase', async () => {
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
await nextScreen.click()
await delay(regularDelayMs)
})
async function clickWordAndWait (word) {
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${word}')]`
const word0 = await findElement(driver, By.xpath(xpath), 10000)
await word0.click()
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
await nextScreen.click()
await delay(regularDelayMs)
}
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
await delay(regularDelayMs)
})
it('clicks through the deposit modal', async () => {
const byBuyModal = By.css('span .modal')
const buyModal = await driver.wait(until.elementLocated(byBuyModal))
const closeModal = await findElement(driver, By.css('.page-container__header-close'))
await closeModal.click()
await driver.wait(until.stalenessOf(buyModal))
await delay(regularDelayMs)
})
})
describe('Show account information', () => {
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
await delay(regularDelayMs)
})
})
describe('Import seed phrase', () => {
it('logs out of the vault', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const logoutButton = await findElement(driver, By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click()
await delay(regularDelayMs)
})
it('imports seed phrase', async () => {
const restoreSeedLink = await findElement(driver, By.css('.unlock-page__link--import'))
assert.equal(await restoreSeedLink.getText(), 'Import using account seed phrase')
await restoreSeedLink.click()
await delay(regularDelayMs)
const seedTextArea = await findElement(driver, By.css('textarea'))
await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs)
const passwordInputs = await driver.findElements(By.css('input'))
await delay(regularDelayMs)
await passwordInputs[0].sendKeys('correct horse battery staple')
await passwordInputs[1].sendKeys('correct horse battery staple')
await driver.findElement(By.css('.first-time-flow__button')).click()
await delay(regularDelayMs)
})
it('switches to localhost', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(largeDelayMs * 2)
})
it('balance renders', async () => {
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await driver.wait(until.elementTextMatches(balance, /100\s*ETH/))
await delay(regularDelayMs)
})
})
describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () {
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click()
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
const inputValue = await inputAmount.getAttribute('value')
assert.equal(inputValue, '1')
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
await configureGas.click()
await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
await delay(regularDelayMs)
// Continue to next screen
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
it('confirms the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(largeDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
})
})
})

@ -271,16 +271,21 @@ describe('MetaMask', function () {
await driver.wait(until.stalenessOf(accountModal))
await delay(regularDelayMs)
})
it('show account details dropdown menu', async () => {
})
describe('Enable privacy mode', () => {
it('enables privacy mode', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const {width, height} = await driver.manage().window().getSize()
driver.manage().window().setSize(320, 480)
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
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
await customRpcButton.click()
await delay(regularDelayMs)
driver.manage().window().setSize(width, height)
const privacyToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(10) .settings-page__content-item-col > div'))
await privacyToggle.click()
await delay(largeDelayMs * 2)
})
})
@ -371,7 +376,7 @@ describe('MetaMask', function () {
it('balance renders', async () => {
const balance = await findElement(driver, By.css('.balance-display .token-amount'))
await driver.wait(until.elementTextMatches(balance, /100.+ETH/))
await driver.wait(until.elementTextMatches(balance, /100\s*ETH/))
await delay(regularDelayMs)
})
})
@ -383,7 +388,7 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
@ -420,30 +425,43 @@ describe('MetaMask', function () {
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
})
})
describe('Send ETH from dapp', () => {
let windowHandles
let extension
let popup
let dapp
it('starts a send transaction inside the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 2)
let windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
const dapp = windowHandles[1]
await waitUntilXWindowHandles(driver, 3)
windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click()
})
it('initiates a send from the dapp', async () => {
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
await send3eth.click()
await delay(regularDelayMs)
await delay(5000)
windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[2])
await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
await delay(regularDelayMs)
await assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`))
@ -462,7 +480,7 @@ describe('MetaMask', function () {
assert.equal(transactions.length, 2)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
await driver.wait(until.elementTextMatches(txValues, /-3\s*ETH/), 10000)
})
})
@ -540,7 +558,7 @@ describe('MetaMask', function () {
await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
await driver.wait(until.elementTextMatches(txListValue, /-4\s*ETH/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -574,7 +592,7 @@ describe('MetaMask', function () {
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
await driver.wait(until.elementTextMatches(txValues[0], /-4\s*ETH/), 10000)
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
// const firstTxAddress = await txAccounts[0].getText()
@ -606,7 +624,7 @@ describe('MetaMask', function () {
}, 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
await driver.wait(until.elementTextMatches(txValues, /-0\s*ETH/), 10000)
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
@ -616,9 +634,9 @@ describe('MetaMask', function () {
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
await driver.wait(until.elementTextMatches(balance, /^92.*\s*ETH.*$/), 10000)
const tokenAmount = await balance.getText()
assert.ok(/^92.*ETH.*$/.test(tokenAmount))
assert.ok(/^92.*\s*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs)
}
})
@ -662,7 +680,7 @@ describe('MetaMask', function () {
})
it('clicks on the Add Token button', async () => {
const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
const addToken = await driver.findElement(By.xpath(`//div[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
@ -702,7 +720,7 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('50')
@ -764,7 +782,7 @@ describe('MetaMask', function () {
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
await driver.wait(until.elementTextMatches(txValues[0], /-50\s*TST/), 10000)
}
driver.wait(async () => {
@ -798,7 +816,7 @@ describe('MetaMask', function () {
await findElements(driver, By.css('.transaction-list__pending-transactions'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
await driver.wait(until.elementTextMatches(txListValue, /-7\s*TST/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -834,8 +852,8 @@ describe('MetaMask', function () {
await save.click()
await driver.wait(until.stalenessOf(gasModal))
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
})
it('submits the transaction', async function () {
@ -851,7 +869,7 @@ describe('MetaMask', function () {
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
@ -897,7 +915,7 @@ describe('MetaMask', function () {
const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
await driver.wait(until.elementTextMatches(txListValue, /-7\s*TST/))
await txListItem.click()
await delay(regularDelayMs)
})
@ -957,8 +975,8 @@ describe('MetaMask', function () {
await save.click()
await driver.wait(until.stalenessOf(gasModal))
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
})
it('submits the transaction', async function () {
@ -974,7 +992,7 @@ describe('MetaMask', function () {
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
@ -1002,7 +1020,7 @@ describe('MetaMask', function () {
describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
const addToken = await findElement(driver, By.xpath(`//div[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
@ -1027,7 +1045,7 @@ describe('MetaMask', function () {
it('renders the balance for the chosen token', async () => {
const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
await driver.wait(until.elementTextMatches(balance, /0\s*BAT/))
await delay(regularDelayMs)
})
})

@ -7,4 +7,5 @@ set -o pipefail
export PATH="$PATH:./node_modules/.bin"
shell-parallel -s 'npm run ganache:start -- -b 2' -x 'sleep 5 && static-server test/e2e/beta/contract-test --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
shell-parallel -s 'npm run ganache:start -- -b 2' -x 'sleep 5 && static-server test/e2e/beta/contract-test --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-responsive-ui.spec'
shell-parallel -s 'npm run ganache:start -- -d -b 2' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'

@ -11,7 +11,7 @@ sleep 5
cd test/e2e/beta/
rm -rf drizzle-test
mkdir drizzle-test && cd drizzle-test
npm install truffle
sudo npm install -g truffle
truffle unbox drizzle
echo "Deploying contracts for Drizzle test..."
truffle compile && truffle migrate

@ -56,23 +56,31 @@ async function setupBrowserAndExtension ({ browser, extPath }) {
return { driver, extensionId, extensionUri }
}
function buildChromeWebDriver (extPath) {
function buildChromeWebDriver (extPath, opts = {}) {
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile'))
const args = [
`load-extension=${extPath}`,
`user-data-dir=${tmpProfile}`,
]
if (opts.responsive) {
args.push('--auto-open-devtools-for-tabs')
}
return new webdriver.Builder()
.withCapabilities({
chromeOptions: {
args: [
`load-extension=${extPath}`,
`user-data-dir=${tmpProfile}`,
],
args,
binary: process.env.SELENIUM_CHROME_BINARY,
},
})
.build()
}
function buildFirefoxWebdriver () {
return new webdriver.Builder().build()
function buildFirefoxWebdriver (opts = {}) {
const driver = new webdriver.Builder().build()
if (opts.responsive) {
driver.manage().window().setSize(320, 600)
}
return driver
}
async function getExtensionIdChrome (driver) {

@ -1,140 +0,0 @@
const reactTriggerChange = require('react-trigger-change')
const {
timeout,
queryAsync,
findAsync,
} = require('../../lib/util')
QUnit.module('Add token flow')
QUnit.test('successful add token flow', (assert) => {
const done = assert.async()
runAddTokenFlowTest(assert)
.then(done)
.catch(err => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
done()
})
})
async function runAddTokenFlowTest (assert, done) {
const selectState = await queryAsync($, 'select')
selectState.val('add token')
reactTriggerChange(selectState[0])
// Used to set values on TextField input component
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set
// Check that no tokens have been added
assert.ok($('.token-list-item').length === 0, 'no tokens added')
// Go to Add Token screen
let addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button')
assert.ok(addTokenButton[0], 'add token button present')
addTokenButton[0].click()
// Verify Add Token screen
let addTokenWrapper = await queryAsync($, '.page-container')
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
let addTokenTitle = await queryAsync($, '.page-container__title')
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Cancel Add Token
const cancelAddTokenButton = await queryAsync($, 'button.btn-default.btn--large.page-container__footer-button')
assert.ok(cancelAddTokenButton[0], 'cancel add token button present')
cancelAddTokenButton.click()
assert.ok($('.wallet-view')[0], 'cancelled and returned to account detail wallet view')
// Return to Add Token Screen
addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button')
assert.ok(addTokenButton[0], 'add token button present')
addTokenButton[0].click()
// Verify Add Token Screen
addTokenWrapper = await queryAsync($, '.page-container')
addTokenTitle = await queryAsync($, '.page-container__title')
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Search for token
const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0]
searchInput.focus()
await timeout(1000)
nativeInputValueSetter.call(searchInput, 'a')
searchInput.dispatchEvent(new Event('input', { bubbles: true}))
// Click token to add
const tokenWrapper = await queryAsync($, 'div.token-list__token')
assert.ok(tokenWrapper[0], 'token found')
const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image')
const tokenImageUrl = tokenImageProp.slice(5, -2)
tokenWrapper[0].click()
// Click Next button
const nextButton = await queryAsync($, 'button.btn-primary.btn--large')
assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
nextButton[0].click()
// Confirm Add token
const confirmAddToken = await queryAsync($, '.confirm-add-token')
assert.ok(confirmAddToken[0], 'confirm add token rendered')
assert.ok($('button.btn-primary.btn--large')[0], 'confirm add token button found')
$('button.btn-primary.btn--large')[0].click()
// Verify added token image
let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
// Return to Add Token Screen
addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button')
assert.ok(addTokenButton[0], 'add token button present')
addTokenButton[0].click()
addTokenWrapper = await queryAsync($, '.page-container')
const addTokenTabs = await queryAsync($, '.page-container__tab')
assert.equal(addTokenTabs.length, 2, 'expected number of tabs')
assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present')
assert.ok(addTokenTabs[1], 'add custom token tab present')
addTokenTabs[1].click()
await timeout(1000)
// Input token contract address
const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0]
customInput.focus()
await timeout(1000)
nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
customInput.dispatchEvent(new Event('input', { bubbles: true}))
// Click Next button
// nextButton = await queryAsync($, 'button.btn-primary--lg')
// assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
// nextButton[0].click()
// // Verify symbol length error since contract address won't return symbol
const errorMessage = await queryAsync($, '#custom-symbol-helper-text')
assert.ok(errorMessage[0], 'error rendered')
$('button.btn-default.btn--large')[0].click()
// await timeout(100000)
// Confirm Add token
// assert.equal(
// $('.page-container__subtitle')[0].textContent,
// 'Would you like to add these tokens?',
// 'confirm add token rendered'
// )
// assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
// $('button.btn-primary--lg')[0].click()
// Verify added token image
heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(heroBalance.find('.identicon')[0], 'token added')
}

@ -25,5 +25,5 @@ async function runCurrencyLocalizationTest (assert, done) {
const txView = await queryAsync($, '.transaction-view')
const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
assert.equal(fiatAmount[0].textContent, '₱102,707.97PHP')
}

@ -77,7 +77,7 @@ async function runFirstTimeUsageTest (assert, done) {
assert.ok(lock, 'Lock menu item found')
lock.click()
await timeout(1000)
await timeout(5000)
const pwBox2 = (await findAsync(app, '#password'))[0]
pwBox2.focus()

@ -40,7 +40,7 @@ async function customizeGas (assert, price, limit, ethFee, usdFee) {
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal(
(await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(),
(await findAsync(sendGasField, '.currency-display-component'))[0].textContent,
ethFee,
'send gas field should show customized gas total'
)
@ -97,9 +97,9 @@ async function runSendFlowTest (assert, done) {
assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address')
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountField.find('.currency-display')[0].click()
sendAmountField.find('.unit-input')[0].click()
const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input')
const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input')
sendAmountFieldInput.val('5.1')
reactTriggerChange(sendAmountField.find('input')[0])
@ -112,9 +112,9 @@ async function runSendFlowTest (assert, done) {
errorMessage = $('.send-v2__error')
assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected')
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD')
await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
await customizeGas(assert, 0, 21000, '0ETH', '$0.00USD')
await customizeGas(assert, 1, 21000, '0.000021ETH', '$0.03USD')
await customizeGas(assert, 500, 60000, '0.03ETH', '$36.03USD')
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
@ -130,11 +130,11 @@ async function runSendFlowTest (assert, done) {
const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__secondary')
const confirmScreenGas = confirmScreenRowFiats[0]
assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas')
const confirmScreenTotal = confirmScreenRowFiats[1]
assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total')
assert.equal(confirmScreenTotal.textContent, '$2,405.37', 'confirm screen should show correct total')
const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button')
confirmScreenBackButton[0].click()
@ -150,9 +150,9 @@ async function runSendFlowTest (assert, done) {
sendToFieldInputInEdit.val('0xd85a4b6a394794842887b8284293d69163007bbb')
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountFieldInEdit.find('.currency-display')[0].click()
sendAmountFieldInEdit.find('.unit-input')[0].click()
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input')
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.unit-input__input')
sendAmountFieldInputInEdit.val('1.0')
reactTriggerChange(sendAmountFieldInputInEdit[0])

@ -47,7 +47,7 @@ describe('# Network Controller', function () {
describe('#setNetworkState', function () {
it('should update the network', function () {
networkController.setNetworkState(1)
networkController.setNetworkState(1, 'rpc')
const networkState = networkController.getNetworkState()
assert.equal(networkState, 1, 'network is 1')
})

@ -375,6 +375,11 @@ 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.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.calledTwice(stubEnd)
sandbox.assert.notCalled(stubNext)
})
it('should through error if method is supported but asset type is not', async function () {
req.method = 'metamask_watchAsset'
@ -413,7 +418,7 @@ describe('preferences controller', function () {
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.showWatchAssetUi = async () => {}
preferencesController.openPopup = async () => {}
await preferencesController._handleWatchAssetERC20(req.params.options)
const suggested = preferencesController.getSuggestedTokens()
@ -433,7 +438,7 @@ describe('preferences controller', function () {
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.showWatchAssetUi = async () => {
preferencesController.openPopup = async () => {
await preferencesController.addToken(address, symbol, decimals, image)
}
@ -448,6 +453,32 @@ describe('preferences controller', function () {
const assetImages = preferencesController.getAssetImages()
assert.ok(assetImages[address], `set image correctly`)
})
it('should validate ERC20 asset correctly', async function () {
const validateSpy = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpy({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC', decimals: 0}) } catch (e) {}
assert.equal(validateSpy.threw(), false, 'correct options object')
const validateSpyAddress = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpyAddress({symbol: 'ABC', decimals: 0}) } catch (e) {}
assert.equal(validateSpyAddress.threw(), true, 'options object with no address')
const validateSpySymbol = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpySymbol({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', decimals: 0}) } catch (e) {}
assert.equal(validateSpySymbol.threw(), true, 'options object with no symbol')
const validateSpyDecimals = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpyDecimals({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC'}) } catch (e) {}
assert.equal(validateSpyDecimals.threw(), true, 'options object with no decimals')
const validateSpyInvalidSymbol = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpyInvalidSymbol({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 0}) } catch (e) {}
assert.equal(validateSpyInvalidSymbol.threw(), true, 'options object with invalid symbol')
const validateSpyInvalidDecimals1 = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpyInvalidDecimals1({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: -1}) } catch (e) {}
assert.equal(validateSpyInvalidDecimals1.threw(), true, 'options object with decimals less than zero')
const validateSpyInvalidDecimals2 = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpyInvalidDecimals2({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 38}) } catch (e) {}
assert.equal(validateSpyInvalidDecimals2.threw(), true, 'options object with decimals more than 36')
const validateSpyInvalidAddress = sandbox.spy(preferencesController._validateERC20AssetParams)
try { validateSpyInvalidAddress({rawAddress: '0x123', symbol: 'ABC', decimals: 0}) } catch (e) {}
assert.equal(validateSpyInvalidAddress.threw(), true, 'options object with address invalid')
})
})
describe('setPasswordForgotten', function () {
@ -479,5 +510,24 @@ describe('preferences controller', function () {
assert.equal(preferencesController.store.getState().seedWords, 'foo bar baz')
})
})
describe('on updateFrequentRpcList', function () {
it('should add custom RPC url to state', function () {
preferencesController.addToFrequentRpcList('rpc_url', 1)
preferencesController.addToFrequentRpcList('http://localhost:8545', 1)
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }] )
preferencesController.addToFrequentRpcList('rpc_url', 1)
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }] )
})
it('should remove custom RPC url from state', function () {
preferencesController.addToFrequentRpcList('rpc_url', 1)
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }] )
preferencesController.removeFromFrequentRpcList('other_rpc_url')
preferencesController.removeFromFrequentRpcList('http://localhost:8545')
preferencesController.removeFromFrequentRpcList('rpc_url')
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [])
})
})
})

@ -1,44 +0,0 @@
const assert = require('assert')
const h = require('react-hyperscript')
const { createMockStore } = require('redux-test-utils')
const { shallowWithStore } = require('../../lib/render-helpers')
const BalanceComponent = require('../../../ui/app/components/balance-component')
const mockState = {
metamask: {
accounts: { abc: {} },
network: 1,
selectedAddress: 'abc',
},
}
describe('BalanceComponent', function () {
let balanceComponent
let store
let component
beforeEach(function () {
store = createMockStore(mockState)
component = shallowWithStore(h(BalanceComponent), store)
balanceComponent = component.dive()
})
it('shows token balance and convert to fiat value based on conversion rate', function () {
const formattedBalance = '1.23 ETH'
const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false)
const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 2)
assert.equal('1.23 ETH', tokenBalance)
assert.equal(2.46, fiatDisplayNumber)
})
it('shows only the token balance when conversion rate is not available', function () {
const formattedBalance = '1.23 ETH'
const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false)
const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 0)
assert.equal('1.23 ETH', tokenBalance)
assert.equal('N/A', fiatDisplayNumber)
})
})

@ -1133,7 +1133,7 @@ describe('Actions', () => {
{ type: 'DISPLAY_WARNING', value: 'Had a problem changing networks!' },
]
setRpcTargetSpy.callsFake((newRpc, callback) => {
setRpcTargetSpy.callsFake((newRpc, chainId, ticker, nickname, callback) => {
callback(new Error('error'))
})

@ -0,0 +1,998 @@
import assert from 'assert'
import reduceApp from '../../../../../ui/app/reducers/app'
import * as actions from '../../../../../ui/app/actions'
describe('App State', () => {
const metamaskState = {
metamask: {
selectedAddress: '0xAddress',
identities: {
'0xAddress': {
name: 'account 1',
address: '0xAddress',
},
},
},
}
it('App init state', () => {
const initState = reduceApp(metamaskState, {})
assert(initState)
})
it('sets networkd dropdown to true', () => {
const state = reduceApp(metamaskState, {
type: actions.NETWORK_DROPDOWN_OPEN,
})
assert.equal(state.networkDropdownOpen, true)
})
it('sets networkd dropdown to false', () => {
const dropdown = { networkDropdowopen: true }
const state = {...metamaskState, ...dropdown}
const newState = reduceApp(state, {
type: actions.NETWORK_DROPDOWN_CLOSE,
})
assert.equal(newState.networkDropdownOpen, false)
})
it('opens sidebar', () => {
const value = {
'transitionName': 'sidebar-right',
'type': 'wallet-view',
'isOpen': true,
}
const state = reduceApp(metamaskState, {
type: actions.SIDEBAR_OPEN,
value,
})
assert.deepEqual(state.sidebar, value)
})
it('closes sidebar', () => {
const openSidebar = { sidebar: { isOpen: true }}
const state = {...metamaskState, ...openSidebar}
const newState = reduceApp(state, {
type: actions.SIDEBAR_CLOSE,
})
assert.equal(newState.sidebar.isOpen, false)
})
it('opens alert', () => {
const state = reduceApp(metamaskState, {
type: actions.ALERT_OPEN,
value: 'test message',
})
assert.equal(state.alertOpen, true)
assert.equal(state.alertMessage, 'test message')
})
it('closes alert', () => {
const alert = { alertOpen: true, alertMessage: 'test message' }
const state = {...metamaskState, ...alert}
const newState = reduceApp(state, {
type: actions.ALERT_CLOSE,
})
assert.equal(newState.alertOpen, false)
assert.equal(newState.alertMessage, null)
})
it('detects qr code data', () => {
const state = reduceApp(metamaskState, {
type: actions.QR_CODE_DETECTED,
value: 'qr data',
})
assert.equal(state.qrCodeData, 'qr data')
})
it('opens modal', () => {
const state = reduceApp(metamaskState, {
type: actions.MODAL_OPEN,
payload: {
name: 'test',
},
})
assert.equal(state.modal.open, true)
assert.equal(state.modal.modalState.name, 'test')
})
it('closes modal, but moves open modal state to previous modal state', () => {
const opensModal = {
modal: {
open: true,
modalState: {
name: 'test',
},
},
}
const state = { ...metamaskState, appState: { ...opensModal } }
const newState = reduceApp(state, {
type: actions.MODAL_CLOSE,
})
assert.equal(newState.modal.open, false)
assert.equal(newState.modal.modalState.name, null)
})
it('tansitions forwards', () => {
const state = reduceApp(metamaskState, {
type: actions.TRANSITION_FORWARD,
})
assert.equal(state.transForward, true)
})
it('transition backwards', () => {
const transitionForwardState = { transitionForward: true }
const state = { ...metamaskState, ...transitionForwardState }
const newState = reduceApp(state, {
type: actions.TRANSITION_BACKWARD,
})
assert.equal(newState.transForward, false)
})
it('shows create vault', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_CREATE_VAULT,
})
assert.equal(state.currentView.name, 'createVault')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('shows restore vault', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_RESTORE_VAULT,
})
assert.equal(state.currentView.name, 'restoreVault')
assert.equal(state.transForward, true)
assert.equal(state.forgottenPassword, true)
})
it('sets forgot password', () => {
const state = reduceApp(metamaskState, {
type: actions.FORGOT_PASSWORD,
value: true,
})
assert.equal(state.currentView.name, 'restoreVault')
})
it('shows init menu', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_INIT_MENU,
})
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.currentView.context, '0xAddress')
})
it('shows config page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_CONFIG_PAGE,
value: true,
})
assert.equal(state.currentView.name, 'config')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
})
it('shows add token page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_ADD_TOKEN_PAGE,
value: true,
})
assert.equal(state.currentView.name, 'add-token')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
})
it('shows add suggested token page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE,
value: true,
})
assert.equal(state.currentView.name, 'add-suggested-token')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
})
it('shows import page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_IMPORT_PAGE,
})
assert.equal(state.currentView.name, 'import-menu')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('shows new account page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_NEW_ACCOUNT_PAGE,
formToSelect: 'context',
})
assert.equal(state.currentView.name, 'new-account-page')
assert.equal(state.currentView.context, 'context')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('sets new account form', () => {
const state = reduceApp(metamaskState, {
type: actions.SET_NEW_ACCOUNT_FORM,
formToSelect: 'context',
})
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.currentView.context, 'context')
})
it('shows info page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_INFO_PAGE,
})
assert.equal(state.currentView.name, 'info')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
})
it('creates new vault in progress', () => {
const state = reduceApp(metamaskState, {
type: actions.CREATE_NEW_VAULT_IN_PROGRESS,
})
assert.equal(state.currentView.name, 'createVault')
assert.equal(state.currentView.inProgress, true)
assert.equal(state.transForward, true)
assert.equal(state.isLoading, true)
})
it('shows new vault seed', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_NEW_VAULT_SEED,
value: 'test seed words',
})
assert.equal(state.currentView.name, 'createVaultComplete')
assert.equal(state.currentView.seedWords, 'test seed words')
assert.equal(state.transForward, true)
assert.equal(state.isLoading, false)
})
it('shows new account screen', () => {
const state = reduceApp(metamaskState, {
type: actions.NEW_ACCOUNT_SCREEN,
})
assert.equal(state.currentView.name, 'new-account')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
})
it('shows send page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_SEND_PAGE,
})
assert.equal(state.currentView.name, 'sendTransaction')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('shows send token page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_SEND_TOKEN_PAGE,
})
assert.equal(state.currentView.name, 'sendToken')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('shows new keychain', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_NEW_KEYCHAIN,
})
assert.equal(state.currentView.name, 'newKeychain')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, true)
})
it('unlocks Metamask', () => {
const state = reduceApp(metamaskState, {
type: actions.UNLOCK_METAMASK,
})
assert.equal(state.forgottenPassword, null)
assert.deepEqual(state.detailView, {})
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('locks Metamask', () => {
const state = reduceApp(metamaskState, {
type: actions.LOCK_METAMASK,
})
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, false)
assert.equal(state.warning, null)
})
it('goes back to init menu', () => {
const state = reduceApp(metamaskState, {
type: actions.BACK_TO_INIT_MENU,
})
assert.equal(state.currentView.name, 'InitMenu')
assert.equal(state.transForward, false)
assert.equal(state.warning, null)
assert.equal(state.forgottenPassword, true)
})
it('goes back to unlock view', () => {
const state = reduceApp(metamaskState, {
type: actions.BACK_TO_UNLOCK_VIEW,
})
assert.equal(state.currentView.name, 'UnlockScreen')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
assert.equal(state.forgottenPassword, false)
})
it('reveals seed words', () => {
const state = reduceApp(metamaskState, {
type: actions.REVEAL_SEED_CONFIRMATION,
})
assert.equal(state.currentView.name, 'reveal-seed-conf')
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
})
it('sets selected account', () => {
const state = reduceApp(metamaskState, {
type: actions.SET_SELECTED_ACCOUNT,
value: 'active address',
})
assert.equal(state.activeAddress, 'active address')
})
it('goes home', () => {
const state = reduceApp(metamaskState, {
type: actions.GO_HOME,
})
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.accountDetail.subview, 'transactions')
assert.equal(state.accountDetail.accountExport, 'none')
assert.equal(state.accountDetail.privateKey, '')
assert.equal(state.transForward, false)
assert.equal(state.warning, null)
})
it('shows account detail', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_ACCOUNT_DETAIL,
value: 'context address',
})
assert.equal(state.forgottenPassword, null) // default
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.currentView.context, 'context address')
assert.equal(state.accountDetail.subview, 'transactions') // default
assert.equal(state.accountDetail.accountExport, 'none') // default
assert.equal(state.accountDetail.privateKey, '') // default
assert.equal(state.transForward, false)
})
it('goes back to account detail', () => {
const state = reduceApp(metamaskState, {
type: actions.BACK_TO_ACCOUNT_DETAIL,
value: 'context address',
})
assert.equal(state.forgottenPassword, null) // default
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.currentView.context, 'context address')
assert.equal(state.accountDetail.subview, 'transactions') // default
assert.equal(state.accountDetail.accountExport, 'none') // default
assert.equal(state.accountDetail.privateKey, '') // default
assert.equal(state.transForward, false)
})
it('shoes account page', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_ACCOUNTS_PAGE,
})
assert.equal(state.currentView.name, 'accounts')
assert.equal(state.currentView.seedWords, undefined)
assert.equal(state.transForward, true)
assert.equal(state.isLoading, false)
assert.equal(state.warning, null)
assert.equal(state.scrollToBottom, false)
assert.equal(state.forgottenPassword, false)
})
it('shows notice', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_NOTICE,
})
assert.equal(state.transForward, true)
assert.equal(state.isLoading, false)
})
it('reveals account', () => {
const state = reduceApp(metamaskState, {
type: actions.REVEAL_ACCOUNT,
})
assert.equal(state.scrollToBottom, true)
})
it('shows confirm tx page', () => {
const txs = {
unapprovedTxs: {
1: {
id: 1,
},
2: {
id: 2,
},
},
}
const oldState = {
metamask: {...metamaskState.metamask, ...txs},
}
const state = reduceApp(oldState, {
type: actions.SHOW_CONF_TX_PAGE,
id: 2,
transForward: false,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.context, 1)
assert.equal(state.transForward, false)
assert.equal(state.warning, null)
assert.equal(state.isLoading, false)
})
it('shows confirm msg page', () => {
const msgs = {
unapprovedMsgs: {
1: {
id: 1,
},
2: {
id: 2,
},
},
}
const oldState = {
metamask: {...metamaskState, ...msgs},
}
const state = reduceApp(oldState, {
type: actions.SHOW_CONF_MSG_PAGE,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.context, 0)
assert.equal(state.transForward, true)
assert.equal(state.warning, null)
assert.equal(state.isLoading, false)
})
it('completes tx continues to show pending txs current view context', () => {
const txs = {
unapprovedTxs: {
1: {
id: 1,
},
2: {
id: 2,
},
},
}
const oldState = {
metamask: {...metamaskState, ...txs},
}
const state = reduceApp(oldState, {
type: actions.COMPLETED_TX,
value: 1,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.context, 0)
assert.equal(state.transForward, false)
assert.equal(state.warning, null)
})
it('returns to account detail page when no unconf actions completed tx', () => {
const state = reduceApp(metamaskState, {
type: actions.COMPLETED_TX,
})
assert.equal(state.currentView.name, 'accountDetail')
assert.equal(state.currentView.context, '0xAddress')
assert.equal(state.transForward, false)
assert.equal(state.warning, null)
assert.equal(state.accountDetail.subview, 'transactions')
})
it('proceeds to change current view context in confTx', () => {
const oldState = {
metamask: {metamaskState},
appState: {currentView: {context: 0}},
}
const state = reduceApp(oldState, {
type: actions.NEXT_TX,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.context, 1)
assert.equal(state.warning, null)
})
it('views pending tx', () => {
const txs = {
unapprovedTxs: {
1: {
id: 1,
},
2: {
id: 2,
},
},
}
const oldState = {
metamask: {...metamaskState, ...txs},
}
const state = reduceApp(oldState, {
type: actions.VIEW_PENDING_TX,
value: 2,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.context, 1)
assert.equal(state.warning, null)
})
it('views previous tx', () => {
const txs = {
unapprovedTxs: {
1: {
id: 1,
},
2: {
id: 2,
},
},
}
const oldState = {
metamask: {...metamaskState, ...txs},
}
const state = reduceApp(oldState, {
type: actions.VIEW_PENDING_TX,
value: 2,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.context, 1)
assert.equal(state.warning, null)
})
it('sets error message in confTx view', () => {
const state = reduceApp(metamaskState, {
type: actions.TRANSACTION_ERROR,
})
assert.equal(state.currentView.name, 'confTx')
assert.equal(state.currentView.errorMessage, 'There was a problem submitting this transaction.')
})
it('sets default warning when unlock fails', () => {
const state = reduceApp(metamaskState, {
type: actions.UNLOCK_FAILED,
})
assert.equal(state.warning, 'Incorrect password. Try again.')
})
it('sets default warning when unlock fails', () => {
const state = reduceApp(metamaskState, {
type: actions.UNLOCK_FAILED,
value: 'errors',
})
assert.equal(state.warning, 'errors')
})
it('sets warning to empty string when unlock succeeds', () => {
const errorState = { warning: 'errors' }
const oldState = {...metamaskState, ...errorState}
const state = reduceApp(oldState, {
type: actions.UNLOCK_SUCCEEDED,
})
assert.equal(state.warning, '')
})
it('sets hardware wallet default hd path', () => {
const hdPaths = {
trezor: "m/44'/60'/0'/0",
ledger: "m/44'/60'/0'",
}
const state = reduceApp(metamaskState, {
type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
value: {
device: 'ledger',
path: "m/44'/60'/0'",
},
})
assert.deepEqual(state.defaultHdPaths, hdPaths)
})
it('shows loading message', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_LOADING,
value: 'loading',
})
assert.equal(state.isLoading, true)
assert.equal(state.loadingMessage, 'loading')
})
it('hides loading message', () => {
const loadingState = { isLoading: true}
const oldState = {...metamaskState, ...loadingState}
const state = reduceApp(oldState, {
type: actions.HIDE_LOADING,
})
assert.equal(state.isLoading, false)
})
it('shows sub loading indicator', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_SUB_LOADING_INDICATION,
})
assert.equal(state.isSubLoading, true)
})
it('hides sub loading indicator', () => {
const oldState = {...metamaskState, ...oldState}
const state = reduceApp(oldState, {
type: actions.HIDE_SUB_LOADING_INDICATION,
})
assert.equal(state.isSubLoading, false)
})
it('displays warning', () => {
const state = reduceApp(metamaskState, {
type: actions.DISPLAY_WARNING,
value: 'warning',
})
assert.equal(state.isLoading, false)
assert.equal(state.warning, 'warning')
})
it('hides warning', () => {
const displayWarningState = { warning: 'warning'}
const oldState = {...metamaskState, ...displayWarningState}
const state = reduceApp(oldState, {
type: actions.HIDE_WARNING,
})
assert.equal(state.warning, undefined)
})
it('request to display account export', () => {
const state = reduceApp(metamaskState, {
type: actions.REQUEST_ACCOUNT_EXPORT,
})
assert.equal(state.transForward, true)
assert.equal(state.accountDetail.subview, 'export')
assert.equal(state.accountDetail.accountExport, 'requested')
})
it('completes account export', () => {
const requestAccountExportState = {
accountDetail: {
subview: 'something',
accountExport: 'progress',
},
}
const oldState = {...metamaskState, ...requestAccountExportState}
const state = reduceApp(oldState, {
type: actions.EXPORT_ACCOUNT,
})
assert.equal(state.accountDetail.subview, 'export')
assert.equal(state.accountDetail.accountExport, 'completed')
})
it('shows private key', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_PRIVATE_KEY,
value: 'private key',
})
assert.equal(state.accountDetail.subview, 'export')
assert.equal(state.accountDetail.accountExport, 'completed')
assert.equal(state.accountDetail.privateKey, 'private key')
})
it('shows buy eth view', () => {
const state = reduceApp(metamaskState, {
type: actions.BUY_ETH_VIEW,
value: '0xAddress',
})
assert.equal(state.currentView.name, 'buyEth')
assert.equal(state.currentView.context, 'accountDetail')
assert.equal(state.identity.address, '0xAddress')
assert.equal(state.buyView.subview, 'Coinbase')
assert.equal(state.buyView.amount, '15.00')
assert.equal(state.buyView.buyAddress, '0xAddress')
assert.equal(state.buyView.formView.coinbase, true)
assert.equal(state.buyView.formView.shapeshift, false)
})
it('shows onboarding subview to buy eth', () => {
const state = reduceApp(metamaskState, {
type: actions.ONBOARDING_BUY_ETH_VIEW,
value: '0xAddress',
})
assert.equal(state.currentView.name, 'onboardingBuyEth')
assert.equal(state.currentView.context, 'accountDetail')
assert.equal(state.identity.address, '0xAddress')
})
it('shows coinbase subview', () => {
const appState = {
appState: {
buyView: {
buyAddress: '0xAddress',
amount: '12.00',
},
},
}
const oldState = {...metamaskState, ...appState}
const state = reduceApp(oldState, {
type: actions.COINBASE_SUBVIEW,
})
assert.equal(state.buyView.subview, 'Coinbase')
assert.equal(state.buyView.formView.coinbase, true)
assert.equal(state.buyView.buyAddress, '0xAddress')
assert.equal(state.buyView.amount, '12.00')
})
it('shows shapeshift subview', () => {
const appState = {
appState: {
buyView: {
buyAddress: '0xAddress',
amount: '12.00',
},
},
}
const marketinfo = {
pair: 'BTC_ETH',
rate: 28.91191106,
minerFee: 0.0022,
limit: 0.76617432,
minimum: 0.00015323,
maxLimit: 0.76617432,
}
const coinOptions = {
BTC: {
symbol: 'BTC',
name: 'Bitcoin',
image: 'https://shapeshift.io/images/coins/bitcoin.png',
imageSmall: 'https://shapeshift.io/images/coins-sm/bitcoin.png',
status: 'available',
minerFee: 0.00025,
},
}
const oldState = {...metamaskState, ...appState}
const state = reduceApp(oldState, {
type: actions.SHAPESHIFT_SUBVIEW,
value: {
marketinfo,
coinOptions,
},
})
assert.equal(state.buyView.subview, 'ShapeShift')
assert.equal(state.buyView.formView.shapeshift, true)
assert.deepEqual(state.buyView.formView.marketinfo, marketinfo)
assert.deepEqual(state.buyView.formView.coinOptions, coinOptions)
assert.equal(state.buyView.buyAddress, '0xAddress')
assert.equal(state.buyView.amount, '12.00')
})
it('updates pair', () => {
const coinOptions = {
BTC: {
symbol: 'BTC',
name: 'Bitcoin',
image: 'https://shapeshift.io/images/coins/bitcoin.png',
imageSmall: 'https://shapeshift.io/images/coins-sm/bitcoin.png',
status: 'available',
minerFee: 0.00025,
},
}
const appState = {
appState: {
buyView: {
buyAddress: '0xAddress',
amount: '12.00',
formView: {
coinOptions,
},
},
},
}
const marketinfo = {
pair: 'BTC_ETH',
rate: 28.91191106,
minerFee: 0.0022,
limit: 0.76617432,
minimum: 0.00015323,
maxLimit: 0.76617432,
}
const oldState = {...metamaskState, ...appState}
const state = reduceApp(oldState, {
type: actions.PAIR_UPDATE,
value: {
marketinfo,
},
})
assert.equal(state.buyView.subview, 'ShapeShift')
assert.equal(state.buyView.formView.shapeshift, true)
assert.deepEqual(state.buyView.formView.marketinfo, marketinfo)
assert.deepEqual(state.buyView.formView.coinOptions, coinOptions)
assert.equal(state.buyView.buyAddress, '0xAddress')
assert.equal(state.buyView.amount, '12.00')
})
it('shows QR', () => {
const state = reduceApp(metamaskState, {
type: actions.SHOW_QR,
value: {
message: 'message',
data: 'data',
},
})
assert.equal(state.qrRequested, true)
assert.equal(state.transForward, true)
assert.equal(state.Qr.message, 'message')
assert.equal(state.Qr.data, 'data')
})
it('shows qr view', () => {
const appState = {
appState: {
currentView: {
context: 'accounts',
},
},
}
const oldState = {...metamaskState, ...appState}
const state = reduceApp(oldState, {
type: actions.SHOW_QR_VIEW,
value: {
message: 'message',
data: 'data',
},
})
assert.equal(state.currentView.name, 'qr')
assert.equal(state.currentView.context, 'accounts')
assert.equal(state.transForward, true)
assert.equal(state.Qr.message, 'message')
assert.equal(state.Qr.data, 'data')
})
it('set mouse user state', () => {
const state = reduceApp(metamaskState, {
type: actions.SET_MOUSE_USER_STATE,
value: true,
})
assert.equal(state.isMouseUser, true)
})
it('sets gas loading', () => {
const state = reduceApp(metamaskState, {
type: actions.GAS_LOADING_STARTED,
})
assert.equal(state.gasIsLoading, true)
})
it('unsets gas loading', () => {
const gasLoadingState = { gasIsLoading: true }
const oldState = {...metamaskState, ...gasLoadingState}
const state = reduceApp(oldState, {
type: actions.GAS_LOADING_FINISHED,
})
assert.equal(state.gasIsLoading, false)
})
it('sets network nonce', () => {
const state = reduceApp(metamaskState, {
type: actions.SET_NETWORK_NONCE,
value: '33',
})
assert.equal(state.networkNonce, '33')
})
})

@ -0,0 +1,576 @@
import assert from 'assert'
import reduceMetamask from '../../../../../ui/app/reducers/metamask'
import * as actions from '../../../../../ui/app/actions'
describe('MetaMask Reducers', () => {
it('init state', () => {
const initState = reduceMetamask({metamask:{}}, {})
assert(initState)
})
it('sets revealing seed to true and adds seed words to new state', () => {
const seedWordsState = reduceMetamask({}, {
type: actions.SHOW_NEW_VAULT_SEED,
value: 'test seed words',
})
assert.equal(seedWordsState.seedWords, 'test seed words')
assert.equal(seedWordsState.isRevealingSeedWords, true)
})
it('shows account page', () => {
const seedWordsState = {
metamask: {
seedwords: 'test seed words',
isRevealing: true,
},
}
const state = reduceMetamask(seedWordsState, {
type: actions.SHOW_ACCOUNTS_PAGE,
})
assert.equal(state.seedWords, undefined)
assert.equal(state.isRevealingSeedWords, false)
})
it('shows notice', () => {
const notice = {
id: 0,
read: false,
date: 'Date',
title: 'Title',
body: 'Body',
}
const state = reduceMetamask({}, {
type: actions.SHOW_NOTICE,
value: notice,
})
assert.equal(state.noActiveNotices, false)
assert.equal(state.nextUnreadNotice, notice)
})
it('clears notice', () => {
const notice = {
id: 0,
read: false,
date: 'Date',
title: 'Title',
body: 'Body',
}
const noticesState = {
metamask: {
noActiveNotices: false,
nextUnreadNotice: notice,
},
}
const state = reduceMetamask(noticesState, {
type: actions.CLEAR_NOTICES,
})
assert.equal(state.noActiveNotices, true)
assert.equal(state.nextUnreadNotice, null)
})
it('unlocks MetaMask', () => {
const state = reduceMetamask({}, {
type: actions.UNLOCK_METAMASK,
value: 'test address',
})
assert.equal(state.isUnlocked, true)
assert.equal(state.isInitialized, true)
assert.equal(state.selectedAddress, 'test address')
})
it('locks MetaMask', () => {
const unlockMetaMaskState = {
metamask: {
isUnlocked: true,
isInitialzed: false,
selectedAddress: 'test address',
},
}
const lockMetaMask = reduceMetamask(unlockMetaMaskState, {
type: actions.LOCK_METAMASK,
})
assert.equal(lockMetaMask.isUnlocked, false)
})
it('sets frequent rpc list', () => {
const state = reduceMetamask({}, {
type: actions.SET_RPC_LIST,
value: 'https://custom.rpc',
})
assert.equal(state.frequentRpcList, 'https://custom.rpc')
})
it('sets rpc target', () => {
const state = reduceMetamask({}, {
type: actions.SET_RPC_TARGET,
value: 'https://custom.rpc',
})
assert.equal(state.provider.rpcTarget, 'https://custom.rpc')
})
it('sets provider type', () => {
const state = reduceMetamask({}, {
type: actions.SET_PROVIDER_TYPE,
value: 'provider type',
})
assert.equal(state.provider.type, 'provider type')
})
describe('CompletedTx', () => {
const oldState = {
metamask: {
unapprovedTxs: {
1: {
id: 1,
time: 1538495996507,
status: 'unapproved',
metamaskNetworkId: 4,
loadingDefaults: false,
txParams: {
from: '0xAddress',
to: '0xAddress2',
value: '0x16345785d8a0000',
gas: '0x5208',
gasPrice: '0x3b9aca00',
},
type: 'standard',
},
2: {
test: 'Should persist',
},
},
unapprovedMsgs: {
1: {
id: 2,
msgParams: {
from: '0xAddress',
data: '0xData',
origin: 'test origin',
},
time: 1538498521717,
status: 'unapproved',
type: 'eth_sign',
},
2: {
test: 'Should Persist',
},
},
},
}
it('removes tx from new state if completed in action.', () => {
const state = reduceMetamask(oldState, {
type: actions.COMPLETED_TX,
id: 1,
})
assert.equal(Object.keys(state.unapprovedTxs).length, 1)
assert.equal(state.unapprovedTxs[2].test, 'Should persist')
})
it('removes msg from new state if completed id in action', () => {
const state = reduceMetamask(oldState, {
type: actions.COMPLETED_TX,
id: 1,
})
assert.equal(Object.keys(state.unapprovedMsgs).length, 1)
assert.equal(state.unapprovedTxs[2].test, 'Should persist')
})
})
it('shows new vault seed words and sets isRevealingSeedWords to true', () => {
const showNewVaultSeedState = reduceMetamask({}, {
type: actions.SHOW_NEW_VAULT_SEED,
value: 'test seed words',
})
assert.equal(showNewVaultSeedState.isRevealingSeedWords, true)
assert.equal(showNewVaultSeedState.seedWords, 'test seed words')
})
it('shows account detail', () => {
const state = reduceMetamask({}, {
type: actions.SHOW_ACCOUNT_DETAIL,
value: 'test address',
})
assert.equal(state.isUnlocked, true)
assert.equal(state.isInitialized, true)
assert.equal(state.selectedAddress, 'test address')
})
it('sets select ', () => {
const state = reduceMetamask({}, {
type: actions.SET_SELECTED_TOKEN,
value: 'test token',
})
assert.equal(state.selectedTokenAddress, 'test token')
})
it('sets account label', () => {
const state = reduceMetamask({}, {
type: actions.SET_ACCOUNT_LABEL,
value: {
account: 'test account',
label: 'test label',
},
})
assert.deepEqual(state.identities, { 'test account': { name: 'test label' } })
})
it('sets current fiat', () => {
const value = {
currentCurrency: 'yen',
conversionRate: 3.14,
conversionDate: new Date(2018, 9),
}
const state = reduceMetamask({}, {
type: actions.SET_CURRENT_FIAT,
value,
})
assert.equal(state.currentCurrency, value.currentCurrency)
assert.equal(state.conversionRate, value.conversionRate)
assert.equal(state.conversionDate, value.conversionDate)
})
it('updates tokens', () => {
const newTokens = {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'decimals': 18,
'symbol': 'META',
}
const state = reduceMetamask({}, {
type: actions.UPDATE_TOKENS,
newTokens,
})
assert.deepEqual(state.tokens, newTokens)
})
it('updates send gas limit', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_GAS_LIMIT,
value: '0xGasLimit',
})
assert.equal(state.send.gasLimit, '0xGasLimit')
})
it('updates send gas price', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_GAS_PRICE,
value: '0xGasPrice',
})
assert.equal(state.send.gasPrice, '0xGasPrice')
})
it('toggles account menu ', () => {
const state = reduceMetamask({}, {
type: actions.TOGGLE_ACCOUNT_MENU,
})
assert.equal(state.isAccountMenuOpen, true)
})
it('updates gas total', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_GAS_TOTAL,
value: '0xGasTotal',
})
assert.equal(state.send.gasTotal, '0xGasTotal')
})
it('updates send token balance', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_SEND_TOKEN_BALANCE,
value: '0xTokenBalance',
})
assert.equal(state.send.tokenBalance, '0xTokenBalance')
})
it('updates data', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_SEND_HEX_DATA,
value: '0xData',
})
assert.equal(state.send.data, '0xData')
})
it('updates send to', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_SEND_TO,
value: {
to: '0xAddress',
nickname: 'nickname',
},
})
assert.equal(state.send.to, '0xAddress')
assert.equal(state.send.toNickname, 'nickname')
})
it('update send from', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_SEND_FROM,
value: '0xAddress',
})
assert.equal(state.send.from, '0xAddress')
})
it('update send amount', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_SEND_AMOUNT,
value: '0xAmount',
})
assert.equal(state.send.amount, '0xAmount')
})
it('update send memo', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_SEND_MEMO,
value: '0xMemo',
})
assert.equal(state.send.memo, '0xMemo')
})
it('updates max mode', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_MAX_MODE,
value: true,
})
assert.equal(state.send.maxModeOn, true)
})
it('update send', () => {
const value = {
gasLimit: '0xGasLimit',
gasPrice: '0xGasPrice',
gasTotal: '0xGasTotal',
tokenBalance: '0xBalance',
from: '0xAddress',
to: '0xAddress',
toNickname: '',
maxModeOn: false,
amount: '0xAmount',
memo: '0xMemo',
errors: {},
editingTransactionId: 22,
forceGasMin: '0xGas',
}
const sendState = reduceMetamask({}, {
type: actions.UPDATE_SEND,
value,
})
assert.deepEqual(sendState.send, value)
})
it('clears send', () => {
const initStateSend = {
send:
{ gasLimit: null,
gasPrice: null,
gasTotal: null,
tokenBalance: null,
from: '',
to: '',
amount: '0x0',
memo: '',
errors: {},
maxModeOn: false,
editingTransactionId: null,
forceGasMin: null,
toNickname: '' },
}
const sendState = {
send: {
gasLimit: '0xGasLimit',
gasPrice: '0xGasPrice',
gasTotal: '0xGasTotal',
tokenBalance: '0xBalance',
from: '0xAddress',
to: '0xAddress',
toNickname: '',
maxModeOn: false,
amount: '0xAmount',
memo: '0xMemo',
errors: {},
editingTransactionId: 22,
forceGasMin: '0xGas',
},
}
const state = reduceMetamask(sendState, {
type: actions.CLEAR_SEND,
})
assert.deepEqual(state.send, initStateSend.send)
})
it('updates value of tx by id', () => {
const oldState = {
metamask: {
selectedAddressTxList: [
{
id: 1,
txParams: 'foo',
},
],
},
}
const state = reduceMetamask(oldState, {
type: actions.UPDATE_TRANSACTION_PARAMS,
id: 1,
value: 'bar',
})
assert.equal(state.selectedAddressTxList[0].txParams, 'bar')
})
it('updates pair for shapeshift', () => {
const state = reduceMetamask({}, {
type: actions.PAIR_UPDATE,
value: {
marketinfo: {
pair: 'test pair',
foo: 'bar',
},
},
})
assert.equal(state.tokenExchangeRates['test pair'].pair, 'test pair')
})
it('upates pair and coin options for shapeshift subview', () => {
const state = reduceMetamask({}, {
type: actions.SHAPESHIFT_SUBVIEW,
value: {
marketinfo: {
pair: 'test pair',
},
coinOptions: {
foo: 'bar',
},
},
})
assert.equal(state.coinOptions.foo, 'bar')
assert.equal(state.tokenExchangeRates['test pair'].pair, 'test pair')
})
it('sets blockies', () => {
const state = reduceMetamask({}, {
type: actions.SET_USE_BLOCKIE,
value: true,
})
assert.equal(state.useBlockie, true)
})
it('updates feature flag', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_FEATURE_FLAGS,
value: {
betaUI: true,
skipAnnounceBetaUI: true,
},
})
assert.equal(state.featureFlags.betaUI, true)
assert.equal(state.featureFlags.skipAnnounceBetaUI, true)
})
it('updates network endpoint type', () => {
const state = reduceMetamask({}, {
type: actions.UPDATE_NETWORK_ENDPOINT_TYPE,
value: 'endpoint',
})
assert.equal(state.networkEndpointType, 'endpoint')
})
it('close welcome screen', () => {
const state = reduceMetamask({}, {
type: actions.CLOSE_WELCOME_SCREEN,
})
assert.equal(state.welcomeScreenSeen, true)
})
it('sets current locale', () => {
const state = reduceMetamask({}, {
type: actions.SET_CURRENT_LOCALE,
value: 'ge',
})
assert.equal(state.currentLocale, 'ge')
})
it('sets pending tokens ', () => {
const payload = {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'decimals': 18,
'symbol': 'META',
}
const pendingTokensState = reduceMetamask({}, {
type: actions.SET_PENDING_TOKENS,
payload,
})
assert.deepEqual(pendingTokensState.pendingTokens, payload)
})
it('clears pending tokens', () => {
const payload = {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'decimals': 18,
'symbol': 'META',
}
const pendingTokensState = {
pendingTokens: payload,
}
const state = reduceMetamask(pendingTokensState, {
type: actions.CLEAR_PENDING_TOKENS,
})
assert.deepEqual(state.pendingTokens, {})
})
})

@ -305,6 +305,12 @@ var actions = {
updateFeatureFlags,
UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS',
// Preferences
setPreference,
updatePreferences,
UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
setUseNativeCurrencyAsPrimaryCurrencyPreference,
setMouseUserState,
SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
@ -319,6 +325,9 @@ var actions = {
clearPendingTokens,
createCancelTransaction,
approveProviderRequest,
rejectProviderRequest,
clearApprovedOrigins,
}
module.exports = actions
@ -1868,10 +1877,10 @@ function updateProviderType (type) {
}
}
function setRpcTarget (newRpc) {
function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname = '') {
return (dispatch) => {
log.debug(`background.setRpcTarget: ${newRpc}`)
background.setCustomRpc(newRpc, (err, result) => {
log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`)
background.setCustomRpc(newRpc, chainId, ticker, nickname, (err, result) => {
if (err) {
log.error(err)
return dispatch(actions.displayWarning('Had a problem changing networks!'))
@ -2298,6 +2307,36 @@ function updateFeatureFlags (updatedFeatureFlags) {
}
}
function setPreference (preference, value) {
return dispatch => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.setPreference(preference, value, (err, updatedPreferences) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.updatePreferences(updatedPreferences))
resolve(updatedPreferences)
})
})
}
}
function updatePreferences (value) {
return {
type: actions.UPDATE_PREFERENCES,
value,
}
}
function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) {
return setPreference('useNativeCurrencyAsPrimaryCurrency', value)
}
function setNetworkNonce (networkNonce) {
return {
type: actions.SET_NETWORK_NONCE,
@ -2448,3 +2487,21 @@ function setPendingTokens (pendingTokens) {
payload: tokens,
}
}
function approveProviderRequest (origin) {
return (dispatch) => {
background.approveProviderRequest(origin)
}
}
function rejectProviderRequest (origin) {
return (dispatch) => {
background.rejectProviderRequest(origin)
}
}
function clearApprovedOrigins () {
return (dispatch) => {
background.clearApprovedOrigins()
}
}

@ -101,7 +101,7 @@ class App extends Component {
network,
isMouseUser,
provider,
frequentRpcList,
frequentRpcListDetail,
currentView,
setMouseUserState,
sidebar,
@ -147,7 +147,7 @@ class App extends Component {
// network dropdown
h(NetworkDropdown, {
provider,
frequentRpcList,
frequentRpcListDetail,
}, []),
h(AccountMenu),
@ -230,7 +230,7 @@ App.propTypes = {
alertMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
frequentRpcListDetail: PropTypes.array,
currentView: PropTypes.object,
sidebar: PropTypes.object,
alertOpen: PropTypes.bool,
@ -322,7 +322,7 @@ function mapStateToProps (state) {
forgottenPassword: state.appState.forgottenPassword,
nextUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
currentCurrency: state.metamask.currentCurrency,
isMouseUser: state.appState.isMouseUser,
betaUI: state.metamask.featureFlags.betaUI,

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

Loading…
Cancel
Save