1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00

fix merge conflicts

This commit is contained in:
brunobar79 2018-11-06 20:21:19 -05:00
commit c651212025
191 changed files with 6150 additions and 3753 deletions

View File

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

View File

@ -2,6 +2,26 @@
## Current Develop Branch
## 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)

View File

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

View File

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

View File

@ -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"
},
@ -170,6 +209,9 @@
"connect": {
"message": "Connect"
},
"connectRequest": {
"message": "Connect Request"
},
"connecting": {
"message": "Connecting..."
},
@ -692,9 +734,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"
},
@ -803,7 +863,7 @@
"message": "Primary Currency"
},
"primaryCurrencySettingDescription": {
"message": "Select ETH to prioritize displaying values in ETH. Select Fiat to prioritize displaying values in your selected currency."
"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"
@ -842,9 +902,6 @@
"refundAddress": {
"message": "Your Refund Address"
},
"reject": {
"message": "Reject"
},
"rejectAll": {
"message": "Reject All"
},
@ -1169,12 +1226,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)"
},

View File

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

View File

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

View File

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

View File

@ -1,4 +1,46 @@
{
"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"
},
@ -824,9 +866,6 @@
"refundAddress": {
"message": "Indirizzo di Rimborso"
},
"reject": {
"message": "Reject"
},
"rejectAll": {
"message": "Reject All"
},

View File

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

View File

@ -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": "수락"
},
@ -815,9 +857,6 @@
"refundAddress": {
"message": "환불받을 주소"
},
"reject": {
"message": "거부"
},
"rejectAll": {
"message": "모두 거부"
},

View File

@ -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"
},
@ -435,7 +480,7 @@
"message": "back-up woorden hebben alleen kleine letters"
},
"mainnet": {
"message": "belangrijkste Ethereum-netwerk"
"message": "Main Netwerk"
},
"message": {
"message": "Bericht"

View File

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

View File

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

View File

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

View File

@ -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": "ยอมรับ"
},

View File

@ -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": "ஏற்கவும்"
},

View File

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

View File

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

View File

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

View File

@ -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
app/images/mm-secure.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "4.16.0",
"version": "5.0.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",

View File

