diff options
author | brunobar79 <brunobar79@gmail.com> | 2018-08-21 09:51:15 +0800 |
---|---|---|
committer | brunobar79 <brunobar79@gmail.com> | 2018-08-21 09:51:35 +0800 |
commit | ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32 (patch) | |
tree | d0e54e987f7ee42d21dd8a5aeb582c4a2c72e02c /app | |
parent | 6e813db47309b7773d430fecc6a467be66f6f16b (diff) | |
parent | 887cad973f25f43d2d4502ff31657f156a44b188 (diff) | |
download | tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.gz tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.bz2 tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.lz tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.xz tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.tar.zst tangerine-wallet-browser-ab7eb73ecc9e1eac8a5934a59c186e9f92a0bb32.zip |
fix merge conflicts
Diffstat (limited to 'app')
45 files changed, 856 insertions, 493 deletions
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 1d2619672..c06a99250 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -84,7 +84,7 @@ "message": "Auf Coinbase kaufen" }, "buyCoinbaseExplainer": { - "message": "Coinbase ist die weltweit bekannteste Art und Weise um bitcoin, ethereum und litecoin zu kaufen und verkaufen." + "message": "Coinbase ist die weltweit bekannteste Art und Weise um Bitcoin, Ethereum und Litecoin zu kaufen und verkaufen." }, "ok": { "message": "Ok" @@ -828,7 +828,7 @@ "message": "Willkommen zur neuen Oberfläche (Beta)" }, "uiWelcomeMessage": { - "message": "Du verwendest nun die neue Metamask Oberfläche. Schau dich um, teste die neuen Features wie z.B. das Senden von Token und lass es uns wissen falls du irgendwelche Probleme hast." + "message": "Du verwendest nun die neue MetaMask Oberfläche. Schau dich um, teste die neuen Features wie z.B. das Senden von Token und lass es uns wissen falls du irgendwelche Probleme hast." }, "unapproved": { "message": "Nicht genehmigt" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1b0183c92..a25a2bd59 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2,6 +2,9 @@ "accept": { "message": "Accept" }, + "accessingYourCamera": { + "message": "Accesing your camera..." + }, "account": { "message": "Account" }, @@ -96,7 +99,7 @@ "message": "Buy on Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin." + "message": "Coinbase is the world’s most popular way to buy and sell Bitcoin, Ethereum, and Litecoin." }, "bytes": { "message": "Bytes" @@ -116,8 +119,8 @@ "close": { "message": "Close" }, - "chromeRequiredForTrezor":{ - "message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device." + "chromeRequiredForHardwareWallets":{ + "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, "confirm": { "message": "Confirm" @@ -143,15 +146,12 @@ "connecting": { "message": "Connecting..." }, + "connectToLedger": { + "message": "Connect to Ledger" + }, "connectToTrezor": { "message": "Connect to Trezor" }, - "connectToTrezorHelp": { - "message": "Metamask is able to access your TREZOR ethereum accounts. First make sure your device is connected and unlocked." - }, - "connectToTrezorTrouble": { - "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware." - }, "continue": { "message": "Continue" }, @@ -286,8 +286,8 @@ "downloadStateLogs": { "message": "Download State Logs" }, - "dontHaveATrezorWallet": { - "message": "Don't have a TREZOR hardware wallet?" + "dontHaveAHardwareWallet": { + "message": "Don’t have a hardware wallet?" }, "dropped": { "message": "Dropped" @@ -423,11 +423,11 @@ "hardwareWalletConnected": { "message": "Hardware wallet connected" }, - "hardwareSupport": { - "message": "Hardware Support" + "hardwareWallets": { + "message": "Connect a hardware wallet" }, - "hardwareSupportMsg": { - "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works." + "hardwareWalletsMsg": { + "message": "Select a hardware wallet you'd like to use with MetaMask" }, "havingTroubleConnecting": { "message": "Having trouble connecting?" @@ -535,6 +535,9 @@ "learnMore": { "message": "Learn more" }, + "ledgerAccountRestriction": { + "message": "You need to make use your last account before you can add a new one." + }, "lessThanMax": { "message": "must be less than or equal to $1.", "description": "helper for inputting hex as decimal input" @@ -656,6 +659,12 @@ "notStarted": { "message": "Not Started" }, + "noWebcamFoundTitle": { + "message": "Webcam not found" + }, + "noWebcamFound": { + "message": "Your computer's webcam was not found. Please try again." + }, "oldUI": { "message": "Old UI" }, @@ -899,7 +908,7 @@ "description": "displays token symbol" }, "orderOneHere": { - "message": "Order one here." + "message": "Order a Trezor or Ledger and keep your funds in cold storage" }, "searchTokens": { "message": "Search Tokens" @@ -911,7 +920,13 @@ "message": "Select an Account" }, "selectAnAccountHelp": { - "message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask." + "message": "Select the account to view in MetaMask" + }, + "selectHdPath": { + "message": "Select HD Path" + }, + "selectPathHelp": { + "message": "If you don't see your existing Ledger accounts below, try switching paths to \"Legacy (MEW / MyCrypto)\"" }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" @@ -940,6 +955,12 @@ "info": { "message": "Info" }, + "scanInstructions": { + "message": "Place the QR code in front of your camera" + }, + "scanQrCode": { + "message": "Scan QR Code" + }, "shapeshiftBuy": { "message": "Buy with Shapeshift" }, @@ -1059,6 +1080,9 @@ "message": "We had trouble loading your token balances. You can view them ", "description": "Followed by a link (here) to view token balances" }, + "tryAgain": { + "message": "Try again" + }, "twelveWords": { "message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret." }, @@ -1069,7 +1093,7 @@ "message": "Welcome to the New UI (Beta)" }, "uiWelcomeMessage": { - "message": "You are now using the new Metamask UI." + "message": "You are now using the new MetaMask UI." }, "unapproved": { "message": "Unapproved" @@ -1089,6 +1113,15 @@ "unknownNetworkId": { "message": "Unknown network ID" }, + "unknownQrCode": { + "message": "Error: We couldn't identify that QR code" + }, + "unknownCameraErrorTitle": { + "message": "Ooops! Something went wrong...." + }, + "unknownCameraError": { + "message": "There was an error while trying to access you camera. Please try again..." + }, "unlock": { "message": "Unlock" }, @@ -1135,6 +1168,9 @@ "whatsThis": { "message": "What's this?" }, + "youNeedToAllowCameraAccess": { + "message": "You need to allow camera access to use this feature." + }, "yourSigRequested": { "message": "Your signature is being requested" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index f287674f1..ed7f8f681 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -75,7 +75,7 @@ "message": "Pedir prestado con Dharma (Beta)" }, "builtInCalifornia": { - "message": "Metamask fue diseñado y construido en California" + "message": "MetaMask fue diseñado y construido en California" }, "buy": { "message": "Comprar" @@ -874,7 +874,7 @@ "message": "Advertencia" }, "welcomeBeta": { - "message": "Bienvenido a Metamask Beta" + "message": "Bienvenido a MetaMask Beta" }, "whatsThis": { "message": "¿Qué es esto?" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index da2cfe4f8..1463e2b5f 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -63,7 +63,7 @@ "message": "Acheter sur Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase est le moyen le plus populaire au monde d'acheter et de vendre du bitcoin, de l'ethereum et du litecoin." + "message": "Coinbase est le moyen le plus populaire au monde d'acheter et de vendre du Bitcoin, de l'Ethereum et du Litecoin." }, "cancel": { "message": "Annuler" @@ -570,7 +570,7 @@ "message": "Bienvenue dans la nouvelle interface utilisateur (Beta)" }, "uiWelcomeMessage": { - "message": "Vous utilisez maintenant la nouvelle interface utilisateur Metamask. Jetez un coup d'oeil, essayez de nouvelles fonctionnalités comme l'envoi de jetons, et faites-nous savoir si vous avez des problèmes." + "message": "Vous utilisez maintenant la nouvelle interface utilisateur MetaMask. Jetez un coup d'oeil, essayez de nouvelles fonctionnalités comme l'envoi de jetons, et faites-nous savoir si vous avez des problèmes." }, "unavailable": { "message": "Indisponible" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index ef3ed4025..2ae5fae72 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -81,7 +81,7 @@ "message": "Compra su Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase è il servizio più popolare al mondo per comprare e vendere bitcoin, ethereum e litecoin." + "message": "Coinbase è il servizio più popolare al mondo per comprare e vendere Bitcoin, Ethereum e Litecoin." }, "cancel": { "message": "Cancella" @@ -178,7 +178,7 @@ "message": "La rete predefinita per transazioni in Ether è la Rete Ethereum Principale." }, "denExplainer": { - "message": "Il DEN è il tuo archivio crittato con password dentro Metamask." + "message": "Il DEN è il tuo archivio crittato con password dentro MetaMask." }, "deposit": { "message": "Deposita" @@ -816,4 +816,4 @@ "youSign": { "message": "Ti stai connettendo" } -}
\ No newline at end of file +} diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 38289f602..46847f1bf 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -81,7 +81,7 @@ "message": "Koop op Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase is 's werelds populairste manier om bitcoin, ethereum en litecoin te kopen en verkopen." + "message": "Coinbase is 's werelds populairste manier om Bitcoin, Ethereum en Litecoin te kopen en verkopen." }, "cancel": { "message": "Annuleer" @@ -435,7 +435,7 @@ "message": "back-up woorden hebben alleen kleine letters" }, "mainnet": { - "message": "belangrijkste ethereum-netwerk" + "message": "belangrijkste Ethereum-netwerk" }, "message": { "message": "Bericht" @@ -762,7 +762,7 @@ "message": "Welkom bij de nieuwe gebruikersinterface (bèta)" }, "uiWelcomeMessage": { - "message": "U gebruikt nu de nieuwe gebruikersinterface van Metamask. Kijk rond, probeer nieuwe functies uit zoals het verzenden van tokens en laat ons weten of u problemen ondervindt." + "message": "U gebruikt nu de nieuwe gebruikersinterface van MetaMask. Kijk rond, probeer nieuwe functies uit zoals het verzenden van tokens en laat ons weten of u problemen ondervindt." }, "unavailable": { "message": "Niet beschikbaar" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 29d63be02..9a243447a 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -63,7 +63,7 @@ "message": "Bumili sa Coinbase" }, "buyCoinbaseExplainer": { - "message": "Ang Coinbase ang pinakasikat na paraan upang bumili at magbenta ng bitcoin, ethereum, at litecoin sa buong mundo." + "message": "Ang Coinbase ang pinakasikat na paraan upang bumili at magbenta ng Bitcoin, Ethereum, at Litecoin sa buong mundo." }, "cancel": { "message": "Kanselahin" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e770392d0..6f0bb1584 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -81,7 +81,7 @@ "message": "Comprar no Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase é a forma mais conhecida para comprar e vender bitcoin, ethereum, e litecoin." + "message": "Coinbase é a forma mais conhecida para comprar e vender Bitcoin, Ethereum, e Litecoin." }, "cancel": { "message": "Cancelar" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b43251064..bb722735d 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -84,7 +84,7 @@ "message": "Купить на Coinbase" }, "buyCoinbaseExplainer": { - "message": "Биржа Coinbase – это наиболее популярный способ купить или продать bitcoin, ethereum и litecoin." + "message": "Биржа Coinbase – это наиболее популярный способ купить или продать Bitcoin, Ethereum и Litecoin." }, "ok": { "message": "ОК" diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 3d7dec226..c9aaa0394 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -762,7 +762,7 @@ "message": "ยินดีต้อนรับสู่หน้าตาใหม่ (เบต้า)" }, "uiWelcomeMessage": { - "message": "ขณะนี้คุณใช้งาน Metamask หน้าตาใหม่แล้ว ลองใช้ความสามรถใหม่ ๆ เช่นการส่งโทเค็นและหากพบปัญหากรุณาแจ้งให้เราทราบ" + "message": "ขณะนี้คุณใช้งาน MetaMask หน้าตาใหม่แล้ว ลองใช้ความสามรถใหม่ ๆ เช่นการส่งโทเค็นและหากพบปัญหากรุณาแจ้งให้เราทราบ" }, "unavailable": { "message": "ใช้งานไม่ได้" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index c2c09bc91..08ba6cde8 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -84,7 +84,7 @@ "message": "Coinbase'de satın al" }, "buyCoinbaseExplainer": { - "message": "Coinbase bitcoin, ethereum, and litecoin alıp satmanın dünyadaki en popüler yolu" + "message": "Coinbase Bitcoin, Ethereum, and Litecoin alıp satmanın dünyadaki en popüler yolu" }, "ok": { "message": "Tamam" @@ -852,7 +852,7 @@ "message": "Yeni UI (Beta)'ya hoşgeldiniz" }, "uiWelcomeMessage": { - "message": "Şu anda yeni Metamask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin" + "message": "Şu anda yeni MetaMask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin" }, "unapproved": { "message": "Onaylanmadı" @@ -909,4 +909,4 @@ "youSign": { "message": "İmzalıyorsunuz" } -}
\ No newline at end of file +} diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index cd30de4de..782dfd119 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -570,7 +570,7 @@ "message": "Chào mừng bạn đến với giao diện mới (Beta)" }, "uiWelcomeMessage": { - "message": "Bạn đang sử dụng giao diện mới của Metamask. Chúng tôi khuyến khích bạn thử nghiệm và khám phá các tính năng mới như gửi token, và nếu bạn có gặp phải vấn đề gì khó khăn, xin hãy liên hệ ngay để chúng tôi có thể giúp đỡ bạn." + "message": "Bạn đang sử dụng giao diện mới của MetaMask. Chúng tôi khuyến khích bạn thử nghiệm và khám phá các tính năng mới như gửi token, và nếu bạn có gặp phải vấn đề gì khó khăn, xin hãy liên hệ ngay để chúng tôi có thể giúp đỡ bạn." }, "unavailable": { "message": "Không có sẵn" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 241ea948d..a39bba9da 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -879,7 +879,7 @@ "message": "欢迎使用新版界面 (Beta)" }, "uiWelcomeMessage": { - "message": "你现在正在使用新的 Metamask 界面。 尝试发送代币等新功能,有任何问题请告知我们。" + "message": "你现在正在使用新的 MetaMask 界面。 尝试发送代币等新功能,有任何问题请告知我们。" }, "unapproved": { "message": "未批准" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 9aaee0e16..200c75c3c 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -362,7 +362,7 @@ "message": "你想怎麼存入 Ether?" }, "holdEther": { - "message": "Metamask 讓您能保存 ether 和代幣, 並成為您接觸分散式應用程式的途徑." + "message": "MetaMask 讓您能保存 ether 和代幣, 並成為您接觸分散式應用程式的途徑." }, "import": { "message": "導入", diff --git a/app/images/ledger-logo.svg b/app/images/ledger-logo.svg new file mode 100644 index 000000000..21b99d0e5 --- /dev/null +++ b/app/images/ledger-logo.svg @@ -0,0 +1 @@ +<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1916.3 516.8" width="2500" height="674"><style>.st0{fill:#333745}</style><g id="squares_1_"><path class="st0" d="M578.2 392.7V24.3h25.6v344.1h175.3v24.3H578.2zm327.5 5.1c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5H804.6c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.3 2.6-28.9 3.9-43.5 3.8zM898 135.6c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm238-21.8c19.2 0 37.1 3.8 51.2 10.2 14.1 7.7 26.9 19.2 38.4 37.1h1.3c-1.3-21.7-1.3-42.2-1.3-62.7V0h24.3v392.7h-16.6l-6.4-42.2c-20.5 30.7-51.2 47.3-89.6 47.3s-66.5-11.5-87-35.8c-20.5-23-29.4-57.6-29.4-102.3 0-47.3 10.2-83.2 29.4-108.7 19.2-25.6 48.6-37.2 85.7-37.2zm0 21.8c-29.4 0-52.4 10.2-67.8 32-15.3 20.5-23 51.2-23 92.1 0 78 30.7 116.4 90.8 116.4 30.7 0 53.7-9 67.8-26.9 14.1-17.9 21.7-47.3 21.7-89.6v-3.8c0-42.2-7.7-72.9-21.7-90.8-12.8-20.5-35.8-29.4-67.8-29.4zm379.9-16.6v17.9l-56.3 3.8c15.3 19.2 23 39.7 23 61.4 0 26.9-9 47.3-26.9 64-17.9 16.6-40.9 24.3-70.4 24.3-12.8 0-21.7 0-25.6-1.3-10.2 5.1-17.9 11.5-23 17.9-5.1 7.7-7.7 14.1-7.7 23s3.8 15.3 10.2 19.2c6.4 3.8 17.9 6.4 33.3 6.4h47.3c29.4 0 52.4 6.4 67.8 17.9s24.3 29.4 24.3 53.7c0 29.4-11.5 51.2-34.5 66.5-23 15.3-56.3 23-99.8 23-34.5 0-61.4-6.4-80.6-20.5-19.2-12.8-28.1-32-28.1-55 0-19.2 6.4-34.5 17.9-47.3s28.1-20.5 47.3-25.6c-7.7-3.8-15.3-9-19.2-15.3-5-6.2-7.7-13.8-7.7-21.7 0-17.9 11.5-34.5 34.5-48.6-15.3-6.4-28.1-16.6-37.1-30.7-9-14.1-12.8-30.7-12.8-48.6 0-26.9 9-49.9 25.6-66.5 17.9-16.6 40.9-24.3 70.4-24.3 17.9 0 32 1.3 42.2 5.1h85.7v1.3h.2zm-222.6 319.8c0 37.1 28.1 56.3 84.4 56.3 71.6 0 107.5-23 107.5-69.1 0-16.6-5.1-28.1-16.6-35.8-11.5-7.7-29.4-11.5-55-11.5h-44.8c-49.9 1.2-75.5 20.4-75.5 60.1zm21.8-235.4c0 21.7 6.4 37.1 19.2 49.9 12.8 11.5 29.4 17.9 51.2 17.9 23 0 40.9-6.4 52.4-17.9 12.8-11.5 17.9-28.1 17.9-49.9 0-23-6.4-40.9-19.2-52.4-12.8-11.5-29.4-17.9-52.4-17.9-21.7 0-39.7 6.4-51.2 19.2-12.8 11.4-17.9 29.3-17.9 51.1z"/><path class="st0" d="M1640 397.8c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5h-197c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.1 2.6-28.2 3.8-44.8 3.8zm-6.4-262.2c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm245.6-21.8c11.5 0 24.3 1.3 37.1 3.8l-5.1 24.3c-11.8-2.6-23.8-3.9-35.8-3.8-23 0-42.2 10.2-57.6 29.4-15.3 20.5-23 44.8-23 75.5v149.7h-25.6V119h21.7l2.6 49.9h1.3c11.5-20.5 23-34.5 35.8-42.2 15.4-9 30.7-12.9 48.6-12.9zM333.9 12.8h-183v245.6h245.6V76.7c.1-34.5-28.1-63.9-62.6-63.9zm-239.2 0H64c-34.5 0-64 28.1-64 64v30.7h94.7V12.8zM0 165h94.7v94.7H0V165zm301.9 245.6h30.7c34.5 0 64-28.1 64-64V316h-94.7v94.6zm-151-94.6h94.7v94.7h-94.7V316zM0 316v30.7c0 34.5 28.1 64 64 64h30.7V316H0z"/></g></svg>
\ No newline at end of file diff --git a/app/images/metamask-fox.svg b/app/images/logo/metamask-fox.svg index f3c24f79e..f3c24f79e 100644 --- a/app/images/metamask-fox.svg +++ b/app/images/logo/metamask-fox.svg diff --git a/app/images/logo/metamask-logo-horizontal-beta.svg b/app/images/logo/metamask-logo-horizontal-beta.svg new file mode 100644 index 000000000..b1fa040ac --- /dev/null +++ b/app/images/logo/metamask-logo-horizontal-beta.svg @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns:ev="http://www.w3.org/2001/xml-events" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1672.1 241.2" + style="enable-background:new 0 0 1672.1 241.2;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#161616;} + .st1{fill:#E17726;stroke:#E17726;stroke-linecap:round;stroke-linejoin:round;} + .st2{fill:#E27625;stroke:#E27625;stroke-linecap:round;stroke-linejoin:round;} + .st3{fill:#D5BFB2;stroke:#D5BFB2;stroke-linecap:round;stroke-linejoin:round;} + .st4{fill:#233447;stroke:#233447;stroke-linecap:round;stroke-linejoin:round;} + .st5{fill:#CC6228;stroke:#CC6228;stroke-linecap:round;stroke-linejoin:round;} + .st6{fill:#E27525;stroke:#E27525;stroke-linecap:round;stroke-linejoin:round;} + .st7{fill:#F5841F;stroke:#F5841F;stroke-linecap:round;stroke-linejoin:round;} + .st8{fill:#C0AC9D;stroke:#C0AC9D;stroke-linecap:round;stroke-linejoin:round;} + .st9{fill:#161616;stroke:#161616;stroke-linecap:round;stroke-linejoin:round;} + .st10{fill:#763E1A;stroke:#763E1A;stroke-linecap:round;stroke-linejoin:round;} + .st11{fill:#F5841F;} +</style> +<g> + <path class="st0" d="M1157.7,121.9c-6.8-4.5-14.3-7.7-21.4-11.7c-4.6-2.6-9.5-4.9-13.5-8.2c-6.8-5.6-5.4-16.6,1.7-21.4 + c10.2-6.8,27.1-3,28.9,10.9c0,0.3,0.3,0.5,0.6,0.5h15.4c0.4,0,0.7-0.3,0.6-0.7c-0.8-9.6-4.5-17.6-11.3-22.7 + c-6.5-4.9-13.9-7.5-21.8-7.5c-40.7,0-44.4,43.1-22.5,56.7c2.5,1.6,24,12.4,31.6,17.1s10,13.3,6.7,20.1c-3,6.2-10.8,10.5-18.6,10 + c-8.5-0.5-15.1-5.1-17.4-12.3c-0.4-1.3-0.6-3.8-0.6-4.9c0-0.3-0.3-0.6-0.6-0.6h-16.7c-0.3,0-0.6,0.3-0.6,0.6 + c0,12.1,3,18.8,11.2,24.9c7.7,5.8,16.1,8.2,24.8,8.2c22.8,0,34.6-12.9,37-26.3C1173.3,141.5,1169.4,129.7,1157.7,121.9z"/> + <path class="st0" d="M432.6,63.3h-7.4h-8.1c-0.3,0-0.5,0.2-0.6,0.4l-13.7,45.2c-0.2,0.6-1,0.6-1.2,0l-13.7-45.2 + c-0.1-0.3-0.3-0.4-0.6-0.4h-8.1h-7.4h-10c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V91.6 + c0-0.7,1-0.8,1.2-0.2l13.8,45.5l1,3.2c0.1,0.3,0.3,0.4,0.6,0.4h12.8c0.3,0,0.5-0.2,0.6-0.4l1-3.2l13.8-45.5 + c0.2-0.7,1.2-0.5,1.2,0.2v87.7c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V63.9c0-0.3-0.3-0.6-0.6-0.6L432.6,63.3 + L432.6,63.3z"/> + <path class="st0" d="M902,63.3c-0.3,0-0.5,0.2-0.6,0.4l-13.7,45.2c-0.2,0.6-1,0.6-1.2,0l-13.7-45.2c-0.1-0.3-0.3-0.4-0.6-0.4h-25.4 + c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V91.6c0-0.7,1-0.8,1.2-0.2l13.8,45.5l1,3.2 + c0.1,0.3,0.3,0.4,0.6,0.4h12.8c0.3,0,0.5-0.2,0.6-0.4l1-3.2l13.8-45.5c0.2-0.7,1.2-0.5,1.2,0.2v87.7c0,0.3,0.3,0.6,0.6,0.6h16.7 + c0.3,0,0.6-0.3,0.6-0.6V63.9c0-0.3-0.3-0.6-0.6-0.6L902,63.3L902,63.3z"/> + <path class="st0" d="M686.6,63.3h-31.1h-16.7h-31.1c-0.3,0-0.6,0.3-0.6,0.6v14.4c0,0.3,0.3,0.6,0.6,0.6h30.5v100.4 + c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V78.9h30.5c0.3,0,0.6-0.3,0.6-0.6V63.9C687.2,63.6,687,63.3,686.6,63.3z"/> + <path class="st0" d="M785.1,179.9h15.2c0.4,0,0.7-0.4,0.6-0.8L769.5,63.3c-0.1-0.3-0.3-0.4-0.6-0.4h-5.8h-10.2h-5.8 + c-0.3,0-0.5,0.2-0.6,0.4l-31.4,115.8c-0.1,0.4,0.2,0.8,0.6,0.8h15.2c0.3,0,0.5-0.2,0.6-0.4l9.1-33.7c0.1-0.3,0.3-0.4,0.6-0.4h33.6 + c0.3,0,0.5,0.2,0.6,0.4l9.1,33.7C784.6,179.7,784.9,179.9,785.1,179.9z M745.2,128.9l12.2-45.1c0.2-0.6,1-0.6,1.2,0l12.2,45.1 + c0.1,0.4-0.2,0.8-0.6,0.8h-24.4C745.4,129.7,745.1,129.3,745.2,128.9z"/> + <path class="st0" d="M1044.3,179.9h15.2c0.4,0,0.7-0.4,0.6-0.8l-31.4-115.8c-0.1-0.3-0.3-0.4-0.6-0.4h-5.8h-10.2h-5.8 + c-0.3,0-0.5,0.2-0.6,0.4l-31.4,115.8c-0.1,0.4,0.2,0.8,0.6,0.8h15.2c0.3,0,0.5-0.2,0.6-0.4l9.1-33.7c0.1-0.3,0.3-0.4,0.6-0.4h33.6 + c0.3,0,0.5,0.2,0.6,0.4l9.1,33.7C1043.8,179.7,1044,179.9,1044.3,179.9z M1004.4,128.9l12.2-45.1c0.2-0.6,1-0.6,1.2,0l12.2,45.1 + c0.1,0.4-0.2,0.8-0.6,0.8H1005C1004.6,129.7,1004.3,129.3,1004.4,128.9z"/> + <path class="st0" d="M510.8,162.8V127c0-0.3,0.3-0.6,0.6-0.6h44.5c0.3,0,0.6-0.3,0.6-0.6v-14.4c0-0.3-0.3-0.6-0.6-0.6h-44.5 + c-0.3,0-0.6-0.3-0.6-0.6V79.6c0-0.3,0.3-0.6,0.6-0.6H562c0.3,0,0.6-0.3,0.6-0.6V64c0-0.3-0.3-0.6-0.6-0.6h-51.2h-17.3 + c-0.3,0-0.6,0.3-0.6,0.6v15v31.9v15.6v37v15.8c0,0.3,0.3,0.6,0.6,0.6h17.3h53.3c0.3,0,0.6-0.3,0.6-0.6v-15.2c0-0.3-0.3-0.6-0.6-0.6 + h-52.8C511,163.4,510.8,163.2,510.8,162.8z"/> + <path class="st0" d="M1310.3,178.9l-57.8-59.7c-0.2-0.2-0.2-0.6,0-0.8l52-54c0.4-0.4,0.1-1-0.4-1h-21.3c-0.2,0-0.3,0.1-0.4,0.2 + l-44.1,45.8c-0.4,0.4-1,0.1-1-0.4V64c0-0.3-0.3-0.6-0.6-0.6H1220c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7 + c0.3,0,0.6-0.3,0.6-0.6v-50.8c0-0.5,0.7-0.8,1-0.4l50,51.6c0.1,0.1,0.3,0.2,0.4,0.2h21.3C1310.4,179.9,1310.7,179.2,1310.3,178.9z" + /> +</g> +<g> + <polygon class="st1" points="247.1,1.2 146,76.2 164.8,32 "/> + <g> + <polygon class="st2" points="13.9,1.2 114.1,76.9 96.2,32 "/> + <polygon class="st2" points="210.7,175.1 183.8,216.3 241.4,232.2 257.9,176 "/> + <polygon class="st2" points="3.2,176 19.6,232.2 77.1,216.3 50.3,175.1 "/> + <polygon class="st2" points="74,105.5 58,129.7 115,132.3 113.1,70.8 "/> + <polygon class="st2" points="187,105.5 147.3,70.1 146,132.3 203,129.7 "/> + <polygon class="st2" points="77.1,216.3 111.6,199.6 81.9,176.4 "/> + <polygon class="st2" points="149.4,199.6 183.8,216.3 179.1,176.4 "/> + </g> + <g> + <polygon class="st3" points="183.8,216.3 149.4,199.6 152.2,222 151.9,231.5 "/> + <polygon class="st3" points="77.1,216.3 109.1,231.5 108.9,222 111.6,199.6 "/> + </g> + <polygon class="st4" points="109.7,161.6 81.1,153.2 101.3,143.9 "/> + <polygon class="st4" points="151.3,161.6 159.7,143.9 180,153.2 "/> + <g> + <polygon class="st5" points="77.1,216.3 82.1,175.1 50.3,176 "/> + <polygon class="st5" points="178.9,175.1 183.8,216.3 210.7,176 "/> + <polygon class="st5" points="203,129.7 146,132.3 151.3,161.6 159.7,143.9 180,153.2 "/> + <polygon class="st5" points="81.1,153.2 101.3,143.9 109.7,161.6 115,132.3 58,129.7 "/> + </g> + <g> + <polygon class="st6" points="58,129.7 81.9,176.4 81.1,153.2 "/> + <polygon class="st6" points="180,153.2 179.1,176.4 203,129.7 "/> + <polygon class="st6" points="115,132.3 109.7,161.6 116.4,196.2 117.9,150.6 "/> + <polygon class="st6" points="146,132.3 143.2,150.5 144.6,196.2 151.3,161.6 "/> + </g> + <polygon class="st7" points="151.3,161.6 144.6,196.2 149.4,199.6 179.1,176.4 180,153.2 "/> + <polygon class="st7" points="81.1,153.2 81.9,176.4 111.6,199.6 116.4,196.2 109.7,161.6 "/> + <polygon class="st8" points="151.9,231.5 152.2,222 149.6,219.8 111.4,219.8 108.9,222 109.1,231.5 77.1,216.3 88.3,225.5 + 111,241.2 149.9,241.2 172.7,225.5 183.8,216.3 "/> + <polygon class="st9" points="149.4,199.6 144.6,196.2 116.4,196.2 111.6,199.6 108.9,222 111.4,219.8 149.6,219.8 152.2,222 "/> + <g> + <polygon class="st10" points="251.4,81.1 259.9,39.7 247.1,1.2 149.4,73.7 187,105.5 240.1,121 251.8,107.3 246.7,103.6 + 254.8,96.2 248.6,91.4 256.7,85.2 "/> + <polygon class="st10" points="1.1,39.7 9.7,81.1 4.2,85.2 12.4,91.4 6.2,96.2 14.3,103.6 9.2,107.3 20.9,121 74,105.5 111.6,73.7 + 13.9,1.2 "/> + </g> + <polygon class="st7" points="240.1,121 187,105.5 203,129.7 179.1,176.4 210.7,176 257.9,176 "/> + <polygon class="st7" points="74,105.5 20.9,121 3.2,176 50.3,176 81.9,176.4 58,129.7 "/> + <polygon class="st7" points="146,132.3 149.4,73.7 164.8,32 96.2,32 111.6,73.7 115,132.3 116.3,150.7 116.4,196.2 144.6,196.2 + 144.7,150.7 "/> +</g> +<g> + <path class="st7" d="M1409.7,92.6V32.1h19.3c2.9,0,5.6,0.4,8.2,1c2.5,0.6,4.7,1.6,6.6,2.9c1.9,1.3,3.4,3,4.5,5.1 + c1.1,2.1,1.6,4.5,1.6,7.3c0,3-0.9,5.5-2.5,7.7c-1.6,2.1-3.8,3.8-6.6,4.9c1.7,0.5,3.2,1.1,4.5,2c1.3,0.9,2.4,1.9,3.3,3.1 + c0.9,1.2,1.6,2.6,2,4c0.5,1.5,0.7,3.1,0.7,4.7c0,2.9-0.5,5.4-1.6,7.6c-1.1,2.2-2.5,4-4.4,5.5c-1.9,1.5-4.1,2.6-6.6,3.3 + c-2.6,0.8-5.3,1.2-8.3,1.2H1409.7z M1419.8,57.6h9.6c1.5,0,2.9-0.2,4.2-0.6c1.3-0.4,2.4-0.9,3.3-1.7c0.9-0.7,1.7-1.6,2.2-2.7 + c0.6-1.1,0.8-2.3,0.8-3.7c0-1.5-0.3-2.8-0.8-3.9c-0.5-1.1-1.3-2-2.2-2.7c-1-0.7-2.1-1.2-3.4-1.5c-1.3-0.3-2.7-0.5-4.3-0.5h-9.5 + V57.6z M1419.8,65.2v19.3h10.9c1.6,0,3-0.3,4.3-0.7c1.3-0.5,2.4-1.1,3.4-1.9c0.9-0.8,1.7-1.8,2.2-2.9c0.5-1.2,0.8-2.5,0.8-3.9 + c0-1.5-0.2-2.9-0.7-4.1c-0.5-1.2-1.1-2.2-2-3.1c-0.9-0.8-2-1.5-3.2-1.9c-1.3-0.5-2.7-0.7-4.3-0.7H1419.8z"/> + <path class="st7" d="M1506.3,65.5h-24.9v19h29.1v8.1h-39.1V32.1h38.8v8.2h-28.8v17.1h24.9V65.5z"/> + <path class="st7" d="M1574.8,40.4h-18.6v52.2h-9.9V40.4h-18.4v-8.2h46.9V40.4z"/> + <path class="st7" d="M1615.2,78.6h-18.9l-4.2,14h-10.3l19.6-60.5h8.8l19.3,60.5h-10.3L1615.2,78.6z M1598.9,70h13.9l-6.9-23.7 + L1598.9,70z"/> +</g> +<path class="st11" d="M1644.3,8c10.7,0,19.5,8.7,19.5,19.5v69.8c0,10.7-8.7,19.5-19.5,19.5H1395c-10.7,0-19.5-8.7-19.5-19.5V27.5 + c0-10.7,8.7-19.5,19.5-19.5H1644.3 M1644.3,0H1395c-15.2,0-27.5,12.3-27.5,27.5v69.8c0,15.2,12.3,27.5,27.5,27.5h249.2 + c15.2,0,27.5-12.3,27.5-27.5V27.5C1671.7,12.3,1659.4,0,1644.3,0L1644.3,0z"/> +</svg> diff --git a/app/images/trezor-logo.svg b/app/images/trezor-logo.svg new file mode 100644 index 000000000..b8d85e3af --- /dev/null +++ b/app/images/trezor-logo.svg @@ -0,0 +1 @@ +<svg id="trezor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2567.5 722.3" width="2567.5" height="722.3"><style>.st0{display:none;fill:#fff;stroke:#000}</style><path id="rect25" class="st0" d="M1186 2932.6h46.2v147H1186v-147z"/><path id="path7" d="M249 0C149.9 0 69.7 80.2 69.7 179.3v67.2C34.9 252.8 0 261.2 0 272.1v350.7s0 9.7 10.9 14.3c39.5 16 194.9 71 230.6 83.6 4.6 1.7 5.9 1.7 7.1 1.7 1.7 0 2.5 0 7.1-1.7 35.7-12.6 191.5-67.6 231-83.6 10.1-4.2 10.5-13.9 10.5-13.9V272.1c0-10.9-34.4-19.7-69.3-25.6v-67.2C428.4 80.2 347.7 0 249 0zm0 85.7c58.4 0 93.7 35.3 93.7 93.7v58.4c-65.5-4.6-121.4-4.6-187.3 0v-58.4c0-58.5 35.3-93.7 93.6-93.7zm-.4 238.1c81.5 0 149.9 6.3 149.9 17.6v218.8c0 3.4-.4 3.8-3.4 5-2.9 1.3-139 50.4-139 50.4s-5.5 1.7-7.1 1.7c-1.7 0-7.1-2.1-7.1-2.1s-136.1-49.1-139-50.4-3.4-1.7-3.4-5V341c-.8-11.3 67.6-17.2 149.1-17.2z"/><g id="g3222" transform="translate(91.363 -287.434) scale(.95575)"><path id="path13" d="M666.6 890V639.3H575v-89.9h285.6v89.9h-90.7V890H666.6z"/><path id="path15" d="M1092 890l-47-107.1h-37.4V890H904.3V549.4h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5H1092zm12.2-223.9c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.2-.4 33.6-8.4 33.6-27.3z"/><path id="path17" d="M1262.9 890V549.4h258.3v89.9h-155.4v33.6h151.6v89.9h-151.6v37.4h155.4V890h-258.3z"/><path id="path19" d="M1574.9 890.4v-81.9l129.8-168.8h-129.8v-89.9h265.8v81.1l-130.2 169.7h134v89.9l-269.6-.1z"/><path id="path21" d="M1869.7 720.3c0-104.6 81.1-176.4 186.5-176.4 105 0 186.5 71.4 186.5 176.4 0 104.6-81.1 176-186.5 176s-186.5-71.4-186.5-176zm268 0c0-47.5-32.3-85.3-81.9-85.3-49.6 0-81.9 37.8-81.9 85.3s32.3 85.3 81.9 85.3c50 0 81.9-37.8 81.9-85.3z"/><path id="path23" d="M2473.6 890.4l-47-107.1h-37.4v107.1h-103.3V549.8h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5h-117.1zm12.6-224.3c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.3-.4 33.6-8.4 33.6-27.3z"/></g></svg>
\ No newline at end of file diff --git a/app/images/webcam.svg b/app/images/webcam.svg new file mode 100644 index 000000000..4b9b58148 --- /dev/null +++ b/app/images/webcam.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="53px" height="53px" viewBox="0 0 53 53" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch --> + <title>webcam</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="QR-Code-Scan" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group-4-Copy" transform="translate(-482.000000, -218.000000)"> + <g id="webcam" transform="translate(482.000000, 218.000000)"> + <circle id="Oval" fill="#D5ECFA" cx="26.5" cy="26.5" r="26.5"></circle> + <g id="Group" transform="translate(14.000000, 19.000000)" fill="#259DE5"> + <rect id="Rectangle" x="0" y="0" width="18" height="16"></rect> + <polygon id="Triangle" points="19 6.57142857 26 3 26 13 19 9.42857143"></polygon> + </g> + </g> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/manifest.json b/app/manifest.json index fb6f98001..1e12f35b4 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.9.1", + "version": "4.9.3", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/background.js b/app/scripts/background.js index 3d3afdd4e..c7395c810 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -19,7 +19,7 @@ const PortStream = require('./lib/port-stream.js') const createStreamSink = require('./lib/createStreamSink') const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') -const firstTimeState = require('./first-time-state') +const rawFirstTimeState = require('./first-time-state') const setupRaven = require('./lib/setupRaven') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') @@ -34,6 +34,9 @@ const { ENVIRONMENT_TYPE_FULLSCREEN, } = require('./lib/enums') +// METAMASK_TEST_CONFIG is used in e2e tests to set the default network to localhost +const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG) + const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = process.env.METAMASK_DEBUG diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index b7496f318..e0a2b0061 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -198,6 +198,6 @@ function blacklistedDomainCheck () { */ function redirectToPhishingWarning () { console.log('MetaMask - routing to Phishing Warning component') - let extensionURL = extension.runtime.getURL('phishing.html') + const extensionURL = extension.runtime.getURL('phishing.html') window.location.href = extensionURL } diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 4c97810a3..465751e61 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -80,7 +80,7 @@ class BalanceController { } }) this.accountTracker.store.subscribe(update) - this.blockTracker.on('block', update) + this.blockTracker.on('latest', update) } /** diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index a93aff49b..d5bc5fe2b 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -1,4 +1,4 @@ - const ObservableStore = require('obs-store') +const ObservableStore = require('obs-store') const extend = require('xtend') const log = require('loglevel') diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js new file mode 100644 index 000000000..41af4d9f9 --- /dev/null +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -0,0 +1,25 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createBlockReEmitMiddleware = require('eth-json-rpc-middleware/block-reemit') +const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') +const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const createInfuraMiddleware = require('eth-json-rpc-infura') +const BlockTracker = require('eth-block-tracker') + +module.exports = createInfuraClient + +function createInfuraClient ({ network }) { + const infuraMiddleware = createInfuraMiddleware({ network }) + const blockProvider = providerFromMiddleware(infuraMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider }) + + const networkMiddleware = mergeMiddleware([ + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockReEmitMiddleware({ blockTracker, provider: blockProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + infuraMiddleware, + ]) + return { networkMiddleware, blockTracker } +} diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js new file mode 100644 index 000000000..40c353f7f --- /dev/null +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -0,0 +1,25 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') +const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const BlockTracker = require('eth-block-tracker') + +module.exports = createJsonRpcClient + +function createJsonRpcClient ({ rpcUrl }) { + const fetchMiddleware = createFetchMiddleware({ rpcUrl }) + const blockProvider = providerFromMiddleware(fetchMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + fetchMiddleware, + ]) + return { networkMiddleware, blockTracker } +} diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js new file mode 100644 index 000000000..fecc512e8 --- /dev/null +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -0,0 +1,21 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const BlockTracker = require('eth-block-tracker') + +module.exports = createLocalhostClient + +function createLocalhostClient () { + const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) + const blockProvider = providerFromMiddleware(fetchMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + fetchMiddleware, + ]) + return { networkMiddleware, blockTracker } +} diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js new file mode 100644 index 000000000..8b17829b7 --- /dev/null +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -0,0 +1,43 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const createWalletSubprovider = require('eth-json-rpc-middleware/wallet') + +module.exports = createMetamaskMiddleware + +function createMetamaskMiddleware ({ + version, + getAccounts, + processTransaction, + processEthSignMessage, + processTypedMessage, + processPersonalMessage, + getPendingNonce, +}) { + const metamaskMiddleware = mergeMiddleware([ + createScaffoldMiddleware({ + // staticSubprovider + eth_syncing: false, + web3_clientVersion: `MetaMask/v${version}`, + }), + createWalletSubprovider({ + getAccounts, + processTransaction, + processEthSignMessage, + processTypedMessage, + processPersonalMessage, + }), + createPendingNonceMiddleware({ getPendingNonce }), + ]) + return metamaskMiddleware +} + +function createPendingNonceMiddleware ({ getPendingNonce }) { + return createAsyncMiddleware(async (req, res, next) => { + if (req.method !== 'eth_getTransactionCount') return next() + const address = req.params[0] + const blockRef = req.params[1] + if (blockRef !== 'pending') return next() + req.result = await getPendingNonce(address) + }) +} diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index b6f7705b5..76fdc3391 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,15 +1,17 @@ const assert = require('assert') const EventEmitter = require('events') -const createMetamaskProvider = require('web3-provider-engine/zero.js') -const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js') -const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') -const extend = require('xtend') const EthQuery = require('eth-query') -const createEventEmitterProxy = require('../../lib/events-proxy.js') +const JsonRpcEngine = require('json-rpc-engine') +const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine') const log = require('loglevel') -const urlUtil = require('url') +const createMetamaskMiddleware = require('./createMetamaskMiddleware') +const createInfuraClient = require('./createInfuraClient') +const createJsonRpcClient = require('./createJsonRpcClient') +const createLocalhostClient = require('./createLocalhostClient') +const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') + const { ROPSTEN, RINKEBY, @@ -17,7 +19,6 @@ const { MAINNET, LOCALHOST, } = require('./enums') -const LOCALHOST_RPC_URL = 'http://localhost:8545' const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] const env = process.env.METAMASK_ENV @@ -39,21 +40,27 @@ module.exports = class NetworkController extends EventEmitter { this.providerStore = new ObservableStore(providerConfig) this.networkStore = new ObservableStore('loading') this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - // create event emitter proxy - this._proxy = createEventEmitterProxy() - this.on('networkDidChange', this.lookupNetwork) + // provider and block tracker + this._provider = null + this._blockTracker = null + // provider and block tracker proxies - because the network changes + this._providerProxy = null + this._blockTrackerProxy = null } - initializeProvider (_providerParams) { - this._baseProviderParams = _providerParams + initializeProvider (providerParams) { + this._baseProviderParams = providerParams const { type, rpcTarget } = this.providerStore.getState() this._configureProvider({ type, rpcTarget }) - this._proxy.on('block', this._logBlock.bind(this)) - this._proxy.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this._proxy) this.lookupNetwork() - return this._proxy + } + + // return the proxies so the references will always be good + getProviderAndBlockTracker () { + const provider = this._providerProxy + const blockTracker = this._blockTrackerProxy + return { provider, blockTracker } } verifyNetwork () { @@ -75,10 +82,11 @@ module.exports = class NetworkController extends EventEmitter { lookupNetwork () { // Prevent firing when provider is not defined. - if (!this.ethQuery || !this.ethQuery.sendAsync) { - return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery') + if (!this._provider) { + return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } - this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + const ethQuery = new EthQuery(this._provider) + ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) this.setNetworkState(network) @@ -131,7 +139,7 @@ module.exports = class NetworkController extends EventEmitter { this._configureInfuraProvider(opts) // other type-based rpc endpoints } else if (type === LOCALHOST) { - this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL }) + this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc') { this._configureStandardProvider({ rpcUrl: rpcTarget }) @@ -141,49 +149,47 @@ module.exports = class NetworkController extends EventEmitter { } _configureInfuraProvider ({ type }) { - log.info('_configureInfuraProvider', type) - const infuraProvider = createInfuraProvider({ network: type }) - const infuraSubprovider = new SubproviderFromProvider(infuraProvider) - const providerParams = extend(this._baseProviderParams, { - engineParams: { - pollingInterval: 8000, - blockTrackerProvider: infuraProvider, - }, - dataSubprovider: infuraSubprovider, - }) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) + log.info('NetworkController - configureInfuraProvider', type) + const networkClient = createInfuraClient({ network: type }) + this._setNetworkClient(networkClient) + } + + _configureLocalhostProvider () { + log.info('NetworkController - configureLocalhostProvider') + const networkClient = createLocalhostClient() + this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl }) { - // urlUtil handles malformed urls - rpcUrl = urlUtil.parse(rpcUrl).format() - const providerParams = extend(this._baseProviderParams, { - rpcUrl, - engineParams: { - pollingInterval: 8000, - }, - }) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) - } - - _setProvider (provider) { - // collect old block tracker events - const oldProvider = this._provider - let blockTrackerHandlers - if (oldProvider) { - // capture old block handlers - blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers - // tear down - oldProvider.removeAllListeners() - oldProvider.stop() + log.info('NetworkController - configureStandardProvider', rpcUrl) + const networkClient = createJsonRpcClient({ rpcUrl }) + this._setNetworkClient(networkClient) + } + + _setNetworkClient ({ networkMiddleware, blockTracker }) { + const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams) + const engine = new JsonRpcEngine() + engine.push(metamaskMiddleware) + engine.push(networkMiddleware) + const provider = providerFromEngine(engine) + this._setProviderAndBlockTracker({ provider, blockTracker }) + } + + _setProviderAndBlockTracker ({ provider, blockTracker }) { + // update or intialize proxies + if (this._providerProxy) { + this._providerProxy.setTarget(provider) + } else { + this._providerProxy = createSwappableProxy(provider) + } + if (this._blockTrackerProxy) { + this._blockTrackerProxy.setTarget(blockTracker) + } else { + this._blockTrackerProxy = createEventEmitterProxy(blockTracker) } - // override block tracler - provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) - // set as new provider + // set new provider and blockTracker this._provider = provider - this._proxy.setTarget(provider) + this._blockTracker = blockTracker } _logBlock (block) { diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index 926268691..d270f6f44 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -1,14 +1,14 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const BN = require('ethereumjs-util').BN const EthQuery = require('eth-query') const log = require('loglevel') +const pify = require('pify') class RecentBlocksController { /** * Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled - * upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event + * upon the controller's construction and then the list is updated when the given block tracker gets a 'latest' event * (indicating that there is a new block to process). * * @typedef {Object} RecentBlocksController @@ -16,7 +16,7 @@ class RecentBlocksController { * @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain * @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance. * @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction, - * listens for 'block' events so that new blocks can be processed and added to storage. + * listens for 'latest' events so that new blocks can be processed and added to storage. * @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider * @property {number} historyLength The maximum length of blocks to track * @property {object} store Stores the recentBlocks @@ -34,7 +34,13 @@ class RecentBlocksController { }, opts.initState) this.store = new ObservableStore(initState) - this.blockTracker.on('block', this.processBlock.bind(this)) + this.blockTracker.on('latest', async (newBlockNumberHex) => { + try { + await this.processBlock(newBlockNumberHex) + } catch (err) { + log.error(err) + } + }) this.backfill() } @@ -55,7 +61,11 @@ class RecentBlocksController { * @param {object} newBlock The new block to modify and add to the recentBlocks array * */ - processBlock (newBlock) { + async processBlock (newBlockNumberHex) { + const newBlockNumber = Number.parseInt(newBlockNumberHex, 16) + const newBlock = await this.getBlockByNumber(newBlockNumber, true) + if (!newBlock) return + const block = this.mapTransactionsToPrices(newBlock) const state = this.store.getState() @@ -108,9 +118,9 @@ class RecentBlocksController { } /** - * On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks + * On this.blockTracker's first 'latest' event after this RecentBlocksController's instantiation, the store.recentBlocks * array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first - * 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying + * 'latest' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying * the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest. * * Each iteration over the block numbers is delayed by 100 milliseconds. @@ -118,18 +128,17 @@ class RecentBlocksController { * @returns {Promise<void>} Promises undefined */ async backfill () { - this.blockTracker.once('block', async (block) => { - const currentBlockNumber = Number.parseInt(block.number, 16) + this.blockTracker.once('latest', async (blockNumberHex) => { + const currentBlockNumber = Number.parseInt(blockNumberHex, 16) const blocksToFetch = Math.min(currentBlockNumber, this.historyLength) const prevBlockNumber = currentBlockNumber - 1 const targetBlockNumbers = Array(blocksToFetch).fill().map((_, index) => prevBlockNumber - index) await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => { try { - const newBlock = await this.getBlockByNumber(targetBlockNumber) + const newBlock = await this.getBlockByNumber(targetBlockNumber, true) + if (!newBlock) return - if (newBlock) { - this.backfillBlock(newBlock) - } + this.backfillBlock(newBlock) } catch (e) { log.error(e) } @@ -138,18 +147,6 @@ class RecentBlocksController { } /** - * A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await - * - * @returns {Promise<void>} Promises undefined - * - */ - async wait () { - return new Promise((resolve) => { - setTimeout(resolve, 100) - }) - } - - /** * Uses EthQuery to get a block that has a given block number. * * @param {number} number The number of the block to get @@ -157,13 +154,8 @@ class RecentBlocksController { * */ async getBlockByNumber (number) { - const bn = new BN(number) - return new Promise((resolve, reject) => { - this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => { - if (err) reject(err) - resolve(block) - }) - }) + const blockNumberHex = '0x' + number.toString(16) + return await pify(this.ethQuery.getBlockByNumber).call(this.ethQuery, blockNumberHex, true) } } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 8e2288aed..5d7d6d6da 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -65,6 +65,7 @@ class TransactionController extends EventEmitter { this.store = this.txStateManager.store this.nonceTracker = new NonceTracker({ provider: this.provider, + blockTracker: this.blockTracker, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) @@ -78,13 +79,17 @@ class TransactionController extends EventEmitter { }) this.txStateManager.store.subscribe(() => this.emit('update:badge')) - this._setupListners() + this._setupListeners() // memstore is computed from a few different stores this._updateMemstore() this.txStateManager.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) + + // request state update to finalize initialization + this._updatePendingTxsAfterFirstBlock() } + /** @returns {number} the chainId*/ getChainId () { const networkState = this.networkStore.getState() @@ -311,6 +316,11 @@ class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } + confirmTransaction (txId) { + this.txStateManager.setTxStatusConfirmed(txId) + this._markNonceDuplicatesDropped(txId) + } + /** Convenience method for the ui thats sets the transaction to rejected @param txId {number} - the tx's Id @@ -354,6 +364,14 @@ class TransactionController extends EventEmitter { this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) } + // called once on startup + async _updatePendingTxsAfterFirstBlock () { + // wait for first block so we know we're ready + await this.blockTracker.getLatestBlock() + // get status update for all pending transactions (for the current network) + await this.pendingTxTracker.updatePendingTxs() + } + /** If transaction controller was rebooted with transactions that are uncompleted in steps of the transaction signing or user confirmation process it will either @@ -386,14 +404,14 @@ class TransactionController extends EventEmitter { is called in constructor applies the listeners for pendingTxTracker txStateManager and blockTracker */ - _setupListners () { + _setupListeners () { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) + this._setupBlockTrackerListener() this.pendingTxTracker.on('tx:warning', (txMeta) => { this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') }) - this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId)) - this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId)) this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { if (!txMeta.firstRetryBlockNumber) { txMeta.firstRetryBlockNumber = latestBlockNumber @@ -405,13 +423,6 @@ class TransactionController extends EventEmitter { txMeta.retryCount++ this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') }) - - this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) - // this is a little messy but until ethstore has been either - // removed or redone this is to guard against the race condition - this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) - this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) - } /** @@ -435,6 +446,40 @@ class TransactionController extends EventEmitter { }) } + _setupBlockTrackerListener () { + let listenersAreActive = false + const latestBlockHandler = this._onLatestBlock.bind(this) + const blockTracker = this.blockTracker + const txStateManager = this.txStateManager + + txStateManager.on('tx:status-update', updateSubscription) + updateSubscription() + + function updateSubscription () { + const pendingTxs = txStateManager.getPendingTransactions() + if (!listenersAreActive && pendingTxs.length > 0) { + blockTracker.on('latest', latestBlockHandler) + listenersAreActive = true + } else if (listenersAreActive && !pendingTxs.length) { + blockTracker.removeListener('latest', latestBlockHandler) + listenersAreActive = false + } + } + } + + async _onLatestBlock (blockNumber) { + try { + await this.pendingTxTracker.updatePendingTxs() + } catch (err) { + log.error(err) + } + try { + await this.pendingTxTracker.resubmitPendingTxs(blockNumber) + } catch (err) { + log.error(err) + } + } + /** Updates the memStore in transaction controller */ diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index 06f336eaa..421036368 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -12,8 +12,9 @@ const Mutex = require('await-semaphore').Mutex */ class NonceTracker { - constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { + constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) { this.provider = provider + this.blockTracker = blockTracker this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.getConfirmedTransactions = getConfirmedTransactions @@ -34,7 +35,7 @@ class NonceTracker { * @typedef NonceDetails * @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction. * @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method. - * @property {number} highetSuggested - The maximum between the other two, the number returned. + * @property {number} highestSuggested - The maximum between the other two, the number returned. */ /** @@ -80,15 +81,6 @@ class NonceTracker { } } - async _getCurrentBlock () { - const blockTracker = this._getBlockTracker() - const currentBlock = blockTracker.getCurrentBlock() - if (currentBlock) return currentBlock - return await new Promise((reject, resolve) => { - blockTracker.once('latest', resolve) - }) - } - async _globalMutexFree () { const globalMutex = this._lookupMutex('global') const releaseLock = await globalMutex.acquire() @@ -114,9 +106,8 @@ class NonceTracker { // calculate next nonce // we need to make sure our base count // and pending count are from the same block - const currentBlock = await this._getCurrentBlock() - const blockNumber = currentBlock.blockNumber - const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest') + const blockNumber = await this.blockTracker.getLatestBlock() + const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber) const baseCount = baseCountBN.toNumber() assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const nonceDetails = { blockNumber, baseCount } @@ -165,15 +156,6 @@ class NonceTracker { return { name: 'local', nonce: highest, details: { startPoint, highest } } } - // this is a hotfix for the fact that the blockTracker will - // change when the network changes - - /** - @returns {Object} the current blockTracker - */ - _getBlockTracker () { - return this.provider._blockTracker - } } module.exports = NonceTracker diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 4e41cdaf8..70cac096b 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,6 +1,7 @@ const EventEmitter = require('events') const log = require('loglevel') const EthQuery = require('ethjs-query') + /** Event emitter utility class for tracking the transactions as they<br> @@ -23,55 +24,26 @@ class PendingTransactionTracker extends EventEmitter { super() this.query = new EthQuery(config.provider) this.nonceTracker = config.nonceTracker - // default is one day this.getPendingTransactions = config.getPendingTransactions this.getCompletedTransactions = config.getCompletedTransactions this.publishTransaction = config.publishTransaction - this._checkPendingTxs() + this.confirmTransaction = config.confirmTransaction } /** - checks if a signed tx is in a block and - if it is included emits tx status as 'confirmed' - @param block {object}, a full block - @emits tx:confirmed - @emits tx:failed - */ - checkForTxInBlock (block) { - const signedTxList = this.getPendingTransactions() - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - const txHash = txMeta.hash - const txId = txMeta.id - - if (!txHash) { - const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') - noTxHashErr.name = 'NoTxHashError' - this.emit('tx:failed', txId, noTxHashErr) - return - } - - - block.transactions.forEach((tx) => { - if (tx.hash === txHash) this.emit('tx:confirmed', txId) - }) - }) - } - - /** - asks the network for the transaction to see if a block number is included on it - if we have skipped/missed blocks - @param object - oldBlock newBlock + checks the network for signed txs and releases the nonce global lock if it is */ - queryPendingTxs ({ oldBlock, newBlock }) { - // check pending transactions on start - if (!oldBlock) { - this._checkPendingTxs() - return + async updatePendingTxs () { + // in order to keep the nonceTracker accurate we block it while updating pending transactions + const nonceGlobalLock = await this.nonceTracker.getGlobalLock() + try { + const pendingTxs = this.getPendingTransactions() + await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta))) + } catch (err) { + log.error('PendingTransactionTracker - Error updating pending transactions') + log.error(err) } - // if we synced by more than one block, check for missed pending transactions - const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16) - if (diff > 1) this._checkPendingTxs() + nonceGlobalLock.releaseLock() } /** @@ -79,11 +51,11 @@ class PendingTransactionTracker extends EventEmitter { @param block {object} - a block object @emits tx:warning */ - resubmitPendingTxs (block) { + resubmitPendingTxs (blockNumber) { const pending = this.getPendingTransactions() // only try resubmitting if their are transactions to resubmit if (!pending.length) return - pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => { + pending.forEach((txMeta) => this._resubmitTx(txMeta, blockNumber).catch((err) => { /* Dont marked as failed if the error is a "known" transaction warning "there is already a transaction with the same sender-nonce @@ -145,6 +117,7 @@ class PendingTransactionTracker extends EventEmitter { this.emit('tx:retry', txMeta) return txHash } + /** Ask the network for the transaction to see if it has been include in a block @param txMeta {Object} - the txMeta object @@ -174,9 +147,8 @@ class PendingTransactionTracker extends EventEmitter { } // get latest transaction status - let txParams try { - txParams = await this.query.getTransactionByHash(txHash) + const txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { this.emit('tx:confirmed', txId) @@ -191,26 +163,12 @@ class PendingTransactionTracker extends EventEmitter { } /** - checks the network for signed txs and releases the nonce global lock if it is - */ - async _checkPendingTxs () { - const signedTxList = this.getPendingTransactions() - // in order to keep the nonceTracker accurate we block it while updating pending transactions - const { releaseLock } = await this.nonceTracker.getGlobalLock() - try { - await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) - } catch (err) { - log.error('PendingTransactionWatcher - Error updating pending transactions') - log.error(err) - } - releaseLock() - } - - /** checks to see if a confirmed txMeta has the same nonce @param txMeta {Object} - txMeta object @returns {boolean} */ + + async _checkIfNonceIsTaken (txMeta) { const address = txMeta.txParams.from const completed = this.getCompletedTransactions(address) diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 5cd0f5407..3dd45507f 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -25,7 +25,7 @@ class TxGasUtil { @returns {object} the txMeta object with the gas written to the txParams */ async analyzeGasUsage (txMeta) { - const block = await this.query.getBlockByNumber('latest', true) + const block = await this.query.getBlockByNumber('latest', false) let estimatedGasHex try { estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 0f7b3d865..b7e2c7cbe 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -7,14 +7,13 @@ * on each new block. */ -const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') -const EventEmitter = require('events').EventEmitter -function noop () {} +const log = require('loglevel') +const pify = require('pify') -class AccountTracker extends EventEmitter { +class AccountTracker { /** * This module is responsible for tracking any number of accounts and caching their current balances & transaction @@ -35,8 +34,6 @@ class AccountTracker extends EventEmitter { * */ constructor (opts = {}) { - super() - const initState = { accounts: {}, currentBlockGasLimit: '', @@ -44,12 +41,12 @@ class AccountTracker extends EventEmitter { this.store = new ObservableStore(initState) this._provider = opts.provider - this._query = new EthQuery(this._provider) + this._query = pify(new EthQuery(this._provider)) this._blockTracker = opts.blockTracker // subscribe to latest block - this._blockTracker.on('block', this._updateForBlock.bind(this)) + this._blockTracker.on('latest', this._updateForBlock.bind(this)) // blockTracker.currentBlock may be null - this._currentBlockNumber = this._blockTracker.currentBlock + this._currentBlockNumber = this._blockTracker.getCurrentBlock() } /** @@ -67,49 +64,57 @@ class AccountTracker extends EventEmitter { const accounts = this.store.getState().accounts const locals = Object.keys(accounts) - const toAdd = [] + const accountsToAdd = [] addresses.forEach((upstream) => { if (!locals.includes(upstream)) { - toAdd.push(upstream) + accountsToAdd.push(upstream) } }) - const toRemove = [] + const accountsToRemove = [] locals.forEach((local) => { if (!addresses.includes(local)) { - toRemove.push(local) + accountsToRemove.push(local) } }) - toAdd.forEach(upstream => this.addAccount(upstream)) - toRemove.forEach(local => this.removeAccount(local)) - this._updateAccounts() + this.addAccounts(accountsToAdd) + this.removeAccount(accountsToRemove) } /** - * Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be + * Adds new addresses to track the balances of * given a balance as long this._currentBlockNumber is defined. * - * @param {string} address A hex address of a new account to store in this AccountTracker's accounts object + * @param {array} addresses An array of hex addresses of new accounts to track * */ - addAccount (address) { + addAccounts (addresses) { const accounts = this.store.getState().accounts - accounts[address] = {} + // add initial state for addresses + addresses.forEach(address => { + accounts[address] = {} + }) + // save accounts state this.store.updateState({ accounts }) + // fetch balances for the accounts if there is block number ready if (!this._currentBlockNumber) return - this._updateAccount(address) + addresses.forEach(address => this._updateAccount(address)) } /** - * Removes an account from this AccountTracker's accounts object + * Removes accounts from being tracked * - * @param {string} address A hex address of a the account to remove + * @param {array} an array of hex addresses to stop tracking * */ - removeAccount (address) { + removeAccount (addresses) { const accounts = this.store.getState().accounts - delete accounts[address] + // remove each state object + addresses.forEach(address => { + delete accounts[address] + }) + // save accounts state this.store.updateState({ accounts }) } @@ -118,71 +123,56 @@ class AccountTracker extends EventEmitter { * via EthQuery * * @private - * @param {object} block Data about the block that contains the data to update to. + * @param {number} blockNumber the block number to update to. * @fires 'block' The updated state, if all account updates are successful * */ - _updateForBlock (block) { - this._currentBlockNumber = block.number - const currentBlockGasLimit = block.gasLimit + async _updateForBlock (blockNumber) { + this._currentBlockNumber = blockNumber + // block gasLimit polling shouldn't be in account-tracker shouldn't be here... + const currentBlock = await this._query.getBlockByNumber(blockNumber, false) + if (!currentBlock) return + const currentBlockGasLimit = currentBlock.gasLimit this.store.updateState({ currentBlockGasLimit }) - async.parallel([ - this._updateAccounts.bind(this), - ], (err) => { - if (err) return console.error(err) - this.emit('block', this.store.getState()) - }) + try { + await this._updateAccounts() + } catch (err) { + log.error(err) + } } /** * Calls this._updateAccount for each account in this.store * - * @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated + * @returns {Promise} after all account balances updated * */ - _updateAccounts (cb = noop) { + async _updateAccounts () { const accounts = this.store.getState().accounts const addresses = Object.keys(accounts) - async.each(addresses, this._updateAccount.bind(this), cb) + await Promise.all(addresses.map(this._updateAccount.bind(this))) } /** - * Updates the current balance of an account. Gets an updated balance via this._getAccount. + * Updates the current balance of an account. * * @private * @param {string} address A hex address of a the account to be updated - * @param {Function} cb A callback to call once the account at address is successfully update + * @returns {Promise} after the account balance is updated * */ - _updateAccount (address, cb = noop) { - this._getAccount(address, (err, result) => { - if (err) return cb(err) - result.address = address - const accounts = this.store.getState().accounts - // only populate if the entry is still present - if (accounts[address]) { - accounts[address] = result - this.store.updateState({ accounts }) - } - cb(null, result) - }) - } - - /** - * Gets the current balance of an account via EthQuery. - * - * @private - * @param {string} address A hex address of a the account to query - * @param {Function} cb A callback to call once the account at address is successfully update - * - */ - _getAccount (address, cb = noop) { - const query = this._query - async.parallel({ - balance: query.getBalance.bind(query, address), - }, cb) + async _updateAccount (address) { + // query balance + const balance = await this._query.getBalance(address) + const result = { address, balance } + // update accounts state + const { accounts } = this.store.getState() + // only populate if the entry is still present + if (!accounts[address]) return + accounts[address] = result + this.store.updateState({ accounts }) } } diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js index 0a3afca47..c6d57a1bc 100644 --- a/app/scripts/lib/enums.js +++ b/app/scripts/lib/enums.js @@ -2,8 +2,19 @@ const ENVIRONMENT_TYPE_POPUP = 'popup' const ENVIRONMENT_TYPE_NOTIFICATION = 'notification' const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen' +const PLATFORM_BRAVE = 'Brave' +const PLATFORM_CHROME = 'Chrome' +const PLATFORM_EDGE = 'Edge' +const PLATFORM_FIREFOX = 'Firefox' +const PLATFORM_OPERA = 'Opera' + module.exports = { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, + PLATFORM_BRAVE, + PLATFORM_CHROME, + PLATFORM_EDGE, + PLATFORM_FIREFOX, + PLATFORM_OPERA, } diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js deleted file mode 100644 index f83773ccc..000000000 --- a/app/scripts/lib/events-proxy.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Returns an EventEmitter that proxies events from the given event emitter - * @param {any} eventEmitter - * @param {object} listeners - The listeners to proxy to - * @returns {any} - */ -module.exports = function createEventEmitterProxy (eventEmitter, listeners) { - let target = eventEmitter - const eventHandlers = listeners || {} - const proxy = /** @type {any} */ (new Proxy({}, { - get: (_, name) => { - // intercept listeners - if (name === 'on') return addListener - if (name === 'setTarget') return setTarget - if (name === 'proxyEventHandlers') return eventHandlers - return (/** @type {any} */ (target))[name] - }, - set: (_, name, value) => { - target[name] = value - return true - }, - })) - function setTarget (/** @type {EventEmitter} */ eventEmitter) { - target = eventEmitter - // migrate listeners - Object.keys(eventHandlers).forEach((name) => { - /** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler)) - }) - } - /** - * Attaches a function to be called whenever the specified event is emitted - * @param {string} name - * @param {Function} handler - */ - function addListener (name, handler) { - if (!eventHandlers[name]) eventHandlers[name] = [] - eventHandlers[name].push(handler) - target.on(name, handler) - } - if (listeners) proxy.setTarget(eventEmitter) - return proxy -} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 901367f04..47925b94b 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -69,10 +69,39 @@ module.exports = class MessageManager extends EventEmitter { * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} after signature has been + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + const msgId = this.addUnapprovedMessage(msgParams, req) + // await finished + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the + * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object where the origin may be specificied * @returns {number} The id of the newly created message. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { + // add origin from request + if (req) msgParams.origin = req.origin msgParams.data = normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index e96ced1f2..fc2cccdf1 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -73,11 +73,43 @@ module.exports = class PersonalMessageManager extends EventEmitter { * this.memStore. * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} When the message has been signed or rejected + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + if (!msgParams.from) { + reject(new Error('MetaMask Message Signature: from field is required.')) + } + const msgId = this.addUnapprovedMessage(msgParams, req) + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to + * this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin * @returns {number} The id of the newly created PersonalMessage. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) + // add origin from request + if (req) msgParams.origin = req.origin msgParams.data = this.normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -257,4 +289,3 @@ module.exports = class PersonalMessageManager extends EventEmitter { } } - diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js index fd65d94f3..cdf7a7315 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -46,7 +46,7 @@ PortDuplexStream.prototype._onMessage = function (msg) { * @private */ PortDuplexStream.prototype._onDisconnect = function () { - this.destroy() + this.destroy && this.destroy() } /** diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 3651524f1..e6e511640 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -70,11 +70,11 @@ function simplifyErrorMessages (report) { function rewriteErrorMessages (report, rewriteFn) { // rewrite top level message - if (report.message) report.message = rewriteFn(report.message) + if (typeof report.message === 'string') report.message = rewriteFn(report.message) // rewrite each exception message if (report.exception && report.exception.values) { report.exception.values.forEach(item => { - item.value = rewriteFn(item.value) + if (typeof item.value === 'string') item.value = rewriteFn(item.value) }) } } diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index c58921610..e5e1c94b3 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -72,11 +72,40 @@ module.exports = class TypedMessageManager extends EventEmitter { * this.memStore. Before any of this is done, msgParams are validated * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} When the message has been signed or rejected + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + const msgId = this.addUnapprovedMessage(msgParams, req) + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to + * this.memStore. Before any of this is done, msgParams are validated + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin * @returns {number} The id of the newly created TypedMessage. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { this.validateParams(msgParams) + // add origin from request + if (req) msgParams.origin = req.origin log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) // create txData obj with parameters and meta data diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 51e9036cc..ea13b26be 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -5,6 +5,11 @@ const { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, + PLATFORM_FIREFOX, + PLATFORM_OPERA, + PLATFORM_CHROME, + PLATFORM_EDGE, + PLATFORM_BRAVE, } = require('./enums') /** @@ -38,6 +43,29 @@ const getEnvironmentType = (url = window.location.href) => { } /** + * Returns the platform (browser) where the extension is running. + * + * @returns {string} the platform ENUM + * + */ +const getPlatform = _ => { + const ua = navigator.userAgent + if (ua.search('Firefox') !== -1) { + return PLATFORM_FIREFOX + } else { + if (window && window.chrome && window.chrome.ipcRenderer) { + return PLATFORM_BRAVE + } else if (ua.search('Edge') !== -1) { + return PLATFORM_EDGE + } else if (ua.search('OPR') !== -1) { + return PLATFORM_OPERA + } else { + return PLATFORM_CHROME + } + } +} + +/** * Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee * * @param {object} txParams Contains data about a transaction @@ -99,7 +127,22 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) { return targetBN.mul(numBN).div(denomBN) } +function applyListeners (listeners, emitter) { + Object.keys(listeners).forEach((key) => { + emitter.on(key, listeners[key]) + }) +} + +function removeListeners (listeners, emitter) { + Object.keys(listeners).forEach((key) => { + emitter.removeListener(key, listeners[key]) + }) +} + module.exports = { + removeListeners, + applyListeners, + getPlatform, getStack, getEnvironmentType, sufficientBalance, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index db323e3fe..2d7d2c671 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -46,9 +46,10 @@ const BN = require('ethereumjs-util').BN const GWEI_BN = new BN('1000000000') const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') -const cleanErrorStack = require('./lib/cleanErrorStack') const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') +const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') +const EthQuery = require('eth-query') module.exports = class MetamaskController extends EventEmitter { @@ -107,8 +108,9 @@ module.exports = class MetamaskController extends EventEmitter { this.blacklistController.scheduleUpdates() // rpc provider - this.provider = this.initializeProvider() - this.blockTracker = this.provider._blockTracker + this.initializeProvider() + this.provider = this.networkController.getProviderAndBlockTracker().provider + this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker // token exchange rate tracker this.tokenRatesController = new TokenRatesController({ @@ -127,7 +129,7 @@ module.exports = class MetamaskController extends EventEmitter { }) // key mgmt - const additionalKeyrings = [TrezorKeyring] + const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, initState: initState.KeyringController, @@ -252,28 +254,22 @@ module.exports = class MetamaskController extends EventEmitter { static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, - eth_sendTransaction: (payload, next, end) => { - const origin = payload.origin - const txParams = payload.params[0] - nodeify(this.txController.newUnapprovedTransaction, this.txController)(txParams, { origin }, end) - }, }, // account mgmt - getAccounts: (cb) => { + getAccounts: async () => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const result = [] const selectedAddress = this.preferencesController.getSelectedAddress() - // only show address if account is unlocked if (isUnlocked && selectedAddress) { - result.push(selectedAddress) + return [selectedAddress] + } else { + return [] } - cb(null, result) }, // tx signing - // old style msg signing - processMessage: this.newUnsignedMessage.bind(this), - // personal_sign msg signing + processTransaction: this.newUnapprovedTransaction.bind(this), + // msg signing + processEthSignMessage: this.newUnsignedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processTypedMessage: this.newUnsignedTypedMessage.bind(this), } @@ -377,9 +373,7 @@ module.exports = class MetamaskController extends EventEmitter { connectHardware: nodeify(this.connectHardware, this), forgetDevice: nodeify(this.forgetDevice, this), checkHardwareStatus: nodeify(this.checkHardwareStatus, this), - - // TREZOR - unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this), // vault management submitPassword: nodeify(this.submitPassword, this), @@ -481,12 +475,32 @@ module.exports = class MetamaskController extends EventEmitter { async createNewVaultAndRestore (password, seed) { const releaseLock = await this.createVaultMutex.acquire() try { + let accounts, lastBalance + + const keyringController = this.keyringController + // clear known identities this.preferencesController.setAddresses([]) // create new vault - const vault = await this.keyringController.createNewVaultAndRestore(password, seed) + const vault = await keyringController.createNewVaultAndRestore(password, seed) + + const ethQuery = new EthQuery(this.provider) + accounts = await keyringController.getAccounts() + lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery) + + const primaryKeyring = keyringController.getKeyringsByType('HD Key Tree')[0] + if (!primaryKeyring) { + throw new Error('MetamaskController - No HD Key Tree found') + } + + // seek out the first zero balance + while (lastBalance !== '0x0') { + await keyringController.addNewAccount(primaryKeyring) + accounts = await keyringController.getAccounts() + lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery) + } + // set new identities - const accounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(accounts) this.selectFirstIdentity() releaseLock() @@ -497,6 +511,30 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * Get an account balance from the AccountTracker or request it directly from the network. + * @param {string} address - The account address + * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network + */ + getBalance (address, ethQuery) { + return new Promise((resolve, reject) => { + const cached = this.accountTracker.store.getState().accounts[address] + + if (cached && cached.balance) { + resolve(cached.balance) + } else { + ethQuery.getBalance(address, (error, balance) => { + if (error) { + reject(error) + log.error(error) + } else { + resolve(balance || '0x0') + } + }) + } + }) + } + /* * Submits the user's password and attempts to unlock the vault. * Also synchronizes the preferencesController, to ensure its schema @@ -540,45 +578,57 @@ module.exports = class MetamaskController extends EventEmitter { // Hardware // + async getKeyringForDevice (deviceName, hdPath = null) { + let keyringName = null + switch (deviceName) { + case 'trezor': + keyringName = TrezorKeyring.type + break + case 'ledger': + keyringName = LedgerBridgeKeyring.type + break + default: + throw new Error('MetamaskController:getKeyringForDevice - Unknown device') + } + let keyring = await this.keyringController.getKeyringsByType(keyringName)[0] + if (!keyring) { + keyring = await this.keyringController.addNewKeyring(keyringName) + } + if (hdPath && keyring.setHdPath) { + keyring.setHdPath(hdPath) + } + + keyring.network = this.networkController.getProviderConfig().type + + return keyring + + } + /** * Fetch account list from a trezor device. * * @returns [] accounts */ - async connectHardware (deviceName, page) { - - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const oldAccounts = await keyringController.getAccounts() - let keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - keyring = await this.keyringController.addNewKeyring('Trezor Hardware') - } - let accounts = [] - - switch (page) { - case -1: - accounts = await keyring.getPreviousPage() - break - case 1: - accounts = await keyring.getNextPage() - break - default: - accounts = await keyring.getFirstPage() - } - - // Merge with existing accounts - // and make sure addresses are not repeated - const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] - this.accountTracker.syncWithAddresses(accountsToTrack) - return accounts - - default: - throw new Error('MetamaskController:connectHardware - Unknown device') + async connectHardware (deviceName, page, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + let accounts = [] + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() } + + // Merge with existing accounts + // and make sure addresses are not repeated + const oldAccounts = await this.keyringController.getAccounts() + const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] + this.accountTracker.syncWithAddresses(accountsToTrack) + return accounts } /** @@ -586,21 +636,9 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {Promise<boolean>} */ - async checkHardwareStatus (deviceName) { - - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - return false - } - return keyring.isUnlocked() - default: - throw new Error('MetamaskController:checkHardwareStatus - Unknown device') - } + async checkHardwareStatus (deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + return keyring.isUnlocked() } /** @@ -610,20 +648,9 @@ module.exports = class MetamaskController extends EventEmitter { */ async forgetDevice (deviceName) { - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found') - } - keyring.forgetDevice() - return true - default: - throw new Error('MetamaskController:forgetDevice - Unknown device') - } + const keyring = await this.getKeyringForDevice(deviceName) + keyring.forgetDevice() + return true } /** @@ -631,23 +658,17 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {} keyState */ - async unlockTrezorAccount (index) { - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController - No Trezor Hardware Keyring found') - } + async unlockHardwareWalletAccount (index, deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) keyring.setAccountToUnlock(index) - const oldAccounts = await keyringController.getAccounts() - const keyState = await keyringController.addNewAccount(keyring) - const newAccounts = await keyringController.getAccounts() + const oldAccounts = await this.keyringController.getAccounts() + const keyState = await this.keyringController.addNewAccount(keyring) + const newAccounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(newAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { - this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) + this.preferencesController.setAccountLabel(address, `${deviceName.toUpperCase()} ${parseInt(index, 10) + 1}`) this.preferencesController.setSelectedAddress(address) } }) @@ -809,6 +830,18 @@ module.exports = class MetamaskController extends EventEmitter { // --------------------------------------------------------------------------- // Identity Management (signature operations) + /** + * Called when a Dapp suggests a new tx to be signed. + * this wrapper needs to exist so we can provide a reference to + * "newUnapprovedTransaction" before "txController" is instantiated + * + * @param {Object} msgParams - The params passed to eth_sign. + * @param {Object} req - (optional) the original request, containing the origin + */ + async newUnapprovedTransaction (txParams, req) { + return await this.txController.newUnapprovedTransaction(txParams, req) + } + // eth_sign methods: /** @@ -820,20 +853,11 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_sign. * @param {Function} cb = The callback function called with the signature. */ - newUnsignedMessage (msgParams, cb) { - const msgId = this.messageManager.addUnapprovedMessage(msgParams) + newUnsignedMessage (msgParams, req) { + const promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req) this.sendUpdate() this.opts.showUnconfirmedMessage() - this.messageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) - default: - return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) - } - }) + return promise } /** @@ -887,24 +911,11 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - The callback function called with the signature. * Passed back to the requesting Dapp. */ - newUnsignedPersonalMessage (msgParams, cb) { - if (!msgParams.from) { - return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.'))) - } - - const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + async newUnsignedPersonalMessage (msgParams, req) { + const promise = this.personalMessageManager.addUnapprovedMessageAsync(msgParams, req) this.sendUpdate() this.opts.showUnconfirmedMessage() - this.personalMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) - default: - return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) - } - }) + return promise } /** @@ -953,26 +964,11 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Function} cb - The callback function, called with the signature. */ - newUnsignedTypedMessage (msgParams, cb) { - let msgId - try { - msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - } catch (e) { - return cb(e) - } - - this.typedMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) - default: - return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) - } - }) + newUnsignedTypedMessage (msgParams, req) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + return promise } /** @@ -1237,7 +1233,7 @@ module.exports = class MetamaskController extends EventEmitter { // create filter polyfill middleware const filterMiddleware = createFilterMiddleware({ provider: this.provider, - blockTracker: this.provider._blockTracker, + blockTracker: this.blockTracker, }) engine.push(createOriginMiddleware({ origin })) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 0803164e8..71b162dd0 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -24,8 +24,13 @@ class ExtensionPlatform { return extension.runtime.getManifest().version } - openExtensionInBrowser (route = null) { + openExtensionInBrowser (route = null, queryString = null) { let extensionURL = extension.runtime.getURL('home.html') + + if (queryString) { + extensionURL += `?${queryString}` + } + if (route) { extensionURL += `#${route}` } |