diff --git a/.circleci/config.yml b/.circleci/config.yml index 084ddb365..b5eb6fe85 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -74,6 +74,9 @@ workflows: - prep-build # - prep-docs - all-tests-pass + - coveralls-upload: + requires: + - test-unit jobs: create_release_pull_request: @@ -204,7 +207,7 @@ jobs: at: . - run: name: test:e2e:firefox - command: yarn build:test && yarn test:e2e:chrome + command: yarn build:test && yarn test:e2e:firefox no_output_timeout: 20m - store_artifacts: path: test-artifacts @@ -257,6 +260,11 @@ jobs: - run: name: test:coverage command: yarn test:coverage + - persist_to_workspace: + root: . + paths: + - .nyc_output + - coverage test-mozilla-lint: docker: - image: circleci/node:10.16-browsers @@ -303,6 +311,17 @@ jobs: name: All Tests Passed command: echo 'weew - everything passed!' + coveralls-upload: + docker: + - image: circleci/node:10.16-browsers + steps: + - checkout + - attach_workspace: + at: . + - run: + name: Coveralls upload + command: yarn test:coveralls-upload + create_github_release: docker: - image: circleci/node:8.15.1-browsers diff --git a/CHANGELOG.md b/CHANGELOG.md index 93dc1b0b6..13790627d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## Current Develop Branch +## 7.1.0 Fri Aug 16 2019 +- [#7035](https://github.com/MetaMask/metamask-extension/pull/7035): Filter non-ERC-20 assets during mobile sync (#7035) +- [#7021](https://github.com/MetaMask/metamask-extension/pull/7021): Using translated string for end of flow messaging (#7021) +- [#7018](https://github.com/MetaMask/metamask-extension/pull/7018): Rename Contacts List settings tab to Contacts (#7018) +- [#7013](https://github.com/MetaMask/metamask-extension/pull/7013): Connections settings tab (#7013) +- [#6996](https://github.com/MetaMask/metamask-extension/pull/6996): Fetch & display received transactions (#6996) +- [#6991](https://github.com/MetaMask/metamask-extension/pull/6991): Remove reload from Share Address button (#6991) +- [#6978](https://github.com/MetaMask/metamask-extension/pull/6978): Address book fixes (#6978) +- [#6944](https://github.com/MetaMask/metamask-extension/pull/6944): Show recipient alias in confirm header if exists (#6944) +- [#6930](https://github.com/MetaMask/metamask-extension/pull/6930): Add support for eth_signTypedData_v4 (#6930) +- [#7046](https://github.com/MetaMask/metamask-extension/pull/7046): Update Italian translation (#7046) +- [#7047](https://github.com/MetaMask/metamask-extension/pull/7047): Add warning about reload on network change + ## 7.0.1 Thu Aug 08 2019 - [#6975](https://github.com/MetaMask/metamask-extension/pull/6975): Ensure seed phrase backup notification only shows up for new users diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 187ffd95f..bcc294542 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -1,10 +1,4 @@ { - "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" }, @@ -20,12 +14,6 @@ "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í" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 98a8bf972..3e93172ca 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Datenschutzmodus" - }, - "privacyModeDescription": { - "message": "Websites müssen Zugriff anfordern, um Ihre Kontoinformationen anzuzeigen." - }, "exposeAccounts": { "message": "Expose Konten" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 430d1b50c..86bc1397b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1,21 +1,9 @@ { - "shareAddress": { - "message": "Share Address" + "showIncomingTransactions": { + "message": "Show Incoming Transactions" }, - "shareAddressToConnect": { - "message": "Share your address to connect to $1?" - }, - "shareAddressInfo": { - "message": "Sharing your address with $1 will allow you to interact with this dapp. This permission is to protect your privacy by default." - }, - "privacyModeDefault": { - "message": "Privacy Mode is now enabled by default" - }, - "privacyMode": { - "message": "Privacy Mode" - }, - "privacyModeDescription": { - "message": "Websites must request access to view your account information." + "showIncomingTransactionsDescription": { + "message": "Select this to use Etherscan to show incoming transactions in the transactions list" }, "exposeAccounts": { "message": "Expose Accounts" @@ -32,20 +20,35 @@ "confirmClear": { "message": "Are you sure you want to clear approved websites?" }, + "connections": { + "message": "Connections" + }, + "connectionsSettingsDescription": { + "message": "Sites allowed to read your accounts" + }, + "addSite": { + "message": "Add Site" + }, + "addSiteDescription": { + "message": "Manually add a site to allow it access to your accounts, useful for older dapps" + }, + "connected": { + "message": "Connected" + }, + "connectedDescription": { + "message": "The list of sites allowed access to your addresses" + }, + "privacyModeDefault": { + "message": "Privacy Mode is now enabled by default" + }, "contractInteraction": { "message": "Contract Interaction" }, "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" + "message": "Remove all sites" }, "reject": { "message": "Reject" @@ -399,10 +402,10 @@ "connectToTrezor": { "message": "Connect to Trezor" }, - "contactList": { - "message": "Contact List" + "contacts": { + "message": "Contacts" }, - "contactListDescription": { + "contactsSettingsDescription": { "message": "Add, edit, remove, and manage your contacts" }, "continue": { @@ -620,7 +623,7 @@ "message": "If you ever have questions or see something fishy, email support@metamask.io." }, "endOfFlowMessage8": { - "message": "MetaMask cannot recover your seedphrase. Learn more." + "message": "MetaMask cannot recover your seedphrase." }, "endOfFlowMessage9": { "message": "Learn more." diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index bf466fdc2..066dc824c 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Modo privado" - }, - "privacyModeDescription": { - "message": "Los sitios web deben solicitar acceso para ver la información de su cuenta." - }, "exposeAccounts": { "message": "Exponer cuentas" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "Los datos aprobados del sitio web se borraron con éxito." }, - "approvalData": { - "message": "Datos de aprobación" - }, - "approvalDataDescription": { - "message": "Borrar la información privada de modo que todos los sitios deban volver a requerir acceso para acceder a los datos de la cuenta." - }, "clearApprovalData": { "message": "Borrar datos de aprobación" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 70effb3ea..2c9308e76 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1,10 +1,4 @@ { - "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" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 54ddcc02b..d7e87b005 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "गोपनीयता मोड" - }, - "privacyModeDescription": { - "message": "वेबसाइटों को आपकी खाता जानकारी देखने के लिए पहुंच का अनुरोध करना होगा।" - }, "exposeAccounts": { "message": "खातों का पर्दाफाश करें" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "स्वीकृत वेबसाइट डेटा सफलतापूर्वक मंजूरी दे दी।" }, - "approvalData": { - "message": "स्वीकृति डेटा" - }, - "approvalDataDescription": { - "message": "अनुमोदित वेबसाइट डेटा साफ़ करें ताकि सभी साइटों को फिर से अनुमोदन का अनुरोध करना होगा।" - }, "clearApprovalData": { "message": "अनुमोदन डेटा साफ़ करें" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 720c35889..e73d07738 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Mòd Privacy" - }, - "privacyModeDescription": { - "message": "Sou sit entènèt yo dwe mande aksè pou wè enfòmasyon kont ou." - }, "exposeAccounts": { "message": "Ekspoze Kont" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "Done sou sit wèb apwouve yo te klarifye avèk siksè." }, - "approvalData": { - "message": "Done sou vi prive" - }, - "approvalDataDescription": { - "message": "Done sou vi prive klè pou tout sit entènèt yo dwe mande aksè pou wè enfòmasyon kont ankò." - }, "clearApprovalData": { "message": "Klè Done sou vi prive" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index c79f22b71..6b833a7ba 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Modalità privacy" - }, - "privacyModeDescription": { - "message": "I siti Web devono richiedere l'accesso per visualizzare le informazioni del tuo account." - }, "exposeAccounts": { "message": "Esponi Accounts" }, @@ -20,20 +14,35 @@ "confirmClear": { "message": "Sei sicuro di voler cancellare i siti Web approvati?" }, + "connections": { + "message": "Connessioni" + }, + "connectionsSettingsDescription": { + "message": "Siti autorizzati ad accedere ai tuoi accounts" + }, + "addSite": { + "message": "Aggiungi Sito" + }, + "addSiteDescription": { + "message": "Aggiungi un sito autorizzato ad accedere ai tuoi accounts, utile per dapps obsolete" + }, + "connected": { + "message": "Connesso" + }, + "connectedDescription": { + "message": "La lista di siti web autorizzati ad accedere ai tuoi indirizzi" + }, + "privacyModeDefault": { + "message": "La Modalità Privacy è attiva per impostazione predefinita" + }, "contractInteraction": { "message": "Interazione Contratto" }, "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" + "message": "Rimuovi tutti i siti" }, "reject": { "message": "Annulla" @@ -80,12 +89,21 @@ "activityLog": { "message": "log attività" }, + "add": { + "message": "Aggiungi" + }, "address": { "message": "Indirizzo" }, "addNetwork": { "message": "Aggiungi Rete" }, + "addRecipient": { + "message": "Aggiungi Destinatario" + }, + "addressBook": { + "message": "Rubrica" + }, "advanced": { "message": "Avanzate" }, @@ -98,6 +116,18 @@ "addCustomToken": { "message": "Aggiungi un token personalizzato" }, + "addToAddressBook": { + "message": "Aggiungi alla rubrica" + }, + "addToAddressBookModalPlaceholder": { + "message": "es. John D." + }, + "addAlias": { + "message": "Aggiungi alias" + }, + "addEthAddress": { + "message": "Aggiungi un indirizzo Ethereum" + }, "addToken": { "message": "Aggiungi Token" }, @@ -172,6 +202,18 @@ "back": { "message": "Indietro" }, + "backToAll": { + "message": "Torna a Tutti" + }, + "backupApprovalNotice": { + "message": "Per mantenere il portafoglio e i tuoi fondi sicuri fai il backup del tuo codice di recupero segreto." + }, + "backupApprovalInfo": { + "message": "Questo codice segreto è necessario per recuperare il portafoglio in caso di smarrimento del dispositivo, si dimentichi la password, di necessità di reinstallare MetaMask, o volontà di accedere al portafoglio da un altro dispositivo." + }, + "backupNow": { + "message": "Backup" + }, "balance": { "message": "Bilancio:" }, @@ -237,9 +279,15 @@ "bytes": { "message": "Bytes" }, + "off": { + "message": "Off" + }, "ok": { "message": "Ok" }, + "on": { + "message": "On" + }, "optionalBlockExplorerUrl": { "message": "URL del Block Explorer (opzionale)" }, @@ -348,6 +396,12 @@ "connectToTrezor": { "message": "Connettersi al Trezor" }, + "contacts": { + "message": "Contatti" + }, + "contactsSettingsDescription": { + "message": "Aggiungi, modifica, rimuovi e gestisci i tuoi contatti" + }, "continue": { "message": "Continua" }, @@ -454,6 +508,12 @@ "defaultNetwork": { "message": "La rete predefinita per transazioni in Ether è la Rete Ethereum Principale." }, + "delete": { + "message": "Elimina" + }, + "deleteAccount": { + "message": "Elimina Account" + }, "denExplainer": { "message": "Il DEN è il tuo archivio crittato con password dentro MetaMask." }, @@ -493,6 +553,9 @@ "directDepositEtherExplainer": { "message": "Se possiedi già degli Ether, questa è la via più veloce per aggiungere Ether al tuo portafoglio con un deposito diretto." }, + "dismiss": { + "message": "Scarta" + }, "done": { "message": "Finito" }, @@ -520,6 +583,9 @@ "editAccountName": { "message": "Modifica Nome Account" }, + "editContact": { + "message": "Modifica Contatto" + }, "editingTransaction": { "message": "Modifica la transazione" }, @@ -551,11 +617,26 @@ "message": "Se hai delle domande o vedi delle attività sospette, manda una mail a support@metamask.io." }, "endOfFlowMessage8": { - "message": "MetaMask non può recuperare la tua frase seed. Impara di più." + "message": "MetaMask non può recuperare la tua frase seed." + }, + "endOfFlowMessage9": { + "message": "Scopri di più." + }, + "endOfFlowMessage10": { + "message": "Tutto fatto." }, "ensNameNotFound": { "message": "Nome ENS non trovato" }, + "ensRegistrationError": { + "message": "Errore nella registrazione del nome ENS" + }, + "ensNotFoundOnCurrentNetwork": { + "message": "Nome ENS non trovato nella rete corrente. Prova a selezionare la Rete Ethereum Principale" + }, + "enterAnAlias": { + "message": "Inserisci un alias" + }, "enterPassword": { "message": "Inserisci password" }, @@ -568,6 +649,9 @@ "eth": { "message": "ETH" }, + "ethereumPublicAddress": { + "message": "Indirizzo Pubblico Ethereum" + }, "etherscanView": { "message": "Vedi account su Etherscan" }, @@ -656,7 +740,7 @@ "message": "Generando la frase seed..." }, "gasPrice": { - "message": "Prezzo del Gas (GWEI)" + "message": "Prezzo Gas (GWEI)" }, "gasPriceExtremelyLow": { "message": "Prezzo del gas estremamente basso" @@ -878,6 +962,9 @@ "loadingTokens": { "message": "Caricamento Tokens..." }, + "loadMore": { + "message": "Carica altro" + }, "localhost": { "message": "Localhost 8545" }, @@ -899,6 +986,9 @@ "memorizePhrase": { "message": "Memorizza questa frase." }, + "memo": { + "message": "Promemoria" + }, "menu": { "message": "Menu" }, @@ -930,7 +1020,13 @@ "message": "Per favore inserisci la password per confermare che sei te!" }, "myAccounts": { - "message": "Miei Account" + "message": "I Miei Accounts" + }, + "myWalletAccounts": { + "message": "I Miei Accounts" + }, + "myWalletAccountsDescription": { + "message": "Tutti i tuoi account MetaMask saranno automaticamente aggiunti in questa sezione." }, "mustSelectOne": { "message": "Devi selezionare almeno un token." @@ -964,10 +1060,16 @@ "newAccount": { "message": "Nuovo Account" }, + "newAccountDetectedDialogMessage": { + "message": "Rilevato nuovo indirizzo! Clica qua per aggiungerlo alla rubrica." + }, "newAccountNumberName": { "message": "Account $1", "description": "Nome predefinito per il prossimo account da creare nella schermata di creazione account" }, + "newContact": { + "message": "Nuovo Contatto" + }, "newContract": { "message": "Nuovo Contratto" }, @@ -1178,9 +1280,15 @@ "receive": { "message": "Ricevi" }, + "recents": { + "message": "Recenti" + }, "recipientAddress": { "message": "Indirizzo Destinatario" }, + "recipientAddressPlaceholder": { + "message": "Ricerca, indirizzo pubblico (0x), o ENS" + }, "refundAddress": { "message": "Indirizzo di Rimborso" }, @@ -1205,6 +1313,15 @@ "resetAccountDescription": { "message": "Ripristinare il tuo account cancellerà lo storico delle transazioni." }, + "deleteNetwork": { + "message": "Elimina Rete?" + }, + "deleteNetworkDescription": { + "message": "Sei sicuro di voler eliminare questa rete?" + }, + "remindMeLater": { + "message": "Più tardi" + }, "restoreFromSeed": { "message": "Ripristina da una frase seed" }, @@ -1454,7 +1571,7 @@ "message": "ci può essere solo uno spazio tra le parole" }, "speedUp": { - "message": "velocizza" + "message": "Velocizza" }, "speedUpTitle": { "message": "Velocizza Transazione" @@ -1649,6 +1766,9 @@ "transfer": { "message": "Trasferisci" }, + "transferBetweenAccounts": { + "message": "Trasferisci tra i miei accounts" + }, "transferFrom": { "message": "Trasferisci Da" }, @@ -1729,6 +1849,9 @@ "useOldUI": { "message": "Use la vecchia UI" }, + "userName": { + "message": "Username" + }, "validFileImport": { "message": "Devi selezionare un file valido da importare." }, @@ -1738,6 +1861,12 @@ "viewAccount": { "message": "Vedi Account" }, + "viewinExplorer": { + "message": "Vedi in Explorer" + }, + "viewContact": { + "message": "Visualizza Contatto" + }, "viewOnCustomBlockExplorer": { "message": "Vedi su $1" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 49b033997..cde9b30be 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "プライバシーモード" - }, - "privacyModeDescription": { - "message": "ウェブサイトはあなたのアカウント情報を閲覧するためのアクセスを要求する必要があります。" - }, "exposeAccounts": { "message": "アカウントを公開する" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "承認されたウェブサイトデータが正常に消去されました。" }, - "approvalData": { - "message": "承認データ" - }, - "approvalDataDescription": { - "message": "承認されたウェブサイトのデータをクリアすると、すべてのサイトで承認を再度要求する必要があります" - }, "clearApprovalData": { "message": "承認データのクリア" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 541d9dd5c..5a3ce1c1c 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "개인 정보 보호 모드" - }, - "privacyModeDescription": { - "message": "웹 사이트는 계정 정보를 볼 수있는 액세스 권한을 요청해야합니다." - }, "exposeAccounts": { "message": "계정 노출" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "승인 된 웹 사이트 데이터가 성공적으로 삭제되었습니다." }, - "approvalData": { - "message": "승인 데이터" - }, - "approvalDataDescription": { - "message": "승인 된 웹 사이트 데이터를 삭제하여 모든 사이트에서 승인을 다시 요청해야합니다." - }, "clearApprovalData": { "message": "승인 데이터 삭제" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index ade7327de..3159592ac 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Privacy-modus" - }, - "privacyModeDescription": { - "message": "Websites moeten toegang vragen om uw accountgegevens te bekijken." - }, "exposeAccounts": { "message": "Expose Accounts" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 9763bd273..8179e135d 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1,10 +1,4 @@ { - "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" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 5724bce88..ba406d464 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Modo de privacidade" - }, - "privacyModeDescription": { - "message": "Os sites devem solicitar acesso para visualizar as informações da sua conta." - }, "exposeAccounts": { "message": "Expor contas" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index e56441186..1437f6444 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Режим конфиденциальности" - }, - "privacyModeDescription": { - "message": "Веб-сайты должны запрашивать доступ для просмотра информации об учетной записи." - }, "exposeAccounts": { "message": "Открыть счета" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "Утвержденные данные веб-сайта успешно удалены." }, - "approvalData": { - "message": "Данные об утверждении" - }, - "approvalDataDescription": { - "message": "Очистите утвержденные данные веб-сайта, чтобы все сайты снова запросили подтверждение." - }, "clearApprovalData": { "message": "Четкие данные об утверждении" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index f98de674b..89b077dae 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -1,10 +1,4 @@ { - "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" }, @@ -20,12 +14,6 @@ "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í" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 563bae1b1..24f059f3d 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Zasebnostni način" - }, - "privacyModeDescription": { - "message": "Spletne strani morajo zahtevati dovoljenje za ogled podatkov o vašem računu." - }, "privacyNotice": { "message": "Obvestilo o zasebnosti" }, @@ -26,12 +20,6 @@ "clearApprovalDataSuccess": { "message": "Odobrene spletne strani uspešno počiščene." }, - "approvalData": { - "message": "Podatki o odobritvi" - }, - "approvalDataDescription": { - "message": "Počistite seznam odobrenih spletnih strani, tako da bodo morale ponovno zahtevati odobritev." - }, "clearApprovalData": { "message": "Počisti podatke o odobritvi" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index a49cf31ad..7b1503764 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "โหมดความเป็นส่วนตัว" - }, - "privacyModeDescription": { - "message": "เว็บไซต์ต้องขอเข้าถึงเพื่อดูข้อมูลบัญชีของคุณ" - }, "exposeAccounts": { "message": "เปิดเผยบัญชี" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "อนุมัติข้อมูลเว็บไซต์ที่ได้รับอนุมัติแล้ว" }, - "approvalData": { - "message": "ข้อมูลการอนุมัติ" - }, - "approvalDataDescription": { - "message": "ล้างข้อมูลเว็บไซต์ที่ได้รับการอนุมัติเพื่อให้ทุกไซต์ต้องขออนุมัติอีกครั้ง" - }, "clearApprovalData": { "message": "ล้างข้อมูลการอนุมัติ" }, diff --git a/app/_locales/tml/messages.json b/app/_locales/tml/messages.json index 2f883b26b..394558522 100644 --- a/app/_locales/tml/messages.json +++ b/app/_locales/tml/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "தனியுரிமை முறை" - }, - "privacyModeDescription": { - "message": "உங்கள் கணக்குத் தகவலை பார்வையிட வலைத்தளங்கள் அணுகலைக் கோர வேண்டும்." - }, "exposeAccounts": { "message": "கணக்குகளை அம்பலப்படுத்துங்கள்" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "அங்கீகரிக்கப்பட்ட வலைத்தள தரவு வெற்றிகரமாக அழிக்கப்பட்டது." }, - "approvalData": { - "message": "ஒப்புதல் தரவு" - }, - "approvalDataDescription": { - "message": "அங்கீகரிக்கப்பட்ட வலைத்தள தரவை அழிக்கவும், அனைத்து தளங்களும் ஒப்புதல் மீண்டும் கோர வேண்டும்." - }, "clearApprovalData": { "message": "ஒப்புதல் தரவை அழி" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 726024764..c807084c5 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "Gizlilik modu" - }, - "privacyModeDescription": { - "message": "Web siteleri, hesap bilgilerinizi görmek için erişim istemek zorundadır." - }, "exposeAccounts": { "message": "Hesapları Açığa Çıkar" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 622e0d818..38a098311 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1,10 +1,4 @@ { - "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" }, @@ -20,12 +14,6 @@ "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" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index d14b99acb..5cecffba9 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "隐私模式" - }, - "privacyModeDescription": { - "message": "网站必须请求访问权限才能查看您的帐户信息。" - }, "exposeAccounts": { "message": "公开账户" }, @@ -20,12 +14,6 @@ "clearApprovalDataSuccess": { "message": "已批准的网站数据已成功清除。" }, - "approvalData": { - "message": "审批数据" - }, - "approvalDataDescription": { - "message": "清除已批准的网站数据,以便所有网站都必须再次申请" - }, "clearApprovalData": { "message": "清除批准数据" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 2559fc0ad..3f4eecfd3 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -1,10 +1,4 @@ { - "privacyMode": { - "message": "隱私模式" - }, - "privacyModeDescription": { - "message": "網站必須請求訪問權限才能查看您的帳戶資訊" - }, "exposeAccounts": { "message": "公開賬戶" }, @@ -23,12 +17,6 @@ "clearApprovalDataSuccess": { "message": "已批准的網站紀錄已成功清除。" }, - "approvalData": { - "message": "審核紀錄" - }, - "approvalDataDescription": { - "message": "清除之前已批准過的網站審核紀錄,所有網站都必須再次申請" - }, "clearApprovalData": { "message": "清除批准數據" }, diff --git a/app/images/coinswitch_logo.png b/app/images/coinswitch_logo.png index aa8f525be..445ecf02e 100644 Binary files a/app/images/coinswitch_logo.png and b/app/images/coinswitch_logo.png differ diff --git a/app/images/ethereum-metamask-chrome.png b/app/images/ethereum-metamask-chrome.png index ffd0ff146..0b886babb 100644 Binary files a/app/images/ethereum-metamask-chrome.png and b/app/images/ethereum-metamask-chrome.png differ diff --git a/app/images/icon-64.png b/app/images/icon-64.png index 9a1995874..b3019ad65 100644 Binary files a/app/images/icon-64.png and b/app/images/icon-64.png differ diff --git a/app/images/key-32.png b/app/images/key-32.png index ac9ddcae3..ca11cacd9 100644 Binary files a/app/images/key-32.png and b/app/images/key-32.png differ diff --git a/app/images/logo.png b/app/images/logo.png index 3013d583b..db6ba7d6c 100644 Binary files a/app/images/logo.png and b/app/images/logo.png differ diff --git a/app/images/pw128x128.png b/app/images/pw128x128.png index c1ef2b5b2..a0eb1b730 100644 Binary files a/app/images/pw128x128.png and b/app/images/pw128x128.png differ diff --git a/app/images/shapeshift logo.png b/app/images/shapeshift logo.png index a8b47d31e..ac8faba5b 100644 Binary files a/app/images/shapeshift logo.png and b/app/images/shapeshift logo.png differ diff --git a/app/manifest.json b/app/manifest.json index f8c150281..fdcbd0c1b 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "7.0.1", + "version": "7.1.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/background.js b/app/scripts/background.js index 8e65bd5a4..02497d487 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -142,7 +142,6 @@ setupMetamaskMeshMetrics() * @property {Object} infuraNetworkStatus - An object of infura network status checks. * @property {Block[]} recentBlocks - An array of recent blocks, used to calculate an effective but cheap gas price. * @property {Array} shapeShiftTxList - An array of objects describing shapeshift exchange attempts. - * @property {Array} lostAccounts - TODO: Remove this feature. A leftover from the version-3 migration where our seed-phrase library changed to fix a bug where some accounts were mis-generated, but we recovered the old accounts as "lost" instead of losing them. * @property {boolean} forgottenPassword - Returns true if the user has initiated the password recovery screen, is recovering from seed phrase. */ @@ -410,7 +409,7 @@ function setupController (initState, initLangCode) { controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge) - controller.providerApprovalController.store.on('update', updateBadge) + controller.providerApprovalController.memStore.on('update', updateBadge) /** * Updates the Web Extension's "badge" number, on the little fox in the toolbar. @@ -422,7 +421,7 @@ function setupController (initState, initLangCode) { const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount const unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount const unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount - const pendingProviderRequests = controller.providerApprovalController.store.getState().providerRequests.length + const pendingProviderRequests = controller.providerApprovalController.memStore.getState().providerRequests.length const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingProviderRequests if (count) { label = String(count) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 7415c5fe9..db4d5fd63 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -114,7 +114,6 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { async function setupPublicApi (outStream) { const api = { - forceReloadSite: (cb) => cb(null, forceReloadSite()), getSiteMetadata: (cb) => cb(null, getSiteMetadata()), } const dnode = Dnode(api) @@ -307,10 +306,3 @@ async function domIsReady () { // wait for load await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) } - -/** - * Reloads the site - */ -function forceReloadSite () { - window.location.reload() -} diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js new file mode 100644 index 000000000..032ef8a77 --- /dev/null +++ b/app/scripts/controllers/incoming-transactions.js @@ -0,0 +1,284 @@ +const ObservableStore = require('obs-store') +const log = require('loglevel') +const BN = require('bn.js') +const createId = require('../lib/random-id') +const { bnToHex, fetchWithTimeout } = require('../lib/util') +const { + MAINNET_CODE, + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, + ROPSTEN, + RINKEBY, + KOVAN, + MAINNET, +} = require('./network/enums') +const networkTypeToIdMap = { + [ROPSTEN]: String(ROPSTEN_CODE), + [RINKEBY]: String(RINKEYBY_CODE), + [KOVAN]: String(KOVAN_CODE), + [MAINNET]: String(MAINNET_CODE), +} +const fetch = fetchWithTimeout({ + timeout: 30000, +}) + +class IncomingTransactionsController { + + constructor (opts = {}) { + const { + blockTracker, + networkController, + preferencesController, + } = opts + this.blockTracker = blockTracker + this.networkController = networkController + this.preferencesController = preferencesController + this.getCurrentNetwork = () => networkController.getProviderConfig().type + + this._onLatestBlock = async (newBlockNumberHex) => { + const selectedAddress = this.preferencesController.getSelectedAddress() + const newBlockNumberDec = parseInt(newBlockNumberHex, 16) + await this._update({ + address: selectedAddress, + newBlockNumberDec, + }) + } + + const initState = Object.assign({ + incomingTransactions: {}, + incomingTxLastFetchedBlocksByNetwork: { + [ROPSTEN]: null, + [RINKEBY]: null, + [KOVAN]: null, + [MAINNET]: null, + }, + }, opts.initState) + this.store = new ObservableStore(initState) + + this.preferencesController.store.subscribe(pairwise((prevState, currState) => { + const { featureFlags: { showIncomingTransactions: prevShowIncomingTransactions } = {} } = prevState + const { featureFlags: { showIncomingTransactions: currShowIncomingTransactions } = {} } = currState + + if (currShowIncomingTransactions === prevShowIncomingTransactions) { + return + } + + if (prevShowIncomingTransactions && !currShowIncomingTransactions) { + this.stop() + return + } + + this.start() + })) + + this.preferencesController.store.subscribe(pairwise(async (prevState, currState) => { + const { selectedAddress: prevSelectedAddress } = prevState + const { selectedAddress: currSelectedAddress } = currState + + if (currSelectedAddress === prevSelectedAddress) { + return + } + + await this._update({ + address: currSelectedAddress, + }) + })) + + this.networkController.on('networkDidChange', async (newType) => { + const address = this.preferencesController.getSelectedAddress() + await this._update({ + address, + networkType: newType, + }) + }) + } + + start () { + const { featureFlags = {} } = this.preferencesController.store.getState() + const { showIncomingTransactions } = featureFlags + + if (!showIncomingTransactions) { + return + } + + this.blockTracker.removeListener('latest', this._onLatestBlock) + this.blockTracker.addListener('latest', this._onLatestBlock) + } + + stop () { + this.blockTracker.removeListener('latest', this._onLatestBlock) + } + + async _update ({ address, newBlockNumberDec, networkType } = {}) { + try { + const dataForUpdate = await this._getDataForUpdate({ address, newBlockNumberDec, networkType }) + await this._updateStateWithNewTxData(dataForUpdate) + } catch (err) { + log.error(err) + } + } + + async _getDataForUpdate ({ address, newBlockNumberDec, networkType } = {}) { + const { + incomingTransactions: currentIncomingTxs, + incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork, + } = this.store.getState() + + const network = networkType || this.getCurrentNetwork() + const lastFetchBlockByCurrentNetwork = currentBlocksByNetwork[network] + let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec + if (blockToFetchFrom === undefined) { + blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16) + } + + const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll(address, blockToFetchFrom, network) + + return { + latestIncomingTxBlockNumber, + newTxs, + currentIncomingTxs, + currentBlocksByNetwork, + fetchedBlockNumber: blockToFetchFrom, + network, + } + } + + async _updateStateWithNewTxData ({ + latestIncomingTxBlockNumber, + newTxs, + currentIncomingTxs, + currentBlocksByNetwork, + fetchedBlockNumber, + network, + }) { + const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber + ? parseInt(latestIncomingTxBlockNumber, 10) + 1 + : fetchedBlockNumber + 1 + const newIncomingTransactions = { + ...currentIncomingTxs, + } + newTxs.forEach(tx => { newIncomingTransactions[tx.hash] = tx }) + + this.store.updateState({ + incomingTxLastFetchedBlocksByNetwork: { + ...currentBlocksByNetwork, + [network]: newLatestBlockHashByNetwork, + }, + incomingTransactions: newIncomingTransactions, + }) + } + + async _fetchAll (address, fromBlock, networkType) { + try { + const fetchedTxResponse = await this._fetchTxs(address, fromBlock, networkType) + return this._processTxFetchResponse(fetchedTxResponse) + } catch (err) { + log.error(err) + } + } + + async _fetchTxs (address, fromBlock, networkType) { + let etherscanSubdomain = 'api' + const currentNetworkID = networkTypeToIdMap[networkType] + const supportedNetworkTypes = [ROPSTEN, RINKEBY, KOVAN, MAINNET] + + if (supportedNetworkTypes.indexOf(networkType) === -1) { + return {} + } + + if (networkType !== MAINNET) { + etherscanSubdomain = `api-${networkType}` + } + const apiUrl = `https://${etherscanSubdomain}.etherscan.io` + let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1` + + if (fromBlock) { + url += `&startBlock=${parseInt(fromBlock, 10)}` + } + const response = await fetch(url) + const parsedResponse = await response.json() + + return { + ...parsedResponse, + address, + currentNetworkID, + } + } + + _processTxFetchResponse ({ status, result, address, currentNetworkID }) { + if (status !== '0' && result.length > 0) { + const remoteTxList = {} + const remoteTxs = [] + result.forEach((tx) => { + if (!remoteTxList[tx.hash]) { + remoteTxs.push(this._normalizeTxFromEtherscan(tx, currentNetworkID)) + remoteTxList[tx.hash] = 1 + } + }) + + const incomingTxs = remoteTxs.filter(tx => tx.txParams.to && tx.txParams.to.toLowerCase() === address.toLowerCase()) + incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1)) + + let latestIncomingTxBlockNumber = null + incomingTxs.forEach((tx) => { + if ( + tx.blockNumber && + (!latestIncomingTxBlockNumber || + parseInt(latestIncomingTxBlockNumber, 10) < parseInt(tx.blockNumber, 10)) + ) { + latestIncomingTxBlockNumber = tx.blockNumber + } + }) + return { + latestIncomingTxBlockNumber, + txs: incomingTxs, + } + } + return { + latestIncomingTxBlockNumber: null, + txs: [], + } + } + + _normalizeTxFromEtherscan (txMeta, currentNetworkID) { + const time = parseInt(txMeta.timeStamp, 10) * 1000 + const status = txMeta.isError === '0' ? 'confirmed' : 'failed' + return { + blockNumber: txMeta.blockNumber, + id: createId(), + metamaskNetworkId: currentNetworkID, + status, + time, + txParams: { + from: txMeta.from, + gas: bnToHex(new BN(txMeta.gas)), + gasPrice: bnToHex(new BN(txMeta.gasPrice)), + nonce: bnToHex(new BN(txMeta.nonce)), + to: txMeta.to, + value: bnToHex(new BN(txMeta.value)), + }, + hash: txMeta.hash, + transactionCategory: 'incoming', + } + } +} + +module.exports = IncomingTransactionsController + +function pairwise (fn) { + let first = true + let cache + return (value) => { + try { + if (first) { + first = false + return fn(value, value) + } else { + return fn(cache, value) + } + } finally { + cache = value + } + } +} diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index 319c5bf3e..5dcd3a895 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -12,6 +12,7 @@ function createMetamaskMiddleware ({ processEthSignMessage, processTypedMessage, processTypedMessageV3, + processTypedMessageV4, processPersonalMessage, getPendingNonce, }) { @@ -27,6 +28,7 @@ function createMetamaskMiddleware ({ processEthSignMessage, processTypedMessage, processTypedMessageV3, + processTypedMessageV4, processPersonalMessage, }), createPendingNonceMiddleware({ getPendingNonce }), diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index d480834f5..4ed3afb6c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -41,7 +41,7 @@ class PreferencesController { // for convenient testing of pre-release features, and should never // perform sensitive operations. featureFlags: { - privacyMode: true, + showIncomingTransactions: true, }, knownMethodData: {}, participateInMetaMetrics: null, diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 5d565c385..3beda6d53 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -6,27 +6,23 @@ const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware * A controller that services user-approved requests for a full Ethereum provider API */ class ProviderApprovalController extends SafeEventEmitter { - /** - * Determines if caching is enabled - */ - caching = true - /** * Creates a ProviderApprovalController * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) { + constructor ({ closePopup, initState, keyringController, openPopup, preferencesController } = {}) { super() this.closePopup = closePopup this.keyringController = keyringController this.openPopup = openPopup this.preferencesController = preferencesController - this.store = new ObservableStore({ - approvedOrigins: {}, - dismissedOrigins: {}, + this.memStore = new ObservableStore({ providerRequests: [], }) + + const defaultState = { approvedOrigins: {} } + this.store = new ObservableStore(Object.assign(defaultState, initState)) } /** @@ -65,11 +61,17 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} siteImage - The icon of the window requesting full provider access */ _handleProviderRequest (origin, siteTitle, siteImage) { - this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] }) + const { providerRequests } = this.memStore.getState() + this.memStore.updateState({ + providerRequests: [ + ...providerRequests, + { origin, siteTitle, siteImage }, + ], + }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const { approvedOrigins, dismissedOrigins } = this.store.getState() - const originAlreadyHandled = approvedOrigins[origin] || dismissedOrigins[origin] - if (originAlreadyHandled && this.caching && isUnlocked) { + const { approvedOrigins } = this.store.getState() + const originAlreadyHandled = approvedOrigins[origin] + if (originAlreadyHandled && isUnlocked) { return } this.openPopup && this.openPopup() @@ -85,23 +87,20 @@ class ProviderApprovalController extends SafeEventEmitter { this.closePopup() } - const { approvedOrigins, dismissedOrigins, providerRequests } = this.store.getState() - - let _dismissedOrigins = dismissedOrigins - if (dismissedOrigins[origin]) { - _dismissedOrigins = Object.assign({}, dismissedOrigins) - delete _dismissedOrigins[origin] - } - + const { approvedOrigins } = this.store.getState() + const { providerRequests } = this.memStore.getState() + const providerRequest = providerRequests.find((request) => request.origin === origin) const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) this.store.updateState({ approvedOrigins: { ...approvedOrigins, - [origin]: true, + [origin]: { + siteTitle: providerRequest ? providerRequest.siteTitle : null, + siteImage: providerRequest ? providerRequest.siteImage : null, + }, }, - dismissedOrigins: _dismissedOrigins, - providerRequests: remainingProviderRequests, }) + this.memStore.updateState({ providerRequests: remainingProviderRequests }) this.emit(`resolvedRequest:${origin}`, { approved: true }) } @@ -115,53 +114,21 @@ class ProviderApprovalController extends SafeEventEmitter { this.closePopup() } - const { approvedOrigins, providerRequests, dismissedOrigins } = this.store.getState() + const { approvedOrigins } = this.store.getState() + const { providerRequests } = this.memStore.getState() const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) // We're cloning and deleting keys here because we don't want to keep unneeded keys const _approvedOrigins = Object.assign({}, approvedOrigins) delete _approvedOrigins[origin] - this.store.putState({ - approvedOrigins: _approvedOrigins, - providerRequests: remainingProviderRequests, - dismissedOrigins: { - ...dismissedOrigins, - [origin]: true, - }, - }) + this.store.putState({ approvedOrigins: _approvedOrigins }) + this.memStore.putState({ providerRequests: remainingProviderRequests }) this.emit(`resolvedRequest:${origin}`, { approved: false }) } /** - * Silently approves access to a full Ethereum provider API for the origin - * - * @param {string} origin - origin of the domain that had provider access approved - */ - forceApproveProviderRequestByOrigin (origin) { - const { approvedOrigins, dismissedOrigins, providerRequests } = this.store.getState() - const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) - - let _dismissedOrigins = dismissedOrigins - if (dismissedOrigins[origin]) { - _dismissedOrigins = Object.assign({}, dismissedOrigins) - delete _dismissedOrigins[origin] - } - - this.store.updateState({ - approvedOrigins: { - ...approvedOrigins, - [origin]: true, - }, - dismissedOrigins: _dismissedOrigins, - providerRequests: remainingProviderRequests, - }) - - this.emit(`forceResolvedRequest:${origin}`, { approved: true, forced: true }) - } - - /** - * Clears any cached approvals for user-approved origins + * Clears any approvals for user-approved origins */ clearApprovedOrigins () { this.store.updateState({ @@ -176,10 +143,17 @@ class ProviderApprovalController extends SafeEventEmitter { * @returns {boolean} - True if the origin has been approved */ shouldExposeAccounts (origin) { - const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - return !privacyMode || Boolean(this.store.getState().approvedOrigins[origin]) + return Boolean(this.store.getState().approvedOrigins[origin]) } + /** + * Returns a merged state representation + * @return {object} + * @private + */ + _getMergedState () { + return Object.assign({}, this.memStore.getState(), this.store.getState()) + } } module.exports = ProviderApprovalController diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index a94787b05..31e6a1f49 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -61,8 +61,19 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) +let warnedOfAutoRefreshDeprecation = false // augment the provider with its enable method inpageProvider.enable = function ({ force } = {}) { + if ( + !warnedOfAutoRefreshDeprecation && + inpageProvider.autoRefreshOnNetworkChange + ) { + console.warn(`MetaMask: MetaMask will soon stop reloading pages on network change. +If you rely upon this behavior, add a 'networkChanged' event handler to trigger the reload manually: https://metamask.github.io/metamask-docs/API_Reference/Ethereum_Provider#ethereum.on(eventname%2C-callback) +Set 'ethereum.autoRefreshOnNetworkChange' to 'false' to silence this warning: https://metamask.github.io/metamask-docs/API_Reference/Ethereum_Provider#ethereum.autorefreshonnetworkchange' +`) + warnedOfAutoRefreshDeprecation = true + } return new Promise((resolve, reject) => { inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => { if (error || response.error) { diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index b10145f3b..5e2a4cd0a 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -141,6 +141,7 @@ module.exports = class TypedMessageManager extends EventEmitter { }, 'Expected EIP712 typed data') break case 'V3': + case 'V4': let data assert.equal(typeof params, 'object', 'Params should be an object.') assert.ok('data' in params, 'Params must include a data field.') diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 2eb71c0a0..096334794 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -144,6 +144,29 @@ function removeListeners (listeners, emitter) { }) } +function fetchWithTimeout ({ timeout = 120000 } = {}) { + return async function _fetch (url, opts) { + const abortController = new AbortController() + const abortSignal = abortController.signal + const f = fetch(url, { + ...opts, + signal: abortSignal, + }) + + const timer = setTimeout(() => abortController.abort(), timeout) + + try { + const res = await f + clearTimeout(timer) + return res + } catch (e) { + clearTimeout(timer) + throw e + } + } +} + + module.exports = { removeListeners, applyListeners, @@ -154,4 +177,5 @@ module.exports = { hexToBn, bnToHex, BnMultiplyByFraction, + fetchWithTimeout, } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d999bb790..1bf34a074 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -30,6 +30,7 @@ const InfuraController = require('./controllers/infura') const CachedBalancesController = require('./controllers/cached-balances') const OnboardingController = require('./controllers/onboarding') const RecentBlocksController = require('./controllers/recent-blocks') +const IncomingTransactionsController = require('./controllers/incoming-transactions') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TypedMessageManager = require('./lib/typed-message-manager') @@ -54,6 +55,7 @@ const HW_WALLETS_KEYRINGS = [TrezorKeyring.type, LedgerBridgeKeyring.type] const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const sigUtil = require('eth-sig-util') +const contractMap = require('eth-contract-metadata') const { AddressBookController, CurrencyRateController, @@ -62,7 +64,6 @@ const { } = require('gaba') const backEndMetaMetricsEvent = require('./lib/backend-metametrics') - module.exports = class MetamaskController extends EventEmitter { /** @@ -137,6 +138,13 @@ module.exports = class MetamaskController extends EventEmitter { networkController: this.networkController, }) + this.incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: this.blockTracker, + networkController: this.networkController, + preferencesController: this.preferencesController, + initState: initState.IncomingTransactionsController, + }) + // account tracker watches balances, nonces, and any code at their address. this.accountTracker = new AccountTracker({ provider: this.provider, @@ -148,8 +156,10 @@ module.exports = class MetamaskController extends EventEmitter { this.on('controllerConnectionChanged', (activeControllerConnections) => { if (activeControllerConnections > 0) { this.accountTracker.start() + this.incomingTransactionsController.start() } else { this.accountTracker.stop() + this.incomingTransactionsController.stop() } }) @@ -251,6 +261,7 @@ module.exports = class MetamaskController extends EventEmitter { this.providerApprovalController = new ProviderApprovalController({ closePopup: opts.closePopup, + initState: initState.ProviderApprovalController, keyringController: this.keyringController, openPopup: opts.openPopup, preferencesController: this.preferencesController, @@ -268,6 +279,8 @@ module.exports = class MetamaskController extends EventEmitter { InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, OnboardingController: this.onboardingController.store, + ProviderApprovalController: this.providerApprovalController.store, + IncomingTransactionsController: this.incomingTransactionsController.store, }) this.memStore = new ComposableObservableStore(null, { @@ -288,8 +301,11 @@ module.exports = class MetamaskController extends EventEmitter { CurrencyController: this.currencyRateController, ShapeshiftController: this.shapeshiftController, InfuraController: this.infuraController.store, - ProviderApprovalController: this.providerApprovalController.store, OnboardingController: this.onboardingController.store, + // ProviderApprovalController + ProviderApprovalController: this.providerApprovalController.store, + ProviderApprovalControllerMemStore: this.providerApprovalController.memStore, + IncomingTransactionsController: this.incomingTransactionsController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -325,6 +341,7 @@ module.exports = class MetamaskController extends EventEmitter { processEthSignMessage: this.newUnsignedMessage.bind(this), processTypedMessage: this.newUnsignedTypedMessage.bind(this), processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), + processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), getPendingNonce: this.getPendingNonce.bind(this), } @@ -385,10 +402,6 @@ module.exports = class MetamaskController extends EventEmitter { return { ...{ isInitialized }, ...this.memStore.getFlatState(), - ...{ - // TODO: Remove usages of lost accounts - lostAccounts: [], - }, } } @@ -468,7 +481,7 @@ module.exports = class MetamaskController extends EventEmitter { whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), // AddressController - setAddressBook: this.addressBookController.set.bind(this.addressBookController), + setAddressBook: nodeify(this.addressBookController.set, this.addressBookController), removeFromAddressBook: this.addressBookController.delete.bind(this.addressBookController), // AppStateController @@ -507,7 +520,6 @@ module.exports = class MetamaskController extends EventEmitter { // provider approval approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), - forceApproveProviderRequestByOrigin: providerApprovalController.forceApproveProviderRequestByOrigin.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), // onboarding controller @@ -639,8 +651,24 @@ module.exports = class MetamaskController extends EventEmitter { tokens, } = this.preferencesController.store.getState() + // Filter ERC20 tokens + const filteredAccountTokens = {} + Object.keys(accountTokens).forEach(address => { + const checksummedAddress = ethUtil.toChecksumAddress(address) + filteredAccountTokens[checksummedAddress] = {} + Object.keys(accountTokens[address]).forEach( + networkType => (filteredAccountTokens[checksummedAddress][networkType] = networkType !== 'mainnet' ? + accountTokens[address][networkType] : + accountTokens[address][networkType].filter(({ address }) => { + const tokenAddress = ethUtil.toChecksumAddress(address) + return contractMap[tokenAddress] ? contractMap[tokenAddress].erc20 : true + }) + ) + ) + }) + const preferences = { - accountTokens, + accountTokens: filteredAccountTokens, currentLocale, frequentRpcList, identities, @@ -1114,6 +1142,9 @@ module.exports = class MetamaskController extends EventEmitter { case 'V3': signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) }) break + case 'V4': + signature = sigUtil.signTypedData_v4(privKey, { data: JSON.parse(cleanMsgParams.data) }) + break } } else { signature = await keyring.signTypedData(address, cleanMsgParams.data) @@ -1177,27 +1208,6 @@ module.exports = class MetamaskController extends EventEmitter { * @property string privateKey - The private key of the account. */ - /** - * Probably no longer needed, related to the Version 3 migration. - * Imports a hash of accounts to private keys into the vault. - * - * Described in: - * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd - * - * Uses the array's private keys to create a new Simple Key Pair keychain - * and add it to the keyring controller. - * @deprecated - * @param {Account[]} lostAccounts - - * @returns {Keyring[]} An array of the restored keyrings. - */ - importLostAccounts ({ lostAccounts }) { - const privKeys = lostAccounts.map(acct => acct.privateKey) - return this.keyringController.restoreKeyring({ - type: 'Simple Key Pair', - data: privKeys, - }) - } - //============================================================================= // END (VAULT / KEYRING RELATED METHODS) //============================================================================= @@ -1298,8 +1308,6 @@ module.exports = class MetamaskController extends EventEmitter { const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi) this.setupPublicConfig(mux.createStream('publicConfig'), originDomain) - - this.providerApprovalController.on(`forceResolvedRequest:${originDomain}`, publicApi.forceReloadSite) } /** @@ -1480,10 +1488,6 @@ module.exports = class MetamaskController extends EventEmitter { const publicApi = { // wrap with an await remote - forceReloadSite: async () => { - const remote = await getRemote() - return await pify(remote.forceReloadSite)() - }, getSiteMetadata: async () => { const remote = await getRemote() return await pify(remote.getSiteMetadata)() @@ -1797,4 +1801,3 @@ module.exports = class MetamaskController extends EventEmitter { return this.keyringController.setLocked() } } - diff --git a/app/scripts/migrations/036.js b/app/scripts/migrations/036.js new file mode 100644 index 000000000..5aad16a84 --- /dev/null +++ b/app/scripts/migrations/036.js @@ -0,0 +1,30 @@ +const version = 36 +const clone = require('clone') + +/** + * The purpose of this migration is to remove the {@code privacyMode} feature flag. + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + const { PreferencesController } = state + + if (PreferencesController) { + const featureFlags = PreferencesController.featureFlags || {} + + if (typeof featureFlags.privacyMode !== 'undefined') { + delete featureFlags.privacyMode + } + } + + return state +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index be3328bad..93a4f0a0a 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -44,4 +44,7 @@ module.exports = [ require('./031'), require('./032'), require('./033'), + require('./034'), + require('./035'), + require('./036'), ] diff --git a/development/states/account-list-with-imported.json b/development/states/account-list-with-imported.json index 41d586db6..5ca5283b2 100644 --- a/development/states/account-list-with-imported.json +++ b/development/states/account-list-with-imported.json @@ -60,7 +60,6 @@ ] } ], - "lostAccounts": [], "seedWords": null }, "appState": { diff --git a/development/states/accounts-loose.json b/development/states/accounts-loose.json index df51f0d7e..43f4b0c18 100644 --- a/development/states/accounts-loose.json +++ b/development/states/accounts-loose.json @@ -102,8 +102,7 @@ "aa25854c0379e53c957ac9382e720c577fa31fd5" ] } - ], - "lostAccounts": [] + ] }, "appState": { "menuOpen": false, diff --git a/development/states/add-token.json b/development/states/add-token.json index 6de25664a..7ac344d13 100644 --- a/development/states/add-token.json +++ b/development/states/add-token.json @@ -93,7 +93,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "send": { "gasLimit": null, "gasPrice": null, @@ -104,7 +103,7 @@ "amount": "0x0", "memo": "", "errors": {}, - "warnings": {}, + "warnings": {}, "maxModeOn": false, "editingTransactionId": null }, diff --git a/development/states/compilation-bug.json b/development/states/compilation-bug.json index 588d069d4..84b0c5481 100644 --- a/development/states/compilation-bug.json +++ b/development/states/compilation-bug.json @@ -103,8 +103,7 @@ "keyringTypes": [ "Simple Key Pair", "HD Key Tree" - ], - "lostAccounts": [] + ] }, "appState": { "menuOpen": false, diff --git a/development/states/conf-tx.json b/development/states/conf-tx.json index 3d118a861..b47db9e80 100644 --- a/development/states/conf-tx.json +++ b/development/states/conf-tx.json @@ -191,7 +191,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "frequentRpcListDetail": [] }, "appState": { diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json index 8eb536a31..f03b67202 100644 --- a/development/states/confirm-new-ui.json +++ b/development/states/confirm-new-ui.json @@ -110,7 +110,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "send": { "gasLimit": "0xea60", "gasPrice": "0xba43b7400", diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index d93723f62..ae7f3454d 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -63,6 +63,7 @@ ], "tokens": [], "transactions": {}, + "incomingTransactions": {}, "selectedAddressTxList": [], "unapprovedTxs": {}, "unapprovedMsgs": { @@ -133,7 +134,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "send": { "gasLimit": "0xea60", "gasPrice": "0xba43b7400", diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index af6d1a4e3..dff527f5a 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -64,6 +64,7 @@ ], "tokens": [], "transactions": {}, + "incomingTransactions": {}, "selectedAddressTxList": [], "unapprovedMsgs": {}, "unapprovedMsgCount": 0, @@ -95,7 +96,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "send": { "gasLimit": null, "gasPrice": null, diff --git a/development/states/first-time.json b/development/states/first-time.json index 2c634160f..c6c4899d8 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -34,7 +34,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "tokens": [], "currentLocale": "en", "preferences": { diff --git a/development/states/import-private-key-warning.json b/development/states/import-private-key-warning.json index 80ebc650d..05ad753dc 100644 --- a/development/states/import-private-key-warning.json +++ b/development/states/import-private-key-warning.json @@ -72,8 +72,7 @@ "01208723ba84e15da2e71656544a2963b0c06d40" ] } - ], - "lostAccounts": [] + ] }, "appState": { "menuOpen": false, diff --git a/development/states/import-private-key.json b/development/states/import-private-key.json index bd455c6d5..ed35640b4 100644 --- a/development/states/import-private-key.json +++ b/development/states/import-private-key.json @@ -44,8 +44,7 @@ "01208723ba84e15da2e71656544a2963b0c06d40" ] } - ], - "lostAccounts": [] + ] }, "appState": { "menuOpen": false, diff --git a/development/states/lost-accounts.json b/development/states/lost-accounts.json deleted file mode 100644 index 283b01815..000000000 --- a/development/states/lost-accounts.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "metamask": { - "currentCurrency": "USD", - "lostAccounts": [ - "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", - "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" - ], - "conversionRate": 11.06608791, - "conversionDate": 1470421024, - "isInitialized": true, - "isUnlocked": true, - "currentDomain": "example.com", - "rpcTarget": "https://rawtestrpc.metamask.io/", - "identities": { - "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { - "name": "Wallet 1", - "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", - "mayBeFauceting": false - }, - "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { - "name": "Wallet 2", - "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", - "mayBeFauceting": false - }, - "0xeb9e64b93097bc15f01f13eae97015c57ab64823": { - "name": "Wallet 3", - "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823", - "mayBeFauceting": false - }, - "0x704107d04affddd9b66ab9de3dd7b095852e9b69": { - "name": "Wallet 4", - "address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69", - "mayBeFauceting": false - } - }, - "unconfTxs": {}, - "accounts": { - "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { - "code": "0x", - "balance": "0x100000000000", - "nonce": "0x0", - "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" - }, - "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { - "code": "0x", - "nonce": "0x0", - "balance": "0x100000000000", - "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" - }, - "0xeb9e64b93097bc15f01f13eae97015c57ab64823": { - "code": "0x", - "nonce": "0x0", - "balance": "0x100000000000", - "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823" - }, - "0x704107d04affddd9b66ab9de3dd7b095852e9b69": { - "code": "0x", - "balance": "0x0", - "nonce": "0x0", - "address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69" - } - }, - "transactions": [], - "network": "2", - "seedWords": null, - "unconfMsgs": {}, - "messages": [], - "provider": { - "type": "testnet" - }, - "selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" - }, - "appState": { - "menuOpen": false, - "currentView": { - "name": "accountDetail", - "detailView": null, - "context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" - }, - "accountDetail": { - "subview": "transactions" - }, - "currentDomain": "127.0.0.1:9966", - "transForward": true, - "isLoading": false, - "warning": null - }, - "identities": {} -} diff --git a/development/states/navigate-txs.json b/development/states/navigate-txs.json index 584a754f0..4e47f8bca 100644 --- a/development/states/navigate-txs.json +++ b/development/states/navigate-txs.json @@ -232,8 +232,7 @@ "rinkeby": "ok", "ropsten": "ok", "goerli": "ok" - }, - "lostAccounts": [] + } }, "appState": { "shouldClose": false, diff --git a/development/states/pending-tx-insufficient.json b/development/states/pending-tx-insufficient.json index 8c16f6518..feadbc9c0 100644 --- a/development/states/pending-tx-insufficient.json +++ b/development/states/pending-tx-insufficient.json @@ -86,7 +86,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "seedWords": null }, "appState": { diff --git a/development/states/pending-tx.json b/development/states/pending-tx.json index 28c1751bd..82406a59d 100644 --- a/development/states/pending-tx.json +++ b/development/states/pending-tx.json @@ -707,8 +707,7 @@ "rinkeby": "ok", "goerli": "ok" }, - "shapeShiftTxList": [], - "lostAccounts": [] + "shapeShiftTxList": [] }, "appState": { "shouldClose": true, diff --git a/development/states/personal-sign.json b/development/states/personal-sign.json index f1941a19c..1017514a0 100644 --- a/development/states/personal-sign.json +++ b/development/states/personal-sign.json @@ -78,8 +78,7 @@ "provider": { "type": "testnet" }, - "shapeShiftTxList": [], - "lostAccounts": [] + "shapeShiftTxList": [] }, "appState": { "menuOpen": false, diff --git a/development/states/private-key-export-success.json b/development/states/private-key-export-success.json index 2ff3c4d17..8f38895dd 100644 --- a/development/states/private-key-export-success.json +++ b/development/states/private-key-export-success.json @@ -48,7 +48,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "seedWords": null }, "appState": { diff --git a/development/states/private-key-export.json b/development/states/private-key-export.json index db7a53e22..d41bfc2a2 100644 --- a/development/states/private-key-export.json +++ b/development/states/private-key-export.json @@ -48,7 +48,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "seedWords": null }, "appState": { diff --git a/development/states/send-edit.json b/development/states/send-edit.json index 1b33e6edc..b6130643b 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -128,7 +128,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "send": { "gasLimit": "0xea60", "gasPrice": "0xba43b7400", diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index ed690a672..69b4b0568 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -28,6 +28,7 @@ "conversionRate": 1200.88200327, "conversionDate": 1489013762, "noActiveNotices": true, + "incomingTransactions": {}, "frequentRpcList": [], "network": "3", "accounts": { @@ -96,7 +97,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "send": { "gasLimit": null, "gasPrice": null, diff --git a/development/states/send.json b/development/states/send.json index 0dfcca1c4..bc1fb9034 100644 --- a/development/states/send.json +++ b/development/states/send.json @@ -86,7 +86,6 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [], "frequentRpcListDetail": [] }, "appState": { diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index 2b2bda2da..08d1cf263 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -64,6 +64,7 @@ ], "tokens": [], "transactions": {}, + "incomingTransactions": {}, "selectedAddressTxList": [ { "err": { @@ -1053,7 +1054,6 @@ {"depositAddress":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke","depositType":"BTC","key":"shapeshift","response":{"status":"no_deposits","address":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke"},"time":1522347459106}, {"depositAddress":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkkq","depositType":"BTC","key":"shapeshift","response":{"status":"no_deposits","address":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkkq"},"time":1522345459106} ], - "lostAccounts": [], "send": {}, "currentLocale": "en", "preferences": { diff --git a/docs/transaction-flow.png b/docs/transaction-flow.png index d86965a14..1059b60d8 100644 Binary files a/docs/transaction-flow.png and b/docs/transaction-flow.png differ diff --git a/gentests.js b/gentests.js deleted file mode 100644 index 0d87a11cb..000000000 --- a/gentests.js +++ /dev/null @@ -1,241 +0,0 @@ -const fs = require('fs') -const path = require('path') -const async = require('async') -const promisify = require('pify') - -// start(/\.selectors.js/, generateSelectorTest).catch(console.error) -// start(/\.utils.js/, generateUtilTest).catch(console.error) -startContainer(/\.container.js/, generateContainerTest).catch(console.error) - -async function getAllFileNames (dirName) { - const allNames = (await promisify(fs.readdir)(dirName)) - const fileNames = allNames.filter(name => name.match(/^.+\./)) - const dirNames = allNames.filter(name => name.match(/^[^.]+$/)) - - const fullPathDirNames = dirNames.map(d => `${dirName}/${d}`) - const subNameArrays = await promisify(async.map)(fullPathDirNames, getAllFileNames) - let subNames = [] - subNameArrays.forEach(subNameArray => { subNames = [...subNames, ...subNameArray] }) - - return [ - ...fileNames.map(name => dirName + '/' + name), - ...subNames, - ] -} - -/* -async function start (fileRegEx, testGenerator) { - const fileNames = await getAllFileNames('./ui/app') - const sFiles = fileNames.filter(name => name.match(fileRegEx)) - - let sFileMethodNames - let testFilePath - async.each(sFiles, async (sFile, cb) => { - const [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) - sFileMethodNames = Object.keys(require(__dirname + '/' + sFile)) - - testFilePath = sPath.replace('.', '-').replace('.', '.test.') - - await promisify(fs.writeFile)( - `${__dirname}/${sRootPath}tests/${testFilePath}`, - testGenerator(sPath, sFileMethodNames), - 'utf8' - ) - }, (err) => { - console.log(err) - }) - -} -*/ - -async function startContainer (fileRegEx) { - const fileNames = await getAllFileNames('./ui/app') - const sFiles = fileNames.filter(name => name.match(fileRegEx)) - - async.each(sFiles, async (sFile) => { - console.log(`sFile`, sFile) - const [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) - - const testFilePath = sPath.replace('.', '-').replace('.', '.test.') - - await promisify(fs.readFile)( - path.join(__dirname, sFile), - 'utf8', - async (err, result) => { - if (err) { - console.log('Error: ', err) - } else { - console.log(`result`, result.length) - const returnObjectStrings = result - .match(/return\s(\{[\s\S]+?})\n}/g) - .map(str => { - return str - .slice(0, str.length - 1) - .slice(7) - .replace(/\n/g, '') - .replace(/\s\s+/g, ' ') - - }) - const mapStateToPropsAssertionObject = returnObjectStrings[0] - .replace(/\w+:\s\w+\([\w,\s]+\),/g, str => { - const strKey = str.match(/^\w+/)[0] - return strKey + ': \'mock' + str.match(/^\w+/)[0].replace(/^./, c => c.toUpperCase()) + ':mockState\',\n' - }) - .replace(/{\s\w.+/, firstLinePair => `{\n ${firstLinePair.slice(2)}`) - .replace(/\w+:.+,/g, s => ` ${s}`) - .replace(/}/g, s => ` ${s}`) - let mapDispatchToPropsMethodNames - if (returnObjectStrings[1]) { - mapDispatchToPropsMethodNames = returnObjectStrings[1].match(/\s\w+:\s/g).map(str => str.match(/\w+/)[0]) - } - const proxyquireObject = ('{\n ' + result - .match(/import\s{[\s\S]+?}\sfrom\s.+/g) - .map(s => s.replace(/\n/g, '')) - .map((s) => { - const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g) - return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1 - ? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n ' - : proxyKeys[0] + ': () => {},') + ' }' - }) - .join(',\n ') + '\n}') - .replace('{ connect: () => {}, },', `{ - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - },`) - // console.log(`proxyquireObject`, proxyquireObject); - // console.log(`mapStateToPropsAssertionObject`, mapStateToPropsAssertionObject); - // console.log(`mapDispatchToPropsMethodNames`, mapDispatchToPropsMethodNames); - - const containerTest = generateContainerTest(sPath, { - mapStateToPropsAssertionObject, - mapDispatchToPropsMethodNames, - proxyquireObject, - }) - // console.log(`containerTest`, `${__dirname}/${sRootPath}tests/${testFilePath}`, containerTest); - console.log('----') - console.log(`sRootPath`, sRootPath) - console.log(`testFilePath`, testFilePath) - await promisify(fs.writeFile)( - `${__dirname}/${sRootPath}tests/${testFilePath}`, - containerTest, - 'utf8' - ) - } - } - ) - }, (err) => { - console.log('123', err) - }) - -} -/* -function generateMethodList (methodArray) { - return methodArray.map(n => ' ' + n).join(',\n') + ',' -} - -function generateMethodDescribeBlock (methodName, index) { - const describeBlock = - `${index ? ' ' : ''}describe('${methodName}()', () => { - it('should', () => { - const state = {} - - assert.equal(${methodName}(state), ) - }) - })` - return describeBlock -} -*/ -function generateDispatchMethodDescribeBlock (methodName, index) { - const describeBlock = - `${index ? ' ' : ''}describe('${methodName}()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.${methodName}() - assert(dispatchSpy.calledOnce) - }) - })` - return describeBlock -} -/* -function generateMethodDescribeBlocks (methodArray) { - return methodArray - .map((methodName, index) => generateMethodDescribeBlock(methodName, index)) - .join('\n\n') -} -*/ - -function generateDispatchMethodDescribeBlocks (methodArray) { - return methodArray - .map((methodName, index) => generateDispatchMethodDescribeBlock(methodName, index)) - .join('\n\n') -} - -/* -function generateSelectorTest (name, methodArray) { -return `import assert from 'assert' -import { -${generateMethodList(methodArray)} -} from '../${name}' - -describe('${name.match(/^[^.]+/)} selectors', () => { - - ${generateMethodDescribeBlocks(methodArray)} - -})` -} - -function generateUtilTest (name, methodArray) { -return `import assert from 'assert' -import { -${generateMethodList(methodArray)} -} from '../${name}' - -describe('${name.match(/^[^.]+/)} utils', () => { - - ${generateMethodDescribeBlocks(methodArray)} - -})` -} -*/ - -function generateContainerTest (sPath, { - mapStateToPropsAssertionObject, - mapDispatchToPropsMethodNames, - proxyquireObject, -}) { - return `import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -proxyquire('../${sPath}', ${proxyquireObject}) - -describe('${sPath.match(/^[^.]+/)} container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), ${mapStateToPropsAssertionObject}) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - ${mapDispatchToPropsMethodNames ? generateDispatchMethodDescribeBlocks(mapDispatchToPropsMethodNames) : 'delete'} - - }) - -})` -} diff --git a/package.json b/package.json index 22e94e3b9..44505d0f3 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test:web3:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-web3.sh", "test:web3:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-web3.sh", "test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh", - "test:coverage": "nyc --reporter=text --reporter=html npm run test:unit && yarn test:coveralls-upload", + "test:coverage": "nyc --reporter=text --reporter=html npm run test:unit", "test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi", "test:flat": "yarn test:flat:build && karma start test/flat.conf.js", "test:flat:build": "yarn test:flat:build:ui && yarn test:flat:build:tests && yarn test:flat:build:locales", diff --git a/test/data/2-state.json b/test/data/2-state.json index d41a403ff..fe1d15cc1 100644 --- a/test/data/2-state.json +++ b/test/data/2-state.json @@ -64,7 +64,6 @@ "noActiveNotices": true, "shapeShiftTxList": [], "infuraNetworkStatus": {}, - "lostAccounts": [], "seedWords": "debris dizzy just program just float decrease vacant alarm reduce speak stadium", "forgottenPassword": null -} \ No newline at end of file +} diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 122945ec1..0be365249 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1,5 +1,8 @@ { "metamask": { + "featureFlags": { + "showIncomingTransactions": true + }, "network": "4", "identities": { "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { @@ -12,6 +15,7 @@ } }, "cachedBalances": {}, + "incomingTransactions": {}, "unapprovedTxs": { "8393540981007587": { "id": 8393540981007587, diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 8d0942dc6..90f320904 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1247,7 +1247,9 @@ describe('MetaMask', function () { const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) await transferTokens.click() - await closeAllWindowHandlesExcept(driver, [extension, dapp]) + if (process.env.SELENIUM_BROWSER !== 'firefox') { + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + } await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1341,6 +1343,10 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { + if (process.env.SELENIUM_BROWSER === 'firefox') { + this.skip() + } + await driver.wait(async () => { const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 3 @@ -1354,6 +1360,12 @@ describe('MetaMask', function () { }) describe('Tranfers a custom token from dapp when no gas value is specified', () => { + before(function () { + if (process.env.SELENIUM_BROWSER === 'firefox') { + this.skip() + } + }) + it('transfers an already created token, without specifying gas', async () => { const windowHandles = await driver.getAllWindowHandles() const extension = windowHandles[0] @@ -1403,6 +1415,12 @@ describe('MetaMask', function () { }) describe('Approves a custom token from dapp when no gas value is specified', () => { + before(function () { + if (process.env.SELENIUM_BROWSER === 'firefox') { + this.skip() + } + }) + it('approves an already created token', async () => { const windowHandles = await driver.getAllWindowHandles() const extension = windowHandles[0] diff --git a/test/unit/app/controllers/incoming-transactions-test.js b/test/unit/app/controllers/incoming-transactions-test.js new file mode 100644 index 000000000..0f8385af2 --- /dev/null +++ b/test/unit/app/controllers/incoming-transactions-test.js @@ -0,0 +1,658 @@ +const assert = require('assert') +const sinon = require('sinon') +const proxyquire = require('proxyquire') +const IncomingTransactionsController = proxyquire('../../../../app/scripts/controllers/incoming-transactions', { + '../lib/random-id': () => 54321, +}) + +const { + ROPSTEN, + RINKEBY, + KOVAN, + MAINNET, +} = require('../../../../app/scripts/controllers/network/enums') + +describe('IncomingTransactionsController', () => { + const EMPTY_INIT_STATE = { + incomingTransactions: {}, + incomingTxLastFetchedBlocksByNetwork: { + [ROPSTEN]: null, + [RINKEBY]: null, + [KOVAN]: null, + [MAINNET]: null, + }, + } + + const NON_EMPTY_INIT_STATE = { + incomingTransactions: { + '0x123456': { id: 777 }, + }, + incomingTxLastFetchedBlocksByNetwork: { + [ROPSTEN]: 1, + [RINKEBY]: 2, + [KOVAN]: 3, + [MAINNET]: 4, + }, + } + + const NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE = { + incomingTransactions: { + '0x123456': { id: 777 }, + }, + incomingTxLastFetchedBlocksByNetwork: { + [ROPSTEN]: 1, + [RINKEBY]: 2, + [KOVAN]: 3, + [MAINNET]: 4, + FAKE_NETWORK: 1111, + }, + } + + const MOCK_BLOCKTRACKER = { + addListener: sinon.spy(), + removeListener: sinon.spy(), + testProperty: 'fakeBlockTracker', + getCurrentBlock: () => '0xa', + } + + const MOCK_NETWORK_CONTROLLER = { + getProviderConfig: () => ({ type: 'FAKE_NETWORK' }), + on: sinon.spy(), + } + + const MOCK_PREFERENCES_CONTROLLER = { + getSelectedAddress: sinon.stub().returns('0x0101'), + store: { + getState: sinon.stub().returns({ + featureFlags: { + showIncomingTransactions: true, + }, + }), + subscribe: sinon.spy(), + }, + } + + describe('constructor', () => { + it('should set up correct store, listeners and properties in the constructor', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: {}, + }) + sinon.spy(incomingTransactionsController, '_update') + + assert.deepEqual(incomingTransactionsController.blockTracker, MOCK_BLOCKTRACKER) + assert.deepEqual(incomingTransactionsController.networkController, MOCK_NETWORK_CONTROLLER) + assert.equal(incomingTransactionsController.preferencesController, MOCK_PREFERENCES_CONTROLLER) + assert.equal(incomingTransactionsController.getCurrentNetwork(), 'FAKE_NETWORK') + + assert.deepEqual(incomingTransactionsController.store.getState(), EMPTY_INIT_STATE) + + assert(incomingTransactionsController.networkController.on.calledOnce) + assert.equal(incomingTransactionsController.networkController.on.getCall(0).args[0], 'networkDidChange') + const networkControllerListenerCallback = incomingTransactionsController.networkController.on.getCall(0).args[1] + assert.equal(incomingTransactionsController._update.callCount, 0) + networkControllerListenerCallback('testNetworkType') + assert.equal(incomingTransactionsController._update.callCount, 1) + assert.deepEqual(incomingTransactionsController._update.getCall(0).args[0], { + address: '0x0101', + networkType: 'testNetworkType', + }) + + incomingTransactionsController._update.resetHistory() + }) + + it('should set the store to a provided initial state', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + assert.deepEqual(incomingTransactionsController.store.getState(), NON_EMPTY_INIT_STATE) + }) + }) + + describe('#start', () => { + it('should set up a listener for the latest block', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: {}, + }) + sinon.spy(incomingTransactionsController, '_update') + + incomingTransactionsController.start() + + assert(incomingTransactionsController.blockTracker.addListener.calledOnce) + assert.equal(incomingTransactionsController.blockTracker.addListener.getCall(0).args[0], 'latest') + const blockTrackerListenerCallback = incomingTransactionsController.blockTracker.addListener.getCall(0).args[1] + assert.equal(incomingTransactionsController._update.callCount, 0) + blockTrackerListenerCallback('0xabc') + assert.equal(incomingTransactionsController._update.callCount, 1) + assert.deepEqual(incomingTransactionsController._update.getCall(0).args[0], { + address: '0x0101', + newBlockNumberDec: 2748, + }) + }) + }) + + describe('_getDataForUpdate', () => { + it('should call fetchAll with the correct params when passed a new block number and the current network has no stored block', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + incomingTransactionsController._fetchAll = sinon.stub().returns({}) + + await incomingTransactionsController._getDataForUpdate({ address: 'fakeAddress', newBlockNumberDec: 999 }) + + assert(incomingTransactionsController._fetchAll.calledOnce) + + assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [ + 'fakeAddress', 999, 'FAKE_NETWORK', + ]) + }) + + it('should call fetchAll with the correct params when passed a new block number but the current network has a stored block', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE, + }) + incomingTransactionsController._fetchAll = sinon.stub().returns({}) + + await incomingTransactionsController._getDataForUpdate({ address: 'fakeAddress', newBlockNumberDec: 999 }) + + assert(incomingTransactionsController._fetchAll.calledOnce) + + assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [ + 'fakeAddress', 1111, 'FAKE_NETWORK', + ]) + }) + + it('should call fetchAll with the correct params when passed a new network type but no block info exists', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE, + }) + incomingTransactionsController._fetchAll = sinon.stub().returns({}) + + await incomingTransactionsController._getDataForUpdate({ + address: 'fakeAddress', + networkType: 'NEW_FAKE_NETWORK', + }) + + assert(incomingTransactionsController._fetchAll.calledOnce) + + assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [ + 'fakeAddress', 10, 'NEW_FAKE_NETWORK', + ]) + }) + + it('should call fetchAll with the correct params when passed a new block number but the current network has a stored block', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE, + }) + incomingTransactionsController._fetchAll = sinon.stub().returns({}) + + await incomingTransactionsController._getDataForUpdate({ address: 'fakeAddress', newBlockNumberDec: 999 }) + + assert(incomingTransactionsController._fetchAll.calledOnce) + + assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [ + 'fakeAddress', 1111, 'FAKE_NETWORK', + ]) + }) + + it('should return the expected data', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE, + }) + incomingTransactionsController._fetchAll = sinon.stub().returns({ + latestIncomingTxBlockNumber: 444, + txs: [{ id: 555 }], + }) + + const result = await incomingTransactionsController._getDataForUpdate({ + address: 'fakeAddress', + networkType: 'FAKE_NETWORK', + }) + + assert.deepEqual(result, { + latestIncomingTxBlockNumber: 444, + newTxs: [{ id: 555 }], + currentIncomingTxs: { + '0x123456': { id: 777 }, + }, + currentBlocksByNetwork: { + [ROPSTEN]: 1, + [RINKEBY]: 2, + [KOVAN]: 3, + [MAINNET]: 4, + FAKE_NETWORK: 1111, + }, + fetchedBlockNumber: 1111, + network: 'FAKE_NETWORK', + }) + }) + }) + + describe('_updateStateWithNewTxData', () => { + const MOCK_INPUT_WITHOUT_LASTEST = { + newTxs: [{ id: 555, hash: '0xfff' }], + currentIncomingTxs: { + '0x123456': { id: 777, hash: '0x123456' }, + }, + currentBlocksByNetwork: { + [ROPSTEN]: 1, + [RINKEBY]: 2, + [KOVAN]: 3, + [MAINNET]: 4, + FAKE_NETWORK: 1111, + }, + fetchedBlockNumber: 1111, + network: 'FAKE_NETWORK', + } + + const MOCK_INPUT_WITH_LASTEST = { + ...MOCK_INPUT_WITHOUT_LASTEST, + latestIncomingTxBlockNumber: 444, + } + + it('should update state with correct blockhash and transactions when passed a truthy latestIncomingTxBlockNumber', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + sinon.spy(incomingTransactionsController.store, 'updateState') + + await incomingTransactionsController._updateStateWithNewTxData(MOCK_INPUT_WITH_LASTEST) + + assert(incomingTransactionsController.store.updateState.calledOnce) + + assert.deepEqual(incomingTransactionsController.store.updateState.getCall(0).args[0], { + incomingTxLastFetchedBlocksByNetwork: { + ...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork, + 'FAKE_NETWORK': 445, + }, + incomingTransactions: { + '0x123456': { id: 777, hash: '0x123456' }, + '0xfff': { id: 555, hash: '0xfff' }, + }, + }) + }) + + it('should update state with correct blockhash and transactions when passed a falsy latestIncomingTxBlockNumber', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + sinon.spy(incomingTransactionsController.store, 'updateState') + + await incomingTransactionsController._updateStateWithNewTxData(MOCK_INPUT_WITHOUT_LASTEST) + + assert(incomingTransactionsController.store.updateState.calledOnce) + + assert.deepEqual(incomingTransactionsController.store.updateState.getCall(0).args[0], { + incomingTxLastFetchedBlocksByNetwork: { + ...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork, + 'FAKE_NETWORK': 1112, + }, + incomingTransactions: { + '0x123456': { id: 777, hash: '0x123456' }, + '0xfff': { id: 555, hash: '0xfff' }, + }, + }) + }) + }) + + describe('_fetchTxs', () => { + const mockFetch = sinon.stub().returns(Promise.resolve({ + json: () => Promise.resolve({ someKey: 'someValue' }), + })) + let tempFetch + beforeEach(() => { + tempFetch = global.fetch + global.fetch = mockFetch + }) + + afterEach(() => { + global.fetch = tempFetch + mockFetch.resetHistory() + }) + + it('should call fetch with the expected url when passed an address, block number and supported network', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + await incomingTransactionsController._fetchTxs('0xfakeaddress', '789', ROPSTEN) + + assert(mockFetch.calledOnce) + assert.equal(mockFetch.getCall(0).args[0], `https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`) + }) + + it('should call fetch with the expected url when passed an address, block number and MAINNET', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + await incomingTransactionsController._fetchTxs('0xfakeaddress', '789', MAINNET) + + assert(mockFetch.calledOnce) + assert.equal(mockFetch.getCall(0).args[0], `https://api.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`) + }) + + it('should call fetch with the expected url when passed an address and supported network, but a falsy block number', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + await incomingTransactionsController._fetchTxs('0xfakeaddress', null, ROPSTEN) + + assert(mockFetch.calledOnce) + assert.equal(mockFetch.getCall(0).args[0], `https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1`) + }) + + it('should not fetch and return an empty object when passed an unsported network', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + const result = await incomingTransactionsController._fetchTxs('0xfakeaddress', null, 'UNSUPPORTED_NETWORK') + + assert(mockFetch.notCalled) + assert.deepEqual(result, {}) + }) + + it('should return the results from the fetch call, plus the address and currentNetworkID, when passed an address, block number and supported network', async () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + const result = await incomingTransactionsController._fetchTxs('0xfakeaddress', '789', ROPSTEN) + + assert(mockFetch.calledOnce) + assert.deepEqual(result, { + someKey: 'someValue', + address: '0xfakeaddress', + currentNetworkID: 3, + }) + }) + }) + + describe('_processTxFetchResponse', () => { + it('should return a null block number and empty tx array if status is 0', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + const result = incomingTransactionsController._processTxFetchResponse({ + status: '0', + result: [{ id: 1 }], + address: '0xfakeaddress', + }) + + assert.deepEqual(result, { + latestIncomingTxBlockNumber: null, + txs: [], + }) + }) + + it('should return a null block number and empty tx array if the passed result array is empty', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + const result = incomingTransactionsController._processTxFetchResponse({ + status: '1', + result: [], + address: '0xfakeaddress', + }) + + assert.deepEqual(result, { + latestIncomingTxBlockNumber: null, + txs: [], + }) + }) + + it('should return the expected block number and tx list when passed data from a successful fetch', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + incomingTransactionsController._normalizeTxFromEtherscan = (tx, currentNetworkID) => ({ + ...tx, + currentNetworkID, + normalized: true, + }) + + const result = incomingTransactionsController._processTxFetchResponse({ + status: '1', + address: '0xfakeaddress', + currentNetworkID: 'FAKE_NETWORK', + result: [ + { + hash: '0xabc123', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5000, + time: 10, + }, + { + hash: '0xabc123', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5000, + time: 10, + }, + { + hash: '0xabc1234', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5000, + time: 9, + }, + { + hash: '0xabc12345', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5001, + time: 11, + }, + { + hash: '0xabc123456', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5001, + time: 12, + }, + { + hash: '0xabc1234567', + txParams: { + to: '0xanotherFakeaddress', + }, + blockNumber: 5002, + time: 13, + }, + ], + }) + + assert.deepEqual(result, { + latestIncomingTxBlockNumber: 5001, + txs: [ + { + hash: '0xabc1234', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5000, + time: 9, + normalized: true, + currentNetworkID: 'FAKE_NETWORK', + }, + { + hash: '0xabc123', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5000, + time: 10, + normalized: true, + currentNetworkID: 'FAKE_NETWORK', + }, + { + hash: '0xabc12345', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5001, + time: 11, + normalized: true, + currentNetworkID: 'FAKE_NETWORK', + }, + { + hash: '0xabc123456', + txParams: { + to: '0xfakeaddress', + }, + blockNumber: 5001, + time: 12, + normalized: true, + currentNetworkID: 'FAKE_NETWORK', + }, + ], + }) + }) + }) + + describe('_normalizeTxFromEtherscan', () => { + it('should return the expected data when the tx is in error', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + const result = incomingTransactionsController._normalizeTxFromEtherscan({ + timeStamp: '4444', + isError: '1', + blockNumber: 333, + from: '0xa', + gas: '11', + gasPrice: '12', + nonce: '13', + to: '0xe', + value: '15', + hash: '0xg', + }, 'FAKE_NETWORK') + + assert.deepEqual(result, { + blockNumber: 333, + id: 54321, + metamaskNetworkId: 'FAKE_NETWORK', + status: 'failed', + time: 4444000, + txParams: { + from: '0xa', + gas: '0xb', + gasPrice: '0xc', + nonce: '0xd', + to: '0xe', + value: '0xf', + }, + hash: '0xg', + transactionCategory: 'incoming', + }) + }) + + it('should return the expected data when the tx is not in error', () => { + const incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: MOCK_BLOCKTRACKER, + networkController: MOCK_NETWORK_CONTROLLER, + preferencesController: MOCK_PREFERENCES_CONTROLLER, + initState: NON_EMPTY_INIT_STATE, + }) + + const result = incomingTransactionsController._normalizeTxFromEtherscan({ + timeStamp: '4444', + isError: '0', + blockNumber: 333, + from: '0xa', + gas: '11', + gasPrice: '12', + nonce: '13', + to: '0xe', + value: '15', + hash: '0xg', + }, 'FAKE_NETWORK') + + assert.deepEqual(result, { + blockNumber: 333, + id: 54321, + metamaskNetworkId: 'FAKE_NETWORK', + status: 'confirmed', + time: 4444000, + txParams: { + from: '0xa', + gas: '0xb', + gasPrice: '0xc', + nonce: '0xd', + to: '0xe', + value: '0xf', + }, + hash: '0xg', + transactionCategory: 'incoming', + }) + }) + }) +}) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index effd7c0ce..4f642037a 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -758,14 +758,6 @@ describe('MetaMaskController', function () { }) }) - describe('#markAccountsFound', function () { - it('adds lost accounts to config manager data', function () { - metamaskController.markAccountsFound(noop) - const state = metamaskController.getState() - assert.deepEqual(state.lostAccounts, []) - }) - }) - describe('#markPasswordForgotten', function () { it('adds and sets forgottenPassword to config data to true', function () { metamaskController.markPasswordForgotten(noop) diff --git a/test/unit/app/controllers/provider-approval-test.js b/test/unit/app/controllers/provider-approval-test.js new file mode 100644 index 000000000..c12387a33 --- /dev/null +++ b/test/unit/app/controllers/provider-approval-test.js @@ -0,0 +1,260 @@ +const assert = require('assert') +const sinon = require('sinon') +const ProviderApprovalController = require('../../../../app/scripts/controllers/provider-approval') + +const mockLockedKeyringController = { + memStore: { + getState: () => ({ + isUnlocked: false, + }), + }, +} + +const mockUnlockedKeyringController = { + memStore: { + getState: () => ({ + isUnlocked: true, + }), + }, +} + +describe('ProviderApprovalController', () => { + describe('#_handleProviderRequest', () => { + it('should add a pending provider request when unlocked', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + assert.deepEqual(controller._getMergedState(), { + approvedOrigins: {}, + providerRequests: [{ + origin: 'example.com', + siteTitle: 'Example', + siteImage: 'https://example.com/logo.svg', + }], + }) + }) + + it('should add a pending provider request when locked', () => { + const controller = new ProviderApprovalController({ + keyringController: mockLockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + assert.deepEqual(controller._getMergedState(), { + approvedOrigins: {}, + providerRequests: [{ + origin: 'example.com', + siteTitle: 'Example', + siteImage: 'https://example.com/logo.svg', + }], + }) + }) + + it('should add a 2nd pending provider request when unlocked', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example1.com', 'Example 1', 'https://example1.com/logo.svg') + controller._handleProviderRequest('example2.com', 'Example 2', 'https://example2.com/logo.svg') + assert.deepEqual(controller._getMergedState(), { + approvedOrigins: {}, + providerRequests: [{ + origin: 'example1.com', + siteTitle: 'Example 1', + siteImage: 'https://example1.com/logo.svg', + }, { + origin: 'example2.com', + siteTitle: 'Example 2', + siteImage: 'https://example2.com/logo.svg', + }], + }) + }) + + it('should add a 2nd pending provider request when locked', () => { + const controller = new ProviderApprovalController({ + keyringController: mockLockedKeyringController, + }) + + controller._handleProviderRequest('example1.com', 'Example 1', 'https://example1.com/logo.svg') + controller._handleProviderRequest('example2.com', 'Example 2', 'https://example2.com/logo.svg') + assert.deepEqual(controller._getMergedState(), { + approvedOrigins: {}, + providerRequests: [{ + origin: 'example1.com', + siteTitle: 'Example 1', + siteImage: 'https://example1.com/logo.svg', + }, { + origin: 'example2.com', + siteTitle: 'Example 2', + siteImage: 'https://example2.com/logo.svg', + }], + }) + }) + + it('should call openPopup when unlocked and when given', () => { + const openPopup = sinon.spy() + const controller = new ProviderApprovalController({ + openPopup, + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + assert.ok(openPopup.calledOnce) + }) + + it('should call openPopup when locked and when given', () => { + const openPopup = sinon.spy() + const controller = new ProviderApprovalController({ + openPopup, + keyringController: mockLockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + assert.ok(openPopup.calledOnce) + }) + + it('should NOT call openPopup when unlocked and when the domain has already been approved', () => { + const openPopup = sinon.spy() + const controller = new ProviderApprovalController({ + openPopup, + keyringController: mockUnlockedKeyringController, + }) + + controller.store.updateState({ + approvedOrigins: { + 'example.com': { + siteTitle: 'Example', + siteImage: 'https://example.com/logo.svg', + }, + }, + }) + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + assert.ok(openPopup.notCalled) + }) + }) + + describe('#approveProviderRequestByOrigin', () => { + it('should mark the origin as approved and remove the provider request', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller.approveProviderRequestByOrigin('example.com') + assert.deepEqual(controller._getMergedState(), { + providerRequests: [], + approvedOrigins: { + 'example.com': { + siteTitle: 'Example', + siteImage: 'https://example.com/logo.svg', + }, + }, + }) + }) + + it('should mark the origin as approved and multiple requests for the same domain', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller.approveProviderRequestByOrigin('example.com') + assert.deepEqual(controller._getMergedState(), { + providerRequests: [], + approvedOrigins: { + 'example.com': { + siteTitle: 'Example', + siteImage: 'https://example.com/logo.svg', + }, + }, + }) + }) + + it('should mark the origin as approved without a provider request', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller.approveProviderRequestByOrigin('example.com') + assert.deepEqual(controller._getMergedState(), { + providerRequests: [], + approvedOrigins: { + 'example.com': { + siteTitle: null, + siteImage: null, + }, + }, + }) + }) + }) + + describe('#rejectProviderRequestByOrigin', () => { + it('should remove the origin from approved', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller.approveProviderRequestByOrigin('example.com') + controller.rejectProviderRequestByOrigin('example.com') + assert.deepEqual(controller._getMergedState(), { + providerRequests: [], + approvedOrigins: {}, + }) + }) + + it('should reject the origin even without a pending request', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller.rejectProviderRequestByOrigin('example.com') + assert.deepEqual(controller._getMergedState(), { + providerRequests: [], + approvedOrigins: {}, + }) + }) + }) + + describe('#clearApprovedOrigins', () => { + it('should clear the approved origins', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller.approveProviderRequestByOrigin('example.com') + controller.clearApprovedOrigins() + assert.deepEqual(controller._getMergedState(), { + providerRequests: [], + approvedOrigins: {}, + }) + }) + }) + + describe('#shouldExposeAccounts', () => { + it('should return true for an approved origin', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller.approveProviderRequestByOrigin('example.com') + assert.ok(controller.shouldExposeAccounts('example.com')) + }) + + it('should return false for an origin not yet approved', () => { + const controller = new ProviderApprovalController({ + keyringController: mockUnlockedKeyringController, + }) + + controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg') + controller.approveProviderRequestByOrigin('example.com') + assert.ok(!controller.shouldExposeAccounts('bad.website')) + }) + }) +}) diff --git a/test/unit/migrations/036-test.js b/test/unit/migrations/036-test.js new file mode 100644 index 000000000..5c13c1f0c --- /dev/null +++ b/test/unit/migrations/036-test.js @@ -0,0 +1,119 @@ +const assert = require('assert') +const migration36 = require('../../../app/scripts/migrations/036') + +describe('migration #36', () => { + it('should update the version metadata', (done) => { + const oldStorage = { + 'meta': { + 'version': 35, + }, + 'data': {}, + } + + migration36.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.meta, { + 'version': 36, + }) + done() + }) + .catch(done) + }) + + it('should remove privacyMode if featureFlags.privacyMode was false', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'featureFlags': { + 'privacyMode': false, + }, + }, + }, + } + + migration36.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.PreferencesController, { + 'featureFlags': { + }, + }) + done() + }) + .catch(done) + }) + + it('should remove privacyMode if featureFlags.privacyMode was true', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'featureFlags': { + 'privacyMode': true, + }, + }, + }, + } + + migration36.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.PreferencesController, { + 'featureFlags': { + }, + }) + done() + }) + .catch(done) + }) + + it('should NOT change any state if privacyMode does not exist', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'migratedPrivacyMode': true, + 'featureFlags': { + }, + }, + }, + } + + migration36.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if PreferencesController is missing', (done) => { + const oldStorage = { + 'meta': {}, + 'data': {}, + } + + migration36.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if featureFlags is missing', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + }, + }, + } + + migration36.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) +}) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index 1ff797fa1..d26daf786 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -24,6 +24,7 @@ export default class ConfirmPageContainer extends Component { fromName: PropTypes.string, toAddress: PropTypes.string, toName: PropTypes.string, + toNickname: PropTypes.string, // Content contentComponent: PropTypes.node, errorKey: PropTypes.string, @@ -68,6 +69,7 @@ export default class ConfirmPageContainer extends Component { fromName, fromAddress, toName, + toNickname, toAddress, disabled, errorKey, @@ -126,6 +128,7 @@ export default class ConfirmPageContainer extends Component { senderAddress={fromAddress} recipientName={toName} recipientAddress={toAddress} + recipientNickname={toNickname} assetImage={renderAssetImage ? assetImage : undefined} /> diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request.js index 9c0f53f57..16073d5d3 100644 --- a/ui/app/components/app/signature-request.js +++ b/ui/app/components/app/signature-request.js @@ -215,7 +215,7 @@ SignatureRequest.prototype.msgHexToText = function (hex) { } // eslint-disable-next-line react/display-name -SignatureRequest.prototype.renderTypedDataV3 = function (data) { +SignatureRequest.prototype.renderTypedData = function (data) { const { domain, message } = JSON.parse(data) return [ h('div.request-signature__typed-container', [ @@ -267,17 +267,18 @@ SignatureRequest.prototype.renderBody = function () { }), }, [notice]), - h('div.request-signature__rows', type === 'eth_signTypedData' && version === 'V3' ? - this.renderTypedDataV3(data) : - rows.map(({ name, value }) => { - if (typeof value === 'boolean') { - value = value.toString() - } - return h('div.request-signature__row', [ - h('div.request-signature__row-title', [`${name}:`]), - h('div.request-signature__row-value', value), - ]) - }), + h('div.request-signature__rows', + type === 'eth_signTypedData' && (version === 'V3' || version === 'V4') ? + this.renderTypedData(data) : + rows.map(({ name, value }) => { + if (typeof value === 'boolean') { + value = value.toString() + } + return h('div.request-signature__row', [ + h('div.request-signature__row-title', [`${name}:`]), + h('div.request-signature__row-value', value), + ]) + }), ), ]) } diff --git a/ui/app/components/app/transaction-breakdown/index.scss b/ui/app/components/app/transaction-breakdown/index.scss index c8144eac2..2d2a939cb 100644 --- a/ui/app/components/app/transaction-breakdown/index.scss +++ b/ui/app/components/app/transaction-breakdown/index.scss @@ -12,6 +12,9 @@ } &__value { + display: flex; + justify-content: flex-end; + text-align: end; white-space: nowrap; overflow: hidden; diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index 8bdb6a313..f4f8d97b2 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -36,6 +36,7 @@ export default class TransactionListItem extends PureComponent { rpcPrefs: PropTypes.object, data: PropTypes.string, getContractMethodData: PropTypes.func, + isDeposit: PropTypes.bool, } static defaultProps = { @@ -117,7 +118,7 @@ export default class TransactionListItem extends PureComponent { } renderPrimaryCurrency () { - const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props + const { token, primaryTransaction: { txParams: { data } = {} } = {}, value, isDeposit } = this.props return token ? ( @@ -132,7 +133,7 @@ export default class TransactionListItem extends PureComponent { className="transaction-list-item__amount transaction-list-item__amount--primary" value={value} type={PRIMARY} - prefix="-" + prefix={isDeposit ? '' : '-'} /> ) } diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js index 1675958aa..27b9e2608 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js @@ -21,8 +21,10 @@ const mapStateToProps = (state, ownProps) => { const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) const { transactionGroup: { primaryTransaction } = {} } = ownProps - const { txParams: { gas: gasLimit, gasPrice, data } = {} } = primaryTransaction - const selectedAccountBalance = accounts[getSelectedAddress(state)].balance + const { txParams: { gas: gasLimit, gasPrice, data, to } = {} } = primaryTransaction + const selectedAddress = getSelectedAddress(state) + const selectedAccountBalance = accounts[selectedAddress].balance + const isDeposit = selectedAddress === to const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) const { rpcPrefs } = selectRpcInfo || {} @@ -42,6 +44,7 @@ const mapStateToProps = (state, ownProps) => { selectedAccountBalance, hasEnoughCancelGas, rpcPrefs, + isDeposit, } } @@ -68,12 +71,13 @@ const mapDispatchToProps = dispatch => { const mergeProps = (stateProps, dispatchProps, ownProps) => { const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps + const { isDeposit } = stateProps const { retryTransaction, ...restDispatchProps } = dispatchProps - const { txParams: { nonce, data } = {}, time } = initialTransaction + const { txParams: { nonce, data } = {}, time = 0 } = initialTransaction const { txParams: { value } = {} } = primaryTransaction const tokenData = data && getTokenData(data) - const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) + const nonceAndDate = nonce && !isDeposit ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) return { ...stateProps, diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index a98a94101..933f1b007 100644 --- a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -19,6 +19,7 @@ export default class SenderToRecipient extends PureComponent { senderAddress: PropTypes.string, recipientName: PropTypes.string, recipientAddress: PropTypes.string, + recipientNickname: PropTypes.string, t: PropTypes.func, variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), addressOnly: PropTypes.bool, @@ -88,7 +89,7 @@ export default class SenderToRecipient extends PureComponent { renderRecipientWithAddress () { const { t } = this.context - const { recipientName, recipientAddress, addressOnly, onRecipientClick } = this.props + const { recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props const checksummedRecipientAddress = checksumAddress(recipientAddress) return ( @@ -114,7 +115,7 @@ export default class SenderToRecipient extends PureComponent { { addressOnly ? `${t('to')}: ${checksummedRecipientAddress}` - : (recipientName || this.context.t('newContract')) + : (recipientNickname || recipientName || this.context.t('newContract')) } diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index cd26b3628..8c2a8b8bc 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -3,6 +3,7 @@ const UNLOCK_ROUTE = '/unlock' const LOCK_ROUTE = '/lock' const SETTINGS_ROUTE = '/settings' const GENERAL_ROUTE = '/settings/general' +const CONNECTIONS_ROUTE = '/settings/connections' const ADVANCED_ROUTE = '/settings/advanced' const SECURITY_ROUTE = '/settings/security' const ABOUT_US_ROUTE = '/settings/about-us' @@ -82,6 +83,7 @@ module.exports = { ADVANCED_ROUTE, SECURITY_ROUTE, GENERAL_ROUTE, + CONNECTIONS_ROUTE, ABOUT_US_ROUTE, CONTACT_LIST_ROUTE, CONTACT_EDIT_ROUTE, @@ -93,4 +95,3 @@ module.exports = { NETWORKS_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, } - diff --git a/ui/app/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js index d0a819b9b..e91e56ddc 100644 --- a/ui/app/helpers/constants/transactions.js +++ b/ui/app/helpers/constants/transactions.js @@ -20,5 +20,6 @@ export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' export const SIGNATURE_REQUEST_KEY = 'signatureRequest' export const CONTRACT_INTERACTION_KEY = 'contractInteraction' export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt' +export const DEPOSIT_TRANSACTION_KEY = 'deposit' export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift' diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index b65bda5b2..cb347ffaa 100644 --- a/ui/app/helpers/utils/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -21,6 +21,7 @@ import { SIGNATURE_REQUEST_KEY, CONTRACT_INTERACTION_KEY, CANCEL_ATTEMPT_ACTION_KEY, + DEPOSIT_TRANSACTION_KEY, } from '../constants/transactions' import log from 'loglevel' @@ -124,6 +125,10 @@ export function isTokenMethodAction (transactionCategory) { export function getTransactionActionKey (transaction) { const { msgParams, type, transactionCategory } = transaction + if (transactionCategory === 'incoming') { + return DEPOSIT_TRANSACTION_KEY + } + if (type === 'cancel') { return CANCEL_ATTEMPT_ACTION_KEY } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 5c46c8449..623079e68 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -59,6 +59,7 @@ export default class ConfirmTransactionBase extends Component { tokenData: PropTypes.object, tokenProps: PropTypes.object, toName: PropTypes.string, + toNickname: PropTypes.string, transactionStatus: PropTypes.string, txData: PropTypes.object, unapprovedTxCount: PropTypes.number, @@ -529,6 +530,7 @@ export default class ConfirmTransactionBase extends Component { fromAddress, toName, toAddress, + toNickname, methodData, valid: propsValid = true, errorMessage, @@ -551,13 +553,13 @@ export default class ConfirmTransactionBase extends Component { const { name } = methodData const { valid, errorKey } = this.getErrorKey() const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData() - return ( { const { conversionRate, identities, + addressBook, currentCurrency, selectedAddress, selectedAddressTxList, @@ -75,6 +76,8 @@ const mapStateToProps = (state, ownProps) => { : addressSlicer(checksumAddress(toAddress)) ) + const addressBookObject = addressBook[checksumAddress(toAddress)] + const toNickname = addressBookObject ? addressBookObject.name : '' const isTxReprice = Boolean(lastGasPrice) const transactionStatus = transaction ? transaction.status : '' @@ -115,6 +118,7 @@ const mapStateToProps = (state, ownProps) => { fromName, toAddress, toName, + toNickname, ethTransactionAmount, ethTransactionFee, ethTransactionTotal, diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js index 31658d87a..8cbf4d69f 100644 --- a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js +++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js @@ -49,7 +49,7 @@ export default class EndOfFlowScreen extends PureComponent { { '• ' + t('endOfFlowMessage7') }
- *MetaMask cannot recover your seedphrase. , }, - { - shouldBeRendered: viewingUnconnectedDapp, - component: { - forceApproveProviderRequestByOrigin(activeTab.origin) - }} - ignoreText={t('dismiss')} - onIgnore={() => rejectProviderRequestByOrigin(activeTab.origin)} - infoText={t('shareAddressInfo', [activeTab.origin])} - key="home-shareAddressToConnect" - />, - }, { shouldBeRendered: shouldShowSeedPhraseReminder, component: { - const { activeTab, metamask, appState } = state + const { metamask, appState } = state const { - approvedOrigins, - dismissedOrigins, - lostAccounts, suggestedTokens, providerRequests, migratedPrivacyMode, - featureFlags: { - privacyMode, - } = {}, seedPhraseBackedUp, tokens, } = metamask const accountBalance = getCurrentEthBalance(state) const { forgottenPassword } = appState - const isUnconnected = Boolean( - activeTab && - activeTabDappProtocols.includes(activeTab.protocol) && - privacyMode && - !approvedOrigins[activeTab.origin] && - !dismissedOrigins[activeTab.origin] - ) const isPopup = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP return { - lostAccounts, forgottenPassword, suggestedTokens, unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), providerRequests, showPrivacyModeNotification: migratedPrivacyMode, - activeTab, - viewingUnconnectedDapp: isUnconnected && isPopup, shouldShowSeedPhraseReminder: !seedPhraseBackedUp && (parseInt(accountBalance, 16) > 0 || tokens.length > 0), isPopup, } @@ -57,8 +37,6 @@ const mapStateToProps = state => { const mapDispatchToProps = (dispatch) => ({ unsetMigratedPrivacyMode: () => dispatch(unsetMigratedPrivacyMode()), - forceApproveProviderRequestByOrigin: (origin) => dispatch(forceApproveProviderRequestByOrigin(origin)), - rejectProviderRequestByOrigin: origin => dispatch(rejectProviderRequestByOrigin(origin)), }) export default compose( diff --git a/ui/app/pages/send/send-content/add-recipient/ens-input.component.js b/ui/app/pages/send/send-content/add-recipient/ens-input.component.js index 498d72605..483d5d344 100644 --- a/ui/app/pages/send/send-content/add-recipient/ens-input.component.js +++ b/ui/app/pages/send/send-content/add-recipient/ens-input.component.js @@ -30,10 +30,10 @@ export default class EnsInput extends Component { updateEnsResolution: PropTypes.func, scanQrCode: PropTypes.func, updateEnsResolutionError: PropTypes.func, - addressBook: PropTypes.array, onPaste: PropTypes.func, onReset: PropTypes.func, onValidAddressTyped: PropTypes.func, + contact: PropTypes.object, } state = { @@ -181,8 +181,7 @@ export default class EnsInput extends Component { renderSelected () { const { t } = this.context - const { className, selectedAddress, selectedName, addressBook } = this.props - const contact = addressBook.filter(item => item.address === selectedAddress)[0] || {} + const { className, selectedAddress, selectedName, contact = {} } = this.props const name = contact.name || selectedName diff --git a/ui/app/pages/send/send-content/add-recipient/ens-input.container.js b/ui/app/pages/send/send-content/add-recipient/ens-input.container.js index d74f44832..c37980c49 100644 --- a/ui/app/pages/send/send-content/add-recipient/ens-input.container.js +++ b/ui/app/pages/send/send-content/add-recipient/ens-input.container.js @@ -5,16 +5,19 @@ import { getSendToNickname, } from '../../send.selectors' import { - getAddressBook, + getAddressBookEntry, } from '../../../../selectors/selectors' const connect = require('react-redux').connect export default connect( - state => ({ - network: getCurrentNetwork(state), - selectedAddress: getSendTo(state), - selectedName: getSendToNickname(state), - addressBook: getAddressBook(state), - }) + state => { + const selectedAddress = getSendTo(state) + return { + network: getCurrentNetwork(state), + selectedAddress, + selectedName: getSendToNickname(state), + contact: getAddressBookEntry(state, selectedAddress), + } + } )(EnsInput) diff --git a/ui/app/pages/send/send-content/send-content.component.js b/ui/app/pages/send/send-content/send-content.component.js index aff675e7a..55e0e30e2 100644 --- a/ui/app/pages/send/send-content/send-content.component.js +++ b/ui/app/pages/send/send-content/send-content.component.js @@ -18,9 +18,10 @@ export default class SendContent extends Component { scanQrCode: PropTypes.func, showAddToAddressBookModal: PropTypes.func, showHexData: PropTypes.bool, - to: PropTypes.string, ownedAccounts: PropTypes.array, addressBook: PropTypes.array, + contact: PropTypes.object, + isOwnedAccount: PropTypes.bool, } updateGas = (updateData) => this.props.updateGas(updateData) @@ -47,9 +48,7 @@ export default class SendContent extends Component { maybeRenderAddContact () { const { t } = this.context - const { to, addressBook = [], ownedAccounts = [], showAddToAddressBookModal } = this.props - const isOwnedAccount = !!ownedAccounts.find(({ address }) => address.toLowerCase() === to.toLowerCase()) - const contact = addressBook.find(({ address }) => address === to) || {} + const { isOwnedAccount, showAddToAddressBookModal, contact = {} } = this.props if (isOwnedAccount || contact.name) { return diff --git a/ui/app/pages/send/send-content/send-content.container.js b/ui/app/pages/send/send-content/send-content.container.js index a0732fc20..a122aca1a 100644 --- a/ui/app/pages/send/send-content/send-content.container.js +++ b/ui/app/pages/send/send-content/send-content.container.js @@ -5,15 +5,17 @@ import { getSendTo, } from '../send.selectors' import { - getAddressBook, + getAddressBookEntry, } from '../../../selectors/selectors' import actions from '../../../store/actions' function mapStateToProps (state) { + const ownedAccounts = accountsWithSendEtherInfoSelector(state) + const to = getSendTo(state) return { - to: getSendTo(state), - addressBook: getAddressBook(state), - ownedAccounts: accountsWithSendEtherInfoSelector(state), + isOwnedAccount: !!ownedAccounts.find(({ address }) => address.toLowerCase() === to.toLowerCase()), + contact: getAddressBookEntry(state, to), + to, } } diff --git a/ui/app/pages/send/send-content/tests/send-content-component.test.js b/ui/app/pages/send/send-content/tests/send-content-component.test.js index 451d2ea53..479db9c18 100644 --- a/ui/app/pages/send/send-content/tests/send-content-component.test.js +++ b/ui/app/pages/send/send-content/tests/send-content-component.test.js @@ -52,11 +52,10 @@ describe('SendContent Component', function () { assert.equal(PageContainerContentChild.childAt(4).exists(), false) }) - it('should not render the Dialog if addressBook contains "to" address', () => { + it('should not render the Dialog if contact has a name', () => { wrapper.setProps({ showHexData: false, - to: '0x80F061544cC398520615B5d3e7A3BedD70cd4510', - addressBook: [{ address: '0x80F061544cC398520615B5d3e7A3BedD70cd4510', name: 'dinodan' }], + contact: { name: 'testName' }, }) const PageContainerContentChild = wrapper.find(PageContainerContent).children() assert(PageContainerContentChild.childAt(0).is(SendAssetRow), 'row[1] should be SendAssetRow') @@ -65,12 +64,10 @@ describe('SendContent Component', function () { assert.equal(PageContainerContentChild.childAt(3).exists(), false) }) - it('should not render the Dialog if ownedAccounts contains "to" address', () => { + it('should not render the Dialog if it is an ownedAccount', () => { wrapper.setProps({ showHexData: false, - to: '0x80F061544cC398520615B5d3e7A3BedD70cd4510', - addressBook: [], - ownedAccounts: [{ address: '0x80F061544cC398520615B5d3e7A3BedD70cd4510', name: 'dinodan' }], + isOwnedAccount: true, }) const PageContainerContentChild = wrapper.find(PageContainerContent).children() assert(PageContainerContentChild.childAt(0).is(SendAssetRow), 'row[1] should be SendAssetRow') diff --git a/ui/app/pages/send/send-footer/tests/send-footer-container.test.js b/ui/app/pages/send/send-footer/tests/send-footer-container.test.js index 118ebf356..11b4adb3e 100644 --- a/ui/app/pages/send/send-footer/tests/send-footer-container.test.js +++ b/ui/app/pages/send/send-footer/tests/send-footer-container.test.js @@ -38,6 +38,7 @@ proxyquire('../send-footer.container.js', { getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, getSendFromObject: (s) => `mockFromObject:${s}`, getSendTo: (s) => `mockTo:${s}`, + getSendToNickname: (s) => `mockToNickname:${s}`, getSendToAccounts: (s) => `mockToAccounts:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`, getSendHexData: (s) => `mockHexData:${s}`, diff --git a/ui/app/pages/send/tests/send-selectors-test-data.js b/ui/app/pages/send/tests/send-selectors-test-data.js index 54a494b63..ff9c19b5b 100644 --- a/ui/app/pages/send/tests/send-selectors-test-data.js +++ b/ui/app/pages/send/tests/send-selectors-test-data.js @@ -157,7 +157,6 @@ module.exports = { { id: 'shapeShiftTx2', 'time': 1575000000000 }, { id: 'shapeShiftTx3', 'time': 1475000000000 }, ], - 'lostAccounts': [], 'send': { 'gasLimit': '0xFFFF', 'gasPrice': '0xaa', diff --git a/ui/app/pages/settings/connections-tab/connected-site-row/connected-site-row.component.js b/ui/app/pages/settings/connections-tab/connected-site-row/connected-site-row.component.js new file mode 100644 index 000000000..37c068ab5 --- /dev/null +++ b/ui/app/pages/settings/connections-tab/connected-site-row/connected-site-row.component.js @@ -0,0 +1,31 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +export default class ConnectedSiteRow extends PureComponent { + static defaultProps = { + siteTitle: null, + siteImage: null, + onDelete: () => {}, + } + + static propTypes = { + siteTitle: PropTypes.string, + siteImage: PropTypes.string, + origin: PropTypes.string.isRequired, + onDelete: PropTypes.func, + } + + render () { + const { + origin, + onDelete, + } = this.props + + return ( +
+
{origin}
+
+
+ ) + } +} diff --git a/ui/app/pages/settings/connections-tab/connected-site-row/index.js b/ui/app/pages/settings/connections-tab/connected-site-row/index.js new file mode 100644 index 000000000..57023b6da --- /dev/null +++ b/ui/app/pages/settings/connections-tab/connected-site-row/index.js @@ -0,0 +1 @@ +export { default } from './connected-site-row.component' diff --git a/ui/app/pages/settings/connections-tab/connected-site-row/index.scss b/ui/app/pages/settings/connections-tab/connected-site-row/index.scss new file mode 100644 index 000000000..f0957a8e9 --- /dev/null +++ b/ui/app/pages/settings/connections-tab/connected-site-row/index.scss @@ -0,0 +1,14 @@ +.connected-site-row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + &__origin { + font-family: monospace; + } + + &__delete { + padding: 8px; + } +} diff --git a/ui/app/pages/settings/connections-tab/connections-tab.component.js b/ui/app/pages/settings/connections-tab/connections-tab.component.js new file mode 100644 index 000000000..3309fcc0d --- /dev/null +++ b/ui/app/pages/settings/connections-tab/connections-tab.component.js @@ -0,0 +1,133 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import ConnectedSiteEntry from './connected-site-row' +import TextField from '../../../components/ui/text-field' +import Button from '../../../components/ui/button' + +export default class ConnectionsTab extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static defaultProps = { + activeTab: {}, + } + + static propTypes = { + activeTab: PropTypes.object, + approvedOrigins: PropTypes.object.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, + showClearApprovalModal: PropTypes.func.isRequired, + } + + state = { + input: this.props.activeTab.origin || '', + } + + handleAddOrigin = () => { + const newOrigin = this.state.input + this.setState({ + input: '', + }, () => { + if (newOrigin && newOrigin.trim()) { + this.props.approveProviderRequestByOrigin(newOrigin) + } + }) + } + + renderNewOriginInput () { + const { t } = this.context + + return ( +
+
+ { t('addSite') } +
+ { t('addSiteDescription') } +
+
+
+
+ this.setState({ input: e.target.value })} + fullWidth + margin="dense" + min={0} + /> + +
+
+
+ ) + } + + renderApprovedOriginsList () { + const { t } = this.context + const { approvedOrigins, rejectProviderRequestByOrigin, showClearApprovalModal } = this.props + const approvedEntries = Object.entries(approvedOrigins) + const approvalListEmpty = approvedEntries.length === 0 + + return ( +
+
+ { t('connected') } + + { t('connectedDescription') } + +
+
+ { + approvalListEmpty + ?
+ : null + } + { + approvedEntries.map(([origin, { siteTitle, siteImage }]) => ( + { + rejectProviderRequestByOrigin(origin) + }} + /> + )) + } +
+
+ +
+
+ ) + } + + render () { + return ( +
+ { this.renderNewOriginInput() } + { this.renderApprovedOriginsList() } +
+ ) + } +} diff --git a/ui/app/pages/settings/connections-tab/connections-tab.container.js b/ui/app/pages/settings/connections-tab/connections-tab.container.js new file mode 100644 index 000000000..cf3efc2b4 --- /dev/null +++ b/ui/app/pages/settings/connections-tab/connections-tab.container.js @@ -0,0 +1,39 @@ +import ConnectionsTab from './connections-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { + approveProviderRequestByOrigin, + rejectProviderRequestByOrigin, + showModal, +} from '../../../store/actions' + +export const mapStateToProps = state => { + const { + activeTab, + metamask, + } = state + const { + approvedOrigins, + } = metamask + + return { + activeTab, + approvedOrigins, + } +} + +export const mapDispatchToProps = dispatch => { + return { + approveProviderRequestByOrigin: (origin) => dispatch(approveProviderRequestByOrigin(origin)), + rejectProviderRequestByOrigin: (origin) => dispatch(rejectProviderRequestByOrigin(origin)), + showClearApprovalModal: () => dispatch(showModal({ + name: 'CLEAR_APPROVED_ORIGINS', + })), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConnectionsTab) diff --git a/ui/app/pages/settings/connections-tab/index.js b/ui/app/pages/settings/connections-tab/index.js new file mode 100644 index 000000000..b04f4e33a --- /dev/null +++ b/ui/app/pages/settings/connections-tab/index.js @@ -0,0 +1 @@ +export { default } from './connections-tab.container' diff --git a/ui/app/pages/settings/connections-tab/index.scss b/ui/app/pages/settings/connections-tab/index.scss new file mode 100644 index 000000000..249a7193f --- /dev/null +++ b/ui/app/pages/settings/connections-tab/index.scss @@ -0,0 +1 @@ +@import './connected-site-row/index'; diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss index 73f36806d..4cbe5632e 100644 --- a/ui/app/pages/settings/index.scss +++ b/ui/app/pages/settings/index.scss @@ -4,6 +4,8 @@ @import 'settings-tab/index'; +@import 'connections-tab/index'; + @import 'contact-list-tab/index'; .settings-page { diff --git a/ui/app/pages/settings/security-tab/security-tab.component.js b/ui/app/pages/settings/security-tab/security-tab.component.js index 0d367abfb..117010d0f 100644 --- a/ui/app/pages/settings/security-tab/security-tab.component.js +++ b/ui/app/pages/settings/security-tab/security-tab.component.js @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { exportAsFile } from '../../../helpers/utils/util' import ToggleButton from '../../../components/ui/toggle-button' import { REVEAL_SEED_ROUTE } from '../../../helpers/constants/routes' import Button from '../../../components/ui/button' @@ -12,81 +11,15 @@ export default class SecurityTab extends PureComponent { } static propTypes = { - setPrivacyMode: PropTypes.func, - privacyMode: PropTypes.bool, displayWarning: PropTypes.func, revealSeedConfirmation: PropTypes.func, - showClearApprovalModal: PropTypes.func, warning: PropTypes.string, history: PropTypes.object, mobileSync: PropTypes.bool, participateInMetaMetrics: PropTypes.bool, setParticipateInMetaMetrics: PropTypes.func, - } - - renderStateLogs () { - const { t } = this.context - const { displayWarning } = this.props - - return ( -
-
- { t('stateLogs') } - - { t('stateLogsDescription') } - -
-
-
- -
-
-
- ) - } - - renderClearApproval () { - const { t } = this.context - const { showClearApprovalModal } = this.props - return ( -
-
- { t('approvalData') } - - { t('approvalDataDescription') } - -
-
-
- -
-
-
- ) + showIncomingTransactions: PropTypes.bool, + setShowIncomingTransactionsFeatureFlag: PropTypes.func, } renderSeedWords () { @@ -123,23 +56,23 @@ export default class SecurityTab extends PureComponent { ) } - renderPrivacyOptIn () { + renderMetaMetricsOptIn () { const { t } = this.context - const { privacyMode, setPrivacyMode } = this.props + const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props return (
- { t('privacyMode') } + { t('participateInMetaMetrics') }
- { t('privacyModeDescription') } + { t('participateInMetaMetricsDescription') }
setPrivacyMode(!value)} + value={participateInMetaMetrics} + onToggle={value => setParticipateInMetaMetrics(!value)} offLabel={t('off')} onLabel={t('on')} /> @@ -149,23 +82,23 @@ export default class SecurityTab extends PureComponent { ) } - renderMetaMetricsOptIn () { + renderIncomingTransactionsOptIn () { const { t } = this.context - const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props + const { showIncomingTransactions, setShowIncomingTransactionsFeatureFlag } = this.props return (
- { t('participateInMetaMetrics') } + { t('showIncomingTransactions') }
- { t('participateInMetaMetricsDescription') } + { t('showIncomingTransactionsDescription') }
setParticipateInMetaMetrics(!value)} + value={showIncomingTransactions} + onToggle={value => setShowIncomingTransactionsFeatureFlag(!value)} offLabel={t('off')} onLabel={t('on')} /> @@ -181,9 +114,8 @@ export default class SecurityTab extends PureComponent { return (
{ warning &&
{ warning }
} - { this.renderPrivacyOptIn() } - { this.renderClearApproval() } { this.renderSeedWords() } + { this.renderIncomingTransactionsOptIn() } { this.renderMetaMetricsOptIn() }
) diff --git a/ui/app/pages/settings/security-tab/security-tab.container.js b/ui/app/pages/settings/security-tab/security-tab.container.js index 6036f4eda..35375ebf5 100644 --- a/ui/app/pages/settings/security-tab/security-tab.container.js +++ b/ui/app/pages/settings/security-tab/security-tab.container.js @@ -6,7 +6,6 @@ import { displayWarning, revealSeedConfirmation, setFeatureFlag, - showModal, setParticipateInMetaMetrics, } from '../../../store/actions' @@ -14,14 +13,14 @@ const mapStateToProps = state => { const { appState: { warning }, metamask } = state const { featureFlags: { - privacyMode, + showIncomingTransactions, } = {}, participateInMetaMetrics, } = metamask return { warning, - privacyMode, + showIncomingTransactions, participateInMetaMetrics, } } @@ -30,9 +29,8 @@ const mapDispatchToProps = dispatch => { return { displayWarning: warning => dispatch(displayWarning(warning)), revealSeedConfirmation: () => dispatch(revealSeedConfirmation()), - setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)), - showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })), setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)), + setShowIncomingTransactionsFeatureFlag: shouldShow => dispatch(setFeatureFlag('showIncomingTransactions', shouldShow)), } } diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js index 79f383dc4..781b38eff 100644 --- a/ui/app/pages/settings/settings.component.js +++ b/ui/app/pages/settings/settings.component.js @@ -4,6 +4,7 @@ import { Switch, Route, matchPath, withRouter } from 'react-router-dom' import TabBar from '../../components/app/tab-bar' import c from 'classnames' import SettingsTab from './settings-tab' +import ConnectionsTab from './connections-tab' import NetworksTab from './networks-tab' import AdvancedTab from './advanced-tab' import InfoTab from './info-tab' @@ -14,6 +15,7 @@ import { ADVANCED_ROUTE, SECURITY_ROUTE, GENERAL_ROUTE, + CONNECTIONS_ROUTE, ABOUT_US_ROUTE, SETTINGS_ROUTE, NETWORKS_ROUTE, @@ -148,8 +150,9 @@ class SettingsPage extends PureComponent { + contact.address.toLowerCase() === address.toLowerCase()) + const entry = addressBook.find(contact => contact.address === checksumAddress(address)) return entry } diff --git a/ui/app/selectors/tests/selectors-test-data.js b/ui/app/selectors/tests/selectors-test-data.js index 54a494b63..ff9c19b5b 100644 --- a/ui/app/selectors/tests/selectors-test-data.js +++ b/ui/app/selectors/tests/selectors-test-data.js @@ -157,7 +157,6 @@ module.exports = { { id: 'shapeShiftTx2', 'time': 1575000000000 }, { id: 'shapeShiftTx3', 'time': 1475000000000 }, ], - 'lostAccounts': [], 'send': { 'gasLimit': '0xFFFF', 'gasPrice': '0xaa', diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js index b1d27b333..2a6a92ddf 100644 --- a/ui/app/selectors/transactions.js +++ b/ui/app/selectors/transactions.js @@ -10,11 +10,24 @@ import { TRANSACTION_TYPE_RETRY, } from '../../../app/scripts/controllers/transactions/enums' import { hexToDecimal } from '../helpers/utils/conversions.util' - import { selectedTokenAddressSelector } from './tokens' import txHelper from '../../lib/tx-helper' export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList + +export const incomingTxListSelector = state => { + const { showIncomingTransactions } = state.metamask.featureFlags + if (!showIncomingTransactions) { + return [] + } + + const network = state.metamask.network + const selectedAddress = state.metamask.selectedAddress + return Object.values(state.metamask.incomingTransactions) + .filter(({ metamaskNetworkId, txParams }) => ( + txParams.to === selectedAddress && metamaskNetworkId === network + )) +} export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList export const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs @@ -55,9 +68,10 @@ export const transactionsSelector = createSelector( selectedTokenAddressSelector, unapprovedMessagesSelector, shapeShiftTxListSelector, + incomingTxListSelector, selectedAddressTxListSelector, - (selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], transactions = []) => { - const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList) + (selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], incomingTxList = [], transactions = []) => { + const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList, incomingTxList) return selectedTokenAddress ? txsToRender @@ -158,17 +172,18 @@ const insertTransactionGroupByTime = (transactionGroups, transactionGroup) => { } /** - * @name mergeShapeshiftTransactionGroups + * @name mergeNonNonceTransactionGroups * @private - * @description Inserts (mutates) shapeshift transactionGroups into an array of nonce-ordered - * transactionGroups by time. Shapeshift transactionGroups need to be sorted by time within the list - * of transactions as they do not have nonces. + * @description Inserts (mutates) transactionGroups that are not to be ordered by nonce into an array + * of nonce-ordered transactionGroups by time. Shapeshift transactionGroups need to be sorted by time + * within the list of transactions as they do not have nonces. * @param {transactionGroup[]} orderedTransactionGroups - Array of transactionGroups ordered by * nonce. - * @param {transactionGroup[]} shapeshiftTransactionGroups - Array of shapeshift transactionGroups + * @param {transactionGroup[]} nonNonceTransactionGroups - Array of transactionGroups not intended to be ordered by nonce, + * but intended to be ordered by timestamp */ -const mergeShapeshiftTransactionGroups = (orderedTransactionGroups, shapeshiftTransactionGroups) => { - shapeshiftTransactionGroups.forEach(shapeshiftGroup => { +const mergeNonNonceTransactionGroups = (orderedTransactionGroups, nonNonceTransactionGroups) => { + nonNonceTransactionGroups.forEach(shapeshiftGroup => { insertTransactionGroupByTime(orderedTransactionGroups, shapeshiftGroup) }) } @@ -183,13 +198,14 @@ export const nonceSortedTransactionsSelector = createSelector( (transactions = []) => { const unapprovedTransactionGroups = [] const shapeshiftTransactionGroups = [] + const incomingTransactionGroups = [] const orderedNonces = [] const nonceToTransactionsMap = {} transactions.forEach(transaction => { - const { txParams: { nonce } = {}, status, type, time: txTime, key } = transaction + const { txParams: { nonce } = {}, status, type, time: txTime, key, transactionCategory } = transaction - if (typeof nonce === 'undefined') { + if (typeof nonce === 'undefined' || transactionCategory === 'incoming') { const transactionGroup = { transactions: [transaction], initialTransaction: transaction, @@ -200,6 +216,8 @@ export const nonceSortedTransactionsSelector = createSelector( if (key === 'shapeshift') { shapeshiftTransactionGroups.push(transactionGroup) + } else if (transactionCategory === 'incoming') { + incomingTransactionGroups.push(transactionGroup) } else { insertTransactionGroupByTime(unapprovedTransactionGroups, transactionGroup) } @@ -245,7 +263,8 @@ export const nonceSortedTransactionsSelector = createSelector( }) const orderedTransactionGroups = orderedNonces.map(nonce => nonceToTransactionsMap[nonce]) - mergeShapeshiftTransactionGroups(orderedTransactionGroups, shapeshiftTransactionGroups) + mergeNonNonceTransactionGroups(orderedTransactionGroups, shapeshiftTransactionGroups) + mergeNonNonceTransactionGroups(orderedTransactionGroups, incomingTransactionGroups) return unapprovedTransactionGroups.concat(orderedTransactionGroups) } ) diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index adb5fe450..cbf8e6283 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -349,7 +349,6 @@ var actions = { approveProviderRequestByOrigin, rejectProviderRequestByOrigin, - forceApproveProviderRequestByOrigin, clearApprovedOrigins, setFirstTimeFlowType, @@ -1956,10 +1955,17 @@ function addToAddressBook (recipient, nickname = '', memo = '') { return (dispatch, getState) => { const chainId = getState().metamask.network - const set = background.setAddressBook(checksumAddress(recipient), nickname, chainId, memo) - if (!set) { - return dispatch(displayWarning('Address book failed to update')) - } + background.setAddressBook(checksumAddress(recipient), nickname, chainId, memo, (err, set) => { + if (err) { + log.error(err) + dispatch(displayWarning('Address book failed to update')) + throw err + } + if (!set) { + return dispatch(displayWarning('Address book failed to update')) + } + }) + } } @@ -2643,12 +2649,6 @@ function approveProviderRequestByOrigin (origin) { } } -function forceApproveProviderRequestByOrigin (origin) { - return () => { - background.forceApproveProviderRequestByOrigin(origin) - } -} - function rejectProviderRequestByOrigin (origin) { return () => { background.rejectProviderRequestByOrigin(origin) diff --git a/ui/design/00-metamask-SignIn.jpg b/ui/design/00-metamask-SignIn.jpg index d6f0d346e..2becdb032 100644 Binary files a/ui/design/00-metamask-SignIn.jpg and b/ui/design/00-metamask-SignIn.jpg differ diff --git a/ui/design/01-metamask-SelectAcc.jpg b/ui/design/01-metamask-SelectAcc.jpg index 250e62f5e..239091a98 100644 Binary files a/ui/design/01-metamask-SelectAcc.jpg and b/ui/design/01-metamask-SelectAcc.jpg differ diff --git a/ui/design/02-metamask-AccDetails.jpg b/ui/design/02-metamask-AccDetails.jpg index 618498936..d7d408ffc 100644 Binary files a/ui/design/02-metamask-AccDetails.jpg and b/ui/design/02-metamask-AccDetails.jpg differ diff --git a/ui/design/02a-metamask-AccDetails-OverToken.jpg b/ui/design/02a-metamask-AccDetails-OverToken.jpg index 69ac74406..f26ff31e8 100644 Binary files a/ui/design/02a-metamask-AccDetails-OverToken.jpg and b/ui/design/02a-metamask-AccDetails-OverToken.jpg differ diff --git a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg index a2d32e57f..8a06be6b9 100644 Binary files a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg and b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg differ diff --git a/ui/design/02a-metamask-AccDetails.jpg b/ui/design/02a-metamask-AccDetails.jpg index 5e915f757..c37e0f539 100644 Binary files a/ui/design/02a-metamask-AccDetails.jpg and b/ui/design/02a-metamask-AccDetails.jpg differ diff --git a/ui/design/02b-metamask-AccDetails-Send.jpg b/ui/design/02b-metamask-AccDetails-Send.jpg index 47a49f161..10f2d27fd 100644 Binary files a/ui/design/02b-metamask-AccDetails-Send.jpg and b/ui/design/02b-metamask-AccDetails-Send.jpg differ diff --git a/ui/design/03-metamask-Qr.jpg b/ui/design/03-metamask-Qr.jpg index 0f13673b0..9c09de42f 100644 Binary files a/ui/design/03-metamask-Qr.jpg and b/ui/design/03-metamask-Qr.jpg differ diff --git a/ui/design/05-metamask-Menu.jpg b/ui/design/05-metamask-Menu.jpg index df66f3032..0a43d7b2a 100644 Binary files a/ui/design/05-metamask-Menu.jpg and b/ui/design/05-metamask-Menu.jpg differ diff --git a/ui/design/chromeStorePics/final_screen_dao_accounts.png b/ui/design/chromeStorePics/final_screen_dao_accounts.png index 9d79c3934..805cc96b6 100644 Binary files a/ui/design/chromeStorePics/final_screen_dao_accounts.png and b/ui/design/chromeStorePics/final_screen_dao_accounts.png differ diff --git a/ui/design/chromeStorePics/final_screen_dao_locked.png b/ui/design/chromeStorePics/final_screen_dao_locked.png index 011b0ef6e..9d9e33930 100644 Binary files a/ui/design/chromeStorePics/final_screen_dao_locked.png and b/ui/design/chromeStorePics/final_screen_dao_locked.png differ diff --git a/ui/design/chromeStorePics/final_screen_dao_notification.png b/ui/design/chromeStorePics/final_screen_dao_notification.png index cc73b3690..d56a5ce62 100644 Binary files a/ui/design/chromeStorePics/final_screen_dao_notification.png and b/ui/design/chromeStorePics/final_screen_dao_notification.png differ diff --git a/ui/design/chromeStorePics/final_screen_wei_account.png b/ui/design/chromeStorePics/final_screen_wei_account.png index 2f0cf000e..d503ff301 100644 Binary files a/ui/design/chromeStorePics/final_screen_wei_account.png and b/ui/design/chromeStorePics/final_screen_wei_account.png differ diff --git a/ui/design/chromeStorePics/final_screen_wei_notification.png b/ui/design/chromeStorePics/final_screen_wei_notification.png index c95ffed10..3560c51ff 100644 Binary files a/ui/design/chromeStorePics/final_screen_wei_notification.png and b/ui/design/chromeStorePics/final_screen_wei_notification.png differ diff --git a/ui/design/chromeStorePics/icon-128.png b/ui/design/chromeStorePics/icon-128.png index 754b4cc79..ae687147d 100644 Binary files a/ui/design/chromeStorePics/icon-128.png and b/ui/design/chromeStorePics/icon-128.png differ diff --git a/ui/design/chromeStorePics/icon-64.png b/ui/design/chromeStorePics/icon-64.png index f964b01d6..7062cf4f1 100644 Binary files a/ui/design/chromeStorePics/icon-64.png and b/ui/design/chromeStorePics/icon-64.png differ diff --git a/ui/design/chromeStorePics/promo1400560.png b/ui/design/chromeStorePics/promo1400560.png index 9ada2622e..d3637ecc8 100644 Binary files a/ui/design/chromeStorePics/promo1400560.png and b/ui/design/chromeStorePics/promo1400560.png differ diff --git a/ui/design/chromeStorePics/promo440280.png b/ui/design/chromeStorePics/promo440280.png index 599b53a01..c1f92b1c0 100644 Binary files a/ui/design/chromeStorePics/promo440280.png and b/ui/design/chromeStorePics/promo440280.png differ diff --git a/ui/design/chromeStorePics/promo920680.png b/ui/design/chromeStorePics/promo920680.png index 58c343ff0..726bd810a 100644 Binary files a/ui/design/chromeStorePics/promo920680.png and b/ui/design/chromeStorePics/promo920680.png differ diff --git a/ui/design/chromeStorePics/screen_dao_accounts.png b/ui/design/chromeStorePics/screen_dao_accounts.png index 2b3c75e3e..1a2e8052c 100644 Binary files a/ui/design/chromeStorePics/screen_dao_accounts.png and b/ui/design/chromeStorePics/screen_dao_accounts.png differ diff --git a/ui/design/chromeStorePics/screen_dao_locked.png b/ui/design/chromeStorePics/screen_dao_locked.png index d865abdd6..6592c17e4 100644 Binary files a/ui/design/chromeStorePics/screen_dao_locked.png and b/ui/design/chromeStorePics/screen_dao_locked.png differ diff --git a/ui/design/chromeStorePics/screen_dao_notification.png b/ui/design/chromeStorePics/screen_dao_notification.png index 4ae489540..baeb2ec39 100644 Binary files a/ui/design/chromeStorePics/screen_dao_notification.png and b/ui/design/chromeStorePics/screen_dao_notification.png differ diff --git a/ui/design/chromeStorePics/screen_wei_account.png b/ui/design/chromeStorePics/screen_wei_account.png index 80fea67c8..23301e4bf 100644 Binary files a/ui/design/chromeStorePics/screen_wei_account.png and b/ui/design/chromeStorePics/screen_wei_account.png differ diff --git a/ui/design/metamask-logo-eyes.png b/ui/design/metamask-logo-eyes.png index b439a0122..c29331b28 100644 Binary files a/ui/design/metamask-logo-eyes.png and b/ui/design/metamask-logo-eyes.png differ diff --git a/ui/design/wireframes/metamask_wfs_jan_13.png b/ui/design/wireframes/metamask_wfs_jan_13.png index e13be18b7..d71d7bdb4 100644 Binary files a/ui/design/wireframes/metamask_wfs_jan_13.png and b/ui/design/wireframes/metamask_wfs_jan_13.png differ diff --git a/ui/lib/lost-accounts-notice.js b/ui/lib/lost-accounts-notice.js deleted file mode 100644 index 840bd8dca..000000000 --- a/ui/lib/lost-accounts-notice.js +++ /dev/null @@ -1,23 +0,0 @@ -const summary = require('../app/helpers/utils/util').addressSummary - -module.exports = function (lostAccounts) { - return { - date: new Date().toDateString(), - title: 'Account Problem Caught', - body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! - -We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. - -We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. - -Your affected accounts are: -${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} - -These accounts have been marked as "Loose" so they will be easy to recognize in the account list. - -For more information, please read [our blog post.][1] - -[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 - `, - } -}