@ -23,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,
@ -50,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
@ -58,7 +58,6 @@ const isIE = !!document.documentMode
// Edge 20+
const isEdge = !isIE && !!window.StyleMedia
let ipfsHandle
let popupIsOpen = false
let notificationIsOpen = false
const openMetamaskTabsIDs = {}
@ -164,7 +163,6 @@ async function initialize () {
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.')
ipfsHandle = ipfsContent(initState.NetworkController.provider)
}
//
@ -197,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')
}
}
@ -212,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 },
})
@ -258,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
@ -269,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)
}
@ -459,7 +456,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) => {

View File

@ -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,69 @@ 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 }) => {
switch (action) {
case 'approve-provider-request':
isEnabled = true
injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: {}}))`)
break
case 'reject-provider-request':
injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`)
break
case 'answer-is-approved':
injectScript(`window.dispatchEvent(new CustomEvent('ethereumisapproved', { detail: { isApproved: ${isApproved}, caching: ${caching}}}))`)
break
case 'answer-is-unlocked':
injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`)
break
case 'metamask-set-locked':
isEnabled = false
injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`)
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
@ -210,3 +290,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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ class PreferencesController {
*/
constructor (opts = {}) {
const initState = extend({
frequentRpcList: [],
frequentRpcListDetail: [],
currentAccountTab: 'history',
accountTokens: {},
assetImages: {},
@ -39,14 +39,14 @@ class PreferencesController {
seedWords: null,
forgottenPassword: false,
preferences: {
useETHAsPrimaryCurrency: true,
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
@ -392,19 +392,22 @@ class PreferencesController {
* Adds custom RPC url to state.
*
* @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) {
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 (url !== 'http://localhost:8545') {
rpcList.push(url)
rpcList.push({ rpcUrl: url, chainId, ticker, nickname })
}
this.store.updateState({ frequentRpcList: rpcList })
this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
}
@ -416,23 +419,23 @@ class PreferencesController {
*
*/
removeFromFrequentRpcList (url) {
const rpcList = this.getFrequentRpcList()
const index = rpcList.findIndex((element) => { return element === url })
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
if (index !== -1) {
rpcList.splice(index, 1)
}
this.store.updateState({ frequentRpcList: rpcList })
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
}
/**
@ -564,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
})
@ -580,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`)

View File

@ -0,0 +1,152 @@
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-provider-request' }, { 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' }, { 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

View File

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

View File

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

View File

@ -6,14 +6,21 @@ 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.')
//
// setup plugin communication
@ -27,26 +34,125 @@ var metamaskStream = new LocalMessageDuplexStream({
// compose the inpage provider
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
// set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100)
// Augment the provider with its enable method
inpageProvider.enable = function (options = {}) {
// set up a listener for when MetaMask is locked
window.addEventListener('metamasksetlocked', () => {
isEnabled = false
})
// 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)
}
})
window.removeEventListener('ethereumprovider', providerHandle)
providerHandle = ({ detail }) => {
if (typeof detail.error !== 'undefined') {
reject(detail.error)
} else {
// wait for the publicConfig store to populate with an account
const publicConfig = new Promise((resolve) => {
const { selectedAddress } = inpageProvider.publicConfigStore.getState()
inpageProvider._metamask.isUnlocked().then(unlocked => {
if (!unlocked || selectedAddress) {
resolve()
} else {
inpageProvider.publicConfigStore.on('update', ({ selectedAddress }) => {
selectedAddress && resolve()
})
}
})
})
// wait for the background to update with an account
const ethAccounts = new Promise((resolveAccounts, rejectAccounts) => {
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
if (error) {
rejectAccounts(error)
} else {
resolveAccounts(response.result)
}
})
})
Promise.all([ethAccounts, publicConfig])
.then(([selectedAddress]) => {
isEnabled = true
resolve(selectedAddress)
})
.catch(reject)
}
}
window.addEventListener('ethereumprovider', providerHandle)
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, reject) => {
window.removeEventListener('ethereumisapproved', isApprovedHandle)
isApprovedHandle = ({ detail }) => {
if (typeof detail.error !== 'undefined') {
reject(detail.error)
} else {
if (detail.caching) {
resolve(!!detail.isApproved)
} else {
resolve(false)
}
}
}
window.addEventListener('ethereumisapproved', isApprovedHandle)
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, reject) => {
window.removeEventListener('metamaskisunlocked', isUnlockedHandle)
isUnlockedHandle = ({ detail }) => {
if (typeof detail.error !== 'undefined') {
reject(detail.error)
} else {
resolve(!!detail.isUnlocked)
}
}
window.addEventListener('metamaskisunlocked', isUnlockedHandle)
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, {
@ -57,6 +163,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
//

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ 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
// `TypeError: Failed to Fetch` without any stack or context for the request
// https://github.com/getsentry/sentry-javascript/pull/1293
//
@ -17,9 +17,11 @@ function setupFetchDebugging() {
try {
return await originalFetch.call(window, ...args)
} catch (err) {
console.warn('FetchDebugger - fetch encountered an Error', err)
console.warn('FetchDebugger - overriding stack to point of original call')
err.stack = initialStack
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
}
}

View File

@ -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)
Sentry.configureScope(scope => {
scope.setExtra('isBrave', isBrave)
})
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
}
// make request normally
client._makeRequest(opts)
},
})
client.install()
} catch (err) {
console.warn(err)
}
return report
}
return Raven
}
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) {

View File

@ -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,
})
@ -138,12 +139,12 @@ module.exports = class MetamaskController extends EventEmitter {
this.accountTracker.stop()
}
})
// ensure accountTracker updates balances after network change
this.networkController.on('networkDidChange', () => {
this.accountTracker._updateAccounts()
})
// key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
this.keyringController = new KeyringController({
@ -197,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()
@ -217,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,
@ -246,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))
}
@ -261,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
@ -275,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),
}
@ -345,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
@ -402,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),
@ -433,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),
}
}
@ -978,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
@ -1273,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 }))
@ -1412,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(),
@ -1454,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.addToFrequentRpcList(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
}
@ -1542,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
@ -1571,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()
}
}

View File

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

View File

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

View File

@ -109,7 +109,7 @@
},
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {

View File

@ -152,7 +152,7 @@
},
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {

View File

@ -110,7 +110,7 @@
},
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {

View File

@ -39,7 +39,7 @@
"tokens": [],
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {

View File

@ -111,7 +111,7 @@
},
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {

View File

@ -104,7 +104,7 @@
"send": {},
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
"useNativeCurrencyAsPrimaryCurrency": true
}
},
"appState": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2186
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -19,6 +19,7 @@ const {
openNewPage,
verboseReportOnFailure,
waitUntilXWindowHandles,
switchToWindowWithTitle,
} = require('./helpers')
describe('MetaMask', function () {
@ -266,18 +267,32 @@ 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 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()
assert(addressText.match(/^0x[a-fA-F0-9]{40}$/))

View File

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

View File

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

View File

@ -284,6 +284,22 @@ describe('MetaMask', function () {
})
})
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 customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
await customRpcButton.click()
await delay(regularDelayMs)
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)
})
})
describe('Log out an log back in', () => {
it('logs out of the account', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
@ -371,7 +387,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)
})
})
@ -420,30 +436,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 +491,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 +569,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 +603,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 +635,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 +645,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)
}
})
@ -764,7 +793,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 +827,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)
@ -851,7 +880,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 +926,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)
})
@ -974,7 +1003,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/))
})
@ -1027,7 +1056,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)
})
})

View File

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

View File

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

View File

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

View File

@ -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 ETH', '$0.00 USD')
await customizeGas(assert, 1, 21000, '0.000021 ETH', '$0.03 USD')
await customizeGas(assert, 500, 60000, '0.03 ETH', '$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')

View File

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

View File

@ -418,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()
@ -438,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)
}
@ -453,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 () {
@ -487,20 +513,20 @@ describe('preferences controller', function () {
describe('on updateFrequentRpcList', function () {
it('should add custom RPC url to state', function () {
preferencesController.addToFrequentRpcList('rpc_url')
preferencesController.addToFrequentRpcList('http://localhost:8545')
assert.deepEqual(preferencesController.store.getState().frequentRpcList, ['rpc_url'])
preferencesController.addToFrequentRpcList('rpc_url')
assert.deepEqual(preferencesController.store.getState().frequentRpcList, ['rpc_url'])
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')
assert.deepEqual(preferencesController.store.getState().frequentRpcList, ['rpc_url'])
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().frequentRpcList, [])
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [])
})
})
})

View File

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

View File

@ -309,7 +309,7 @@ var actions = {
setPreference,
updatePreferences,
UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
setUseETHAsPrimaryCurrencyPreference,
setUseNativeCurrencyAsPrimaryCurrencyPreference,
setMouseUserState,
SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
@ -325,6 +325,9 @@ var actions = {
clearPendingTokens,
createCancelTransaction,
approveProviderRequest,
rejectProviderRequest,
clearApprovedOrigins,
}
module.exports = actions
@ -1874,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!'))
@ -2330,8 +2333,8 @@ function updatePreferences (value) {
}
}
function setUseETHAsPrimaryCurrencyPreference (value) {
return setPreference('useETHAsPrimaryCurrency', value)
function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) {
return setPreference('useNativeCurrencyAsPrimaryCurrency', value)
}
function setNetworkNonce (networkNonce) {
@ -2484,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()
}
}

View File

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

View File

@ -6,10 +6,11 @@ const genAccountLink = require('etherscan-link').createAccountLink
const connect = require('react-redux').connect
const Dropdown = require('./dropdown').Dropdown
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
const Identicon = require('./identicon')
const copyToClipboard = require('copy-to-clipboard')
const { checksumAddress } = require('../util')
import Identicon from './identicon'
class AccountDropdowns extends Component {
constructor (props) {
super(props)

View File

@ -7,10 +7,10 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
const Tooltip = require('../tooltip')
import Identicon from '../identicon'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { PRIMARY } from '../../constants/common'

View File

@ -1,7 +1,7 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const Identicon = require('./identicon')
import Identicon from './identicon'
const formatBalance = require('../util').formatBalance
const addressSummary = require('../util').addressSummary

View File

@ -2,13 +2,13 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { matchPath } from 'react-router-dom'
import Identicon from '../identicon'
const {
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
} = require('../../../../app/scripts/lib/enums')
const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes')
const Identicon = require('../identicon')
const NetworkIndicator = require('../network')
export default class AppHeader extends PureComponent {

View File

@ -2,11 +2,11 @@ const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
import TokenBalance from './token-balance'
import Identicon from './identicon'
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../constants/common'
const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { getNativeCurrency, getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { formatBalance } = require('../util')
@ -21,6 +21,7 @@ function mapStateToProps (state) {
return {
account,
network,
nativeCurrency: getNativeCurrency(state),
conversionRate: conversionRateSelector(state),
currentCurrency: getCurrentCurrency(state),
assetImages: getAssetImages(state),
@ -66,10 +67,10 @@ BalanceComponent.prototype.renderTokenBalance = function () {
BalanceComponent.prototype.renderBalance = function () {
const props = this.props
const { account } = props
const { account, nativeCurrency } = props
const balanceValue = account && account.balance
const needsParse = 'needsParse' in props ? props.needsParse : true
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...'
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse, nativeCurrency) : '...'
const showFiat = 'showFiat' in props ? props.showFiat : true
if (formattedBalance === 'None' || formattedBalance === '...') {
@ -81,11 +82,12 @@ BalanceComponent.prototype.renderBalance = function () {
}
return h('div.flex-column.balance-display', {}, [
h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, {
h(UserPreferencedCurrencyDisplay, {
className: 'token-amount',
value: balanceValue,
type: PRIMARY,
ethNumberOfDecimals: 3,
})),
}),
showFiat && h(UserPreferencedCurrencyDisplay, {
value: balanceValue,

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { ETH, GWEI } from '../../constants/common'
import { GWEI } from '../../constants/common'
export default class CurrencyDisplay extends PureComponent {
static propTypes = {
@ -10,8 +10,9 @@ export default class CurrencyDisplay extends PureComponent {
prefix: PropTypes.string,
prefixComponent: PropTypes.node,
style: PropTypes.object,
suffix: PropTypes.string,
// Used in container
currency: PropTypes.oneOf([ETH]),
currency: PropTypes.string,
denomination: PropTypes.oneOf([GWEI]),
value: PropTypes.string,
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@ -19,17 +20,25 @@ export default class CurrencyDisplay extends PureComponent {
}
render () {
const { className, displayValue, prefix, prefixComponent, style } = this.props
const { className, displayValue, prefix, prefixComponent, style, suffix } = this.props
const text = `${prefix || ''}${displayValue}`
const title = `${text} ${suffix}`
return (
<div
className={classnames('currency-display-component', className)}
style={style}
title={text}
title={title}
>
{ prefixComponent}
<span className="currency-display-component__text">{ text }</span>
{
suffix && (
<span className="currency-display-component__suffix">
{ suffix }
</span>
)
}
</div>
)
}

View File

@ -3,16 +3,17 @@ import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
const mapStateToProps = state => {
const { metamask: { currentCurrency, conversionRate } } = state
const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
return {
currentCurrency,
conversionRate,
nativeCurrency,
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { currentCurrency, conversionRate, ...restStateProps } = stateProps
const { nativeCurrency, currentCurrency, conversionRate, ...restStateProps } = stateProps
const {
value,
numberOfDecimals = 2,
@ -24,16 +25,17 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
const toCurrency = currency || currentCurrency
const convertedValue = getValueFromWeiHex({
value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
value, fromCurrency: nativeCurrency, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
})
const formattedValue = formatCurrency(convertedValue, toCurrency)
const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}`
const displayValue = formatCurrency(convertedValue, toCurrency)
const suffix = hideLabel ? undefined : toCurrency.toUpperCase()
return {
...restStateProps,
...dispatchProps,
...restOwnProps,
displayValue,
suffix,
}
}

View File

@ -7,4 +7,8 @@
overflow: hidden;
text-overflow: ellipsis;
}
&__suffix {
padding-left: 4px;
}
}

View File

@ -20,12 +20,14 @@ describe('CurrencyDisplay container', () => {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
},
}
assert.deepEqual(mapStateToProps(mockState), {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
})
})
})
@ -35,6 +37,7 @@ describe('CurrencyDisplay container', () => {
const mockStateProps = {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
}
const tests = [
@ -43,71 +46,93 @@ describe('CurrencyDisplay container', () => {
value: '0x2386f26fc10000',
numberOfDecimals: 2,
currency: 'usd',
nativeCurrency: 'ETH',
},
result: {
displayValue: '$2.80 USD',
displayValue: '$2.80',
suffix: 'USD',
nativeCurrency: 'ETH',
},
},
{
props: {
value: '0x2386f26fc10000',
currency: 'usd',
nativeCurrency: 'ETH',
},
result: {
displayValue: '$2.80 USD',
displayValue: '$2.80',
suffix: 'USD',
nativeCurrency: 'ETH',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 3,
},
result: {
displayValue: '1.266 ETH',
displayValue: '1.266',
suffix: 'ETH',
nativeCurrency: 'ETH',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 3,
hideLabel: true,
},
result: {
nativeCurrency: 'ETH',
displayValue: '1.266',
suffix: undefined,
},
},
{
props: {
value: '0x3b9aca00',
currency: 'ETH',
nativeCurrency: 'ETH',
denomination: 'GWEI',
hideLabel: true,
},
result: {
nativeCurrency: 'ETH',
displayValue: '1',
suffix: undefined,
},
},
{
props: {
value: '0x3b9aca00',
currency: 'ETH',
nativeCurrency: 'ETH',
denomination: 'WEI',
hideLabel: true,
},
result: {
nativeCurrency: 'ETH',
displayValue: '1000000000',
suffix: undefined,
},
},
{
props: {
value: '0x3b9aca00',
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 100,
hideLabel: true,
},
result: {
nativeCurrency: 'ETH',
displayValue: '1e-9',
suffix: undefined,
},
},
]

View File

@ -14,6 +14,7 @@ export default class CurrencyInput extends PureComponent {
static propTypes = {
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
nativeCurrency: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func,
suffix: PropTypes.string,
@ -77,13 +78,13 @@ export default class CurrencyInput extends PureComponent {
}
renderConversionComponent () {
const { useFiat, currentCurrency } = this.props
const { useFiat, currentCurrency, nativeCurrency } = this.props
const { hexValue } = this.state
let currency, numberOfDecimals
if (useFiat) {
// Display ETH
currency = ETH
currency = nativeCurrency || ETH
numberOfDecimals = 6
} else {
// Display Fiat

View File

@ -3,18 +3,19 @@ import CurrencyInput from './currency-input.component'
import { ETH } from '../../constants/common'
const mapStateToProps = state => {
const { metamask: { currentCurrency, conversionRate } } = state
const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
return {
nativeCurrency,
currentCurrency,
conversionRate,
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { currentCurrency } = stateProps
const { nativeCurrency, currentCurrency } = stateProps
const { useFiat } = ownProps
const suffix = useFiat ? currentCurrency.toUpperCase() : ETH
const suffix = useFiat ? currentCurrency.toUpperCase() : nativeCurrency || ETH
return {
...stateProps,

View File

@ -22,6 +22,7 @@ describe('CurrencyInput Component', () => {
it('should render properly with a suffix', () => {
const mockStore = {
metamask: {
nativeCurrency: 'ETH',
currentCurrency: 'usd',
conversionRate: 231.06,
},
@ -32,6 +33,7 @@ describe('CurrencyInput Component', () => {
<Provider store={store}>
<CurrencyInput
suffix="ETH"
nativeCurrency="ETH"
/>
</Provider>
)
@ -45,6 +47,7 @@ describe('CurrencyInput Component', () => {
it('should render properly with an ETH value', () => {
const mockStore = {
metamask: {
nativeCurrency: 'ETH',
currentCurrency: 'usd',
conversionRate: 231.06,
},
@ -56,6 +59,7 @@ describe('CurrencyInput Component', () => {
<CurrencyInput
value="de0b6b3a7640000"
suffix="ETH"
nativeCurrency="ETH"
currentCurrency="usd"
conversionRate={231.06}
/>
@ -69,12 +73,13 @@ describe('CurrencyInput Component', () => {
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD')
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
})
it('should render properly with a fiat value', () => {
const mockStore = {
metamask: {
nativeCurrency: 'ETH',
currentCurrency: 'usd',
conversionRate: 231.06,
},
@ -87,6 +92,7 @@ describe('CurrencyInput Component', () => {
value="f602f2234d0ea"
suffix="USD"
useFiat
nativeCurrency="ETH"
currentCurrency="usd"
conversionRate={231.06}
/>
@ -100,7 +106,7 @@ describe('CurrencyInput Component', () => {
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD')
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH')
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
})
})
@ -116,6 +122,7 @@ describe('CurrencyInput Component', () => {
it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
const mockStore = {
metamask: {
nativeCurrency: 'ETH',
currentCurrency: 'usd',
conversionRate: 231.06,
},
@ -127,6 +134,7 @@ describe('CurrencyInput Component', () => {
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
suffix="ETH"
nativeCurrency="ETH"
currentCurrency="usd"
conversionRate={231.06}
/>
@ -140,14 +148,14 @@ describe('CurrencyInput Component', () => {
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
assert.equal(currencyInputInstance.state.decimalValue, 0)
assert.equal(currencyInputInstance.state.hexValue, undefined)
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD')
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD')
const input = wrapper.find('input')
assert.equal(input.props().value, 0)
input.simulate('change', { target: { value: 1 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000'))
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD')
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD')
assert.equal(currencyInputInstance.state.decimalValue, 1)
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
@ -160,6 +168,7 @@ describe('CurrencyInput Component', () => {
it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
const mockStore = {
metamask: {
nativeCurrency: 'ETH',
currentCurrency: 'usd',
conversionRate: 231.06,
},
@ -171,6 +180,7 @@ describe('CurrencyInput Component', () => {
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
suffix="USD"
nativeCurrency="ETH"
currentCurrency="usd"
conversionRate={231.06}
useFiat
@ -185,14 +195,14 @@ describe('CurrencyInput Component', () => {
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
assert.equal(currencyInputInstance.state.decimalValue, 0)
assert.equal(currencyInputInstance.state.hexValue, undefined)
assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH')
assert.equal(wrapper.find('.currency-display-component').text(), '0ETH')
const input = wrapper.find('input')
assert.equal(input.props().value, 0)
input.simulate('change', { target: { value: 1 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith('f602f2234d0ea'))
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH')
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH')
assert.equal(currencyInputInstance.state.decimalValue, 1)
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
@ -205,6 +215,7 @@ describe('CurrencyInput Component', () => {
it('should change the state and pass in a new decimalValue when props.value changes', () => {
const mockStore = {
metamask: {
nativeCurrency: 'ETH',
currentCurrency: 'usd',
conversionRate: 231.06,
},
@ -216,6 +227,7 @@ describe('CurrencyInput Component', () => {
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
suffix="USD"
nativeCurrency="ETH"
currentCurrency="usd"
conversionRate={231.06}
useFiat

View File

@ -20,12 +20,14 @@ describe('CurrencyInput container', () => {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
},
}
assert.deepEqual(mapStateToProps(mockState), {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
})
})
})
@ -35,12 +37,14 @@ describe('CurrencyInput container', () => {
const mockStateProps = {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
}
const mockDispatchProps = {}
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
useFiat: true,
suffix: 'USD',
})
@ -48,6 +52,7 @@ describe('CurrencyInput container', () => {
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
suffix: 'ETH',
})
})

View File

@ -6,7 +6,7 @@ const genAccountLink = require('../../../../lib/account-link.js')
const connect = require('react-redux').connect
const Dropdown = require('./dropdown').Dropdown
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
const Identicon = require('../../identicon')
import Identicon from '../../identicon'
const { checksumAddress } = require('../../../util')
const copyToClipboard = require('copy-to-clipboard')
const { formatBalance } = require('../../../util')
@ -26,14 +26,14 @@ class AccountDropdowns extends Component {
}
renderAccounts () {
const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props
const { identities, accounts, selected, menuItemStyles, actions, keyrings, ticker } = this.props
return Object.keys(identities).map((key, index) => {
const identity = identities[key]
const isSelected = identity.address === selected
const balanceValue = accounts[key].balance
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, true, ticker) : '...'
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
@ -421,6 +421,7 @@ AccountDropdowns.propTypes = {
network: PropTypes.number,
// actions.showExportPrivateKeyModal: ,
style: PropTypes.object,
ticker: PropTypes.string,
enableAccountsSelector: PropTypes.bool,
enableAccountOption: PropTypes.bool,
enableAccountOptions: PropTypes.bool,
@ -458,6 +459,7 @@ const mapDispatchToProps = (dispatch) => {
function mapStateToProps (state) {
return {
ticker: state.metamask.ticker,
keyrings: state.metamask.keyrings,
sidebarOpen: state.appState.sidebar.isOpen,
}

View File

@ -24,8 +24,9 @@ const notToggleElementClassnames = [
function mapStateToProps (state) {
return {
provider: state.metamask.provider,
frequentRpcList: state.metamask.frequentRpcList || [],
frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
networkDropdownOpen: state.appState.networkDropdownOpen,
network: state.metamask.network,
}
}
@ -40,8 +41,8 @@ function mapDispatchToProps (dispatch) {
setDefaultRpcTarget: type => {
dispatch(actions.setDefaultRpcTarget(type))
},
setRpcTarget: (target) => {
dispatch(actions.setRpcTarget(target))
setRpcTarget: (target, network, ticker, nickname) => {
dispatch(actions.setRpcTarget(target, network, ticker, nickname))
},
delRpcTarget: (target) => {
dispatch(actions.delRpcTarget(target))
@ -71,7 +72,7 @@ module.exports = compose(
NetworkDropdown.prototype.render = function () {
const props = this.props
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
const rpcList = props.frequentRpcList
const rpcListDetail = props.frequentRpcListDetail
const isOpen = this.props.networkDropdownOpen
const dropdownMenuItemStyle = {
fontSize: '16px',
@ -225,7 +226,7 @@ NetworkDropdown.prototype.render = function () {
),
this.renderCustomOption(props.provider),
this.renderCommonRpc(rpcList, props.provider),
this.renderCommonRpc(rpcListDetail, props.provider),
h(
DropdownMenuItem,
@ -267,28 +268,33 @@ NetworkDropdown.prototype.getNetworkName = function () {
} else if (providerName === 'rinkeby') {
name = this.context.t('rinkeby')
} else {
name = this.context.t('unknownNetwork')
name = provider.nickname || this.context.t('unknownNetwork')
}
return name
}
NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) {
const props = this.props
const reversedRpcList = rpcList.slice().reverse()
const reversedRpcListDetail = rpcListDetail.slice().reverse()
const network = props.network
return reversedRpcList.map((rpc) => {
return reversedRpcListDetail.map((entry) => {
const rpc = entry.rpcUrl
const ticker = entry.ticker || 'ETH'
const nickname = entry.nickname || ''
const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
return null
} else {
const chainId = entry.chainId || network
return h(
DropdownMenuItem,
{
key: `common${rpc}`,
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget(rpc),
onClick: () => props.setRpcTarget(rpc, chainId, ticker, nickname),
style: {
fontSize: '16px',
lineHeight: '20px',
@ -302,7 +308,7 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
style: {
color: currentRpcTarget ? '#ffffff' : '#9b9b9b',
},
}, rpc),
}, nickname || rpc),
h('i.fa.fa-times.delete',
{
onClick: (e) => {
@ -317,8 +323,9 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
}
NetworkDropdown.prototype.renderCustomOption = function (provider) {
const { rpcTarget, type } = provider
const { rpcTarget, type, ticker, nickname } = provider
const props = this.props
const network = props.network
if (type !== 'rpc') return null
@ -332,7 +339,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) {
DropdownMenuItem,
{
key: rpcTarget,
onClick: () => props.setRpcTarget(rpcTarget),
onClick: () => props.setRpcTarget(rpcTarget, network, ticker, nickname),
closeMenu: () => this.props.hideNetworkDropdown(),
style: {
fontSize: '16px',
@ -347,7 +354,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) {
style: {
color: '#ffffff',
},
}, rpcTarget),
}, nickname || rpcTarget),
]
)
}

View File

@ -45,8 +45,8 @@ describe('Network Dropdown', () => {
provider: {
'type': 'test',
},
frequentRpcList: [
'http://localhost:7545',
frequentRpcListDetail: [
{ rpcUrl: 'http://localhost:7545' },
],
},
appState: {

View File

@ -1,5 +1,6 @@
const { Component } = require('react')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const { inherits } = require('util')
const {
formatBalance,
@ -8,7 +9,12 @@ const {
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 () {
@ -17,9 +23,9 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () {
const props = this.props
const { value, style, width, needsParse = true } = props
const { ticker, value, style, width, needsParse = true } = props
const formattedValue = value ? formatBalance(value, 6, needsParse) : '...'
const formattedValue = value ? formatBalance(value, 6, needsParse, ticker) : '...'
return (

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