upstream from mm 4.9.3
This commit is contained in:
commit
48f457f3ae
|
@ -27,8 +27,9 @@ If you're a web dapp developer, we've got two types of guides for you:
|
|||
## Building locally
|
||||
|
||||
- Install [Node.js](https://nodejs.org/en/) version 8.11.3 and npm version 6.1.0
|
||||
- Install dependencies:
|
||||
- If you are using nvm (recommended) running `nvm use` will automatically choose the right node version for you.
|
||||
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
|
||||
- Select npm 6.1.0: ```npm install -g npm@6.1.0```
|
||||
- Install dependencies: ```npm install```
|
||||
- Install gulp globally with `npm install -g gulp-cli`.
|
||||
- Build the project to the `./dist/` folder with `gulp build`.
|
||||
- Optionally, to rebuild on file changes, run `gulp dev`.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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?"
|
||||
|
@ -510,6 +510,9 @@
|
|||
"invalidRPC": {
|
||||
"message": "Invalid RPC URI"
|
||||
},
|
||||
"invalidSeedPhrase": {
|
||||
"message": "Invalid seed phrase"
|
||||
},
|
||||
"jsonFail": {
|
||||
"message": "Something went wrong. Please make sure your JSON file is properly formatted."
|
||||
},
|
||||
|
@ -532,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"
|
||||
|
@ -653,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"
|
||||
},
|
||||
|
@ -902,7 +914,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"
|
||||
|
@ -914,7 +926,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"
|
||||
|
@ -943,6 +961,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"
|
||||
},
|
||||
|
@ -1062,6 +1086,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 Nifty Wallet accounts.\nSave them somewhere safe and secret."
|
||||
},
|
||||
|
@ -1092,6 +1119,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"
|
||||
},
|
||||
|
@ -1138,6 +1174,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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
{ "code": "tml", "name": "Tamil" },
|
||||
{ "code": "tr", "name": "Turkish" },
|
||||
{ "code": "vi", "name": "Vietnamese" },
|
||||
{ "code": "zh_CN", "name": "Mandarin" },
|
||||
{ "code": "zh_TW", "name": "Taiwanese" }
|
||||
{ "code": "zh_CN", "name": "Chinese (Simplified)" },
|
||||
{ "code": "zh_TW", "name": "Chinese (Traditional)" }
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -122,6 +122,9 @@
|
|||
"copy": {
|
||||
"message": "コピー"
|
||||
},
|
||||
"copyContractAddress": {
|
||||
"message": "コントラクトアドレスをコピー"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "クリップボードへコピー"
|
||||
},
|
||||
|
@ -395,6 +398,9 @@
|
|||
"mainnet": {
|
||||
"message": "Ethereumメインネットワーク"
|
||||
},
|
||||
"menu": {
|
||||
"message": "メニュー"
|
||||
},
|
||||
"message": {
|
||||
"message": "メッセージ"
|
||||
},
|
||||
|
@ -464,6 +470,9 @@
|
|||
"oldUIMessage": {
|
||||
"message": "旧UIを表示しています。右上のドロップダウンメニューのオプションより、新UIへ切り替えが可能です。"
|
||||
},
|
||||
"openInTab": {
|
||||
"message": "タブを開く"
|
||||
},
|
||||
"or": {
|
||||
"message": "または",
|
||||
"description": "choice between creating or importing a new account"
|
||||
|
@ -576,6 +585,15 @@
|
|||
"searchResults": {
|
||||
"message": "検索結果"
|
||||
},
|
||||
"newPassword8Chars": {
|
||||
"message": "新しいパスワード (8桁以上)"
|
||||
},
|
||||
"select": {
|
||||
"message": "選択"
|
||||
},
|
||||
"selectCurrency": {
|
||||
"message": "通貨を選択"
|
||||
},
|
||||
"selectService": {
|
||||
"message": "サービスを選択"
|
||||
},
|
||||
|
@ -589,10 +607,14 @@
|
|||
"message": "ETHの送信"
|
||||
},
|
||||
"sendTokens": {
|
||||
"message": "トークンを送る"
|
||||
"message": "トークンを送信"
|
||||
},
|
||||
"onlySendToEtherAddress": {
|
||||
"message": "ETHはイーサリウムアカウントのみに送信できます。"
|
||||
"message": "ETH はイーサリウムアカウントのみに送信できます。"
|
||||
},
|
||||
"onlySendTokensToAccountAddress": {
|
||||
"message": "$1 はイーサリアムアカウントのみに送信できます。",
|
||||
"description": "displays token symbol"
|
||||
},
|
||||
"searchTokens": {
|
||||
"message": "トークンの検索"
|
||||
|
@ -693,10 +715,10 @@
|
|||
"message": "パスワードの入力"
|
||||
},
|
||||
"uiWelcome": {
|
||||
"message": "新UIへようこそ!(ベータ版)"
|
||||
"message": "新UIへようこそ! (ベータ版)"
|
||||
},
|
||||
"uiWelcomeMessage": {
|
||||
"message": "現在Metamaskの新しいUIをお使いになっています。トークン送信など、新たな機能を試してみましょう!何か問題があればご報告ください。"
|
||||
"message": "現在、MetaMask の新しいUIをお使いになっています。トークン送信など、新たな機能を試してみましょう! 何か問題があればご報告ください。"
|
||||
},
|
||||
"unavailable": {
|
||||
"message": "有効ではありません。"
|
||||
|
@ -723,6 +745,9 @@
|
|||
"viewAccount": {
|
||||
"message": "アカウントを見る"
|
||||
},
|
||||
"viewOnEtherscan": {
|
||||
"message": "Etherscan で見る"
|
||||
},
|
||||
"warning": {
|
||||
"message": "警告"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
"message": "Купить на Coinbase"
|
||||
},
|
||||
"buyCoinbaseExplainer": {
|
||||
"message": "Биржа Coinbase – это наиболее популярный способ купить или продать bitcoin, ethereum и litecoin."
|
||||
"message": "Биржа Coinbase – это наиболее популярный способ купить или продать Bitcoin, Ethereum и Litecoin."
|
||||
},
|
||||
"ok": {
|
||||
"message": "ОК"
|
||||
|
|
|
@ -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"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
|
@ -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>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -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>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -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>
|
After Width: | Height: | Size: 1020 B |
|
@ -66,7 +66,8 @@
|
|||
"notifications"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"inpage.js"
|
||||
"inpage.js",
|
||||
"phishing.html"
|
||||
],
|
||||
"externally_connectable": {
|
||||
"matches": [
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE HTML>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Dangerous Website Warning</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #c50000;
|
||||
padding: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', 'UA-37075177-6', 'auto');
|
||||
ga('send', 'pageview');
|
||||
//Send referral data to EAL
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', 'UA-68598031-1', 'auto' {'allowLinker':true});
|
||||
ga('send', 'pageview');
|
||||
ga('require', 'linker');
|
||||
ga('linker:autoLink', ['harrydenley.com', 'metamask.io'], false, true);
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="centered">
|
||||
|
||||
<img src="/images/ethereum-metamask-chrome.png" style="width:100%">
|
||||
<h3>ATTENTION</h3>
|
||||
<p>MetaMask believes this domain could currently compromise your security and has prevented you from interacting with it.</p>
|
||||
<p>This is because the site tested positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>. This includes outright malicious websites and legitimate websites that have been compromised by a malicious actor.</p>
|
||||
<p>You can turn MetaMask off to interact with this site, but it is advised not to.</p>
|
||||
<p>If you think this domain is incorrectly flagged or if a blocked legitimate website has resolved its security issues, <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>.</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -44,8 +44,8 @@ const notificationManager = new NotificationManager()
|
|||
global.METAMASK_NOTIFIER = notificationManager
|
||||
|
||||
// setup sentry error reporting
|
||||
const releaseVersion = platform.getVersion()
|
||||
const raven = setupRaven({ releaseVersion })
|
||||
const release = platform.getVersion()
|
||||
const raven = setupRaven({ release })
|
||||
|
||||
// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||
// Internet Explorer 6-11
|
||||
|
|
|
@ -197,6 +197,7 @@ function blacklistedDomainCheck () {
|
|||
* Redirects the current page to a phishing information page
|
||||
*/
|
||||
function redirectToPhishingWarning () {
|
||||
console.log('Nifty Wallet - redirecting to phishing warning')
|
||||
window.location.href = 'https://metamask.io/phishing.html'
|
||||
console.log('Nifty Wallet - routing to Phishing Warning component')
|
||||
const extensionURL = extension.runtime.getURL('phishing.html')
|
||||
window.location.href = extensionURL
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ class DetectTokensController {
|
|||
set preferences (preferences) {
|
||||
if (!preferences) { return }
|
||||
this._preferences = preferences
|
||||
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
|
||||
preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
|
||||
preferences.store.subscribe(({ selectedAddress }) => {
|
||||
if (this.selectedAddress !== selectedAddress) {
|
||||
this.selectedAddress = selectedAddress
|
||||
|
|
|
@ -13,6 +13,7 @@ class PreferencesController {
|
|||
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
|
||||
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
|
||||
* @property {array} store.tokens The tokens the user wants display in their token lists
|
||||
* @property {object} store.accountTokens The tokens stored per account and then per network type
|
||||
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
|
||||
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
|
||||
* user wishes to see that feature
|
||||
|
@ -24,6 +25,7 @@ class PreferencesController {
|
|||
const initState = extend({
|
||||
frequentRpcList: [],
|
||||
currentAccountTab: 'history',
|
||||
accountTokens: {},
|
||||
tokens: [],
|
||||
useBlockie: false,
|
||||
featureFlags: {},
|
||||
|
@ -33,8 +35,9 @@ class PreferencesController {
|
|||
}, opts.initState)
|
||||
|
||||
this.diagnostics = opts.diagnostics
|
||||
|
||||
this.network = opts.network
|
||||
this.store = new ObservableStore(initState)
|
||||
this._subscribeProviderType()
|
||||
}
|
||||
// PUBLIC METHODS
|
||||
|
||||
|
@ -77,12 +80,19 @@ class PreferencesController {
|
|||
*/
|
||||
setAddresses (addresses) {
|
||||
const oldIdentities = this.store.getState().identities
|
||||
const oldAccountTokens = this.store.getState().accountTokens
|
||||
|
||||
const identities = addresses.reduce((ids, address, index) => {
|
||||
const oldId = oldIdentities[address] || {}
|
||||
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
|
||||
return ids
|
||||
}, {})
|
||||
this.store.updateState({ identities })
|
||||
const accountTokens = addresses.reduce((tokens, address) => {
|
||||
const oldTokens = oldAccountTokens[address] || {}
|
||||
tokens[address] = oldTokens
|
||||
return tokens
|
||||
}, {})
|
||||
this.store.updateState({ identities, accountTokens })
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,11 +103,13 @@ class PreferencesController {
|
|||
*/
|
||||
removeAddress (address) {
|
||||
const identities = this.store.getState().identities
|
||||
const accountTokens = this.store.getState().accountTokens
|
||||
if (!identities[address]) {
|
||||
throw new Error(`${address} can't be deleted cause it was not found`)
|
||||
}
|
||||
delete identities[address]
|
||||
this.store.updateState({ identities })
|
||||
delete accountTokens[address]
|
||||
this.store.updateState({ identities, accountTokens })
|
||||
|
||||
// If the selected account is no longer valid,
|
||||
// select an arbitrary other account:
|
||||
|
@ -117,14 +129,17 @@ class PreferencesController {
|
|||
*/
|
||||
addAddresses (addresses) {
|
||||
const identities = this.store.getState().identities
|
||||
const accountTokens = this.store.getState().accountTokens
|
||||
addresses.forEach((address) => {
|
||||
// skip if already exists
|
||||
if (identities[address]) return
|
||||
// add missing identity
|
||||
const identityCount = Object.keys(identities).length
|
||||
|
||||
accountTokens[address] = {}
|
||||
identities[address] = { name: `Account ${identityCount + 1}`, address }
|
||||
})
|
||||
this.store.updateState({ identities })
|
||||
this.store.updateState({ identities, accountTokens })
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -175,15 +190,15 @@ class PreferencesController {
|
|||
* Setter for the `selectedAddress` property
|
||||
*
|
||||
* @param {string} _address A new hex address for an account
|
||||
* @returns {Promise<void>} Promise resolves with undefined
|
||||
* @returns {Promise<void>} Promise resolves with tokens
|
||||
*
|
||||
*/
|
||||
setSelectedAddress (_address) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const address = normalizeAddress(_address)
|
||||
this.store.updateState({ selectedAddress: address })
|
||||
resolve()
|
||||
})
|
||||
const address = normalizeAddress(_address)
|
||||
this._updateTokens(address)
|
||||
this.store.updateState({ selectedAddress: address })
|
||||
const tokens = this.store.getState().tokens
|
||||
return Promise.resolve(tokens)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,9 +247,7 @@ class PreferencesController {
|
|||
} else {
|
||||
tokens.push(newEntry)
|
||||
}
|
||||
|
||||
this.store.updateState({ tokens })
|
||||
|
||||
this._updateAccountTokens(tokens)
|
||||
return Promise.resolve(tokens)
|
||||
}
|
||||
|
||||
|
@ -247,10 +260,8 @@ class PreferencesController {
|
|||
*/
|
||||
removeToken (rawAddress) {
|
||||
const tokens = this.store.getState().tokens
|
||||
|
||||
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
|
||||
|
||||
this.store.updateState({ tokens: updatedTokens })
|
||||
this._updateAccountTokens(updatedTokens)
|
||||
return Promise.resolve(updatedTokens)
|
||||
}
|
||||
|
||||
|
@ -387,6 +398,57 @@ class PreferencesController {
|
|||
//
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
/**
|
||||
* Subscription to network provider type.
|
||||
*
|
||||
*
|
||||
*/
|
||||
_subscribeProviderType () {
|
||||
this.network.providerStore.subscribe(() => {
|
||||
const { tokens } = this._getTokenRelatedStates()
|
||||
this.store.updateState({ tokens })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates `accountTokens` and `tokens` of current account and network according to it.
|
||||
*
|
||||
* @param {array} tokens Array of tokens to be updated.
|
||||
*
|
||||
*/
|
||||
_updateAccountTokens (tokens) {
|
||||
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
|
||||
accountTokens[selectedAddress][providerType] = tokens
|
||||
this.store.updateState({ accountTokens, tokens })
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates `tokens` of current account and network.
|
||||
*
|
||||
* @param {string} selectedAddress Account address to be updated with.
|
||||
*
|
||||
*/
|
||||
_updateTokens (selectedAddress) {
|
||||
const { tokens } = this._getTokenRelatedStates(selectedAddress)
|
||||
this.store.updateState({ tokens })
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for `tokens` and `accountTokens` related states.
|
||||
*
|
||||
* @param {string} selectedAddress A new hex address for an account
|
||||
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
|
||||
*
|
||||
*/
|
||||
_getTokenRelatedStates (selectedAddress) {
|
||||
const accountTokens = this.store.getState().accountTokens
|
||||
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
|
||||
const providerType = this.network.providerStore.getState().type
|
||||
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
|
||||
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
|
||||
const tokens = accountTokens[selectedAddress][providerType]
|
||||
return { tokens, accountTokens, providerType, selectedAddress }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreferencesController
|
||||
|
|
|
@ -30,14 +30,10 @@ class TxGasUtil {
|
|||
try {
|
||||
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
|
||||
} catch (err) {
|
||||
const simulationFailed = (
|
||||
err.message.includes('Transaction execution error.') ||
|
||||
err.message.includes('gas required exceeds allowance or always failing transaction')
|
||||
)
|
||||
if (simulationFailed) {
|
||||
txMeta.simulationFails = true
|
||||
return txMeta
|
||||
txMeta.simulationFails = {
|
||||
reason: err.message,
|
||||
}
|
||||
return txMeta
|
||||
}
|
||||
this.setTxGas(txMeta, block.gasLimit, estimatedGasHex)
|
||||
return txMeta
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -54,6 +54,11 @@ function MetamaskInpageProvider (connectionStream) {
|
|||
// also remap ids inbound and outbound
|
||||
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
|
||||
const self = this
|
||||
|
||||
if (payload.method === 'eth_signTypedData') {
|
||||
console.warn('MetaMask: This experimental version of eth_signTypedData will be deprecated in the next release in favor of the standard as defined in EIP-712. See https://git.io/fNzPl for more information on the new standard.')
|
||||
}
|
||||
|
||||
self.rpcEngine.handle(payload, cb)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ module.exports = function (provider) {
|
|||
function ipfsContent (details) {
|
||||
const name = details.url.substring(7, details.url.length - 1)
|
||||
let clearTime = null
|
||||
extension.tabs.getSelected(null, tab => {
|
||||
extension.tabs.query({active: true}, tab => {
|
||||
extension.tabs.update(tab.id, { url: 'loading.html' })
|
||||
|
||||
clearTime = setTimeout(() => {
|
||||
|
@ -38,7 +38,7 @@ module.exports = function (provider) {
|
|||
|
||||
return {
|
||||
remove () {
|
||||
extension.webRequest.onBeforeRequest.removeListener(ipfsContent)
|
||||
extension.webRequest.onErrorOccurred.removeListener(ipfsContent)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = setupRaven
|
|||
|
||||
// Setup raven / sentry remote error reporting
|
||||
function setupRaven (opts) {
|
||||
const { releaseVersion } = opts
|
||||
const { release } = opts
|
||||
let ravenTarget
|
||||
// detect brave
|
||||
const isBrave = Boolean(window.chrome.ipcRenderer)
|
||||
|
@ -22,7 +22,7 @@ function setupRaven (opts) {
|
|||
}
|
||||
|
||||
const client = Raven.config(ravenTarget, {
|
||||
releaseVersion,
|
||||
release,
|
||||
transport: function (opts) {
|
||||
opts.data.extra.isBrave = isBrave
|
||||
const report = opts.data
|
||||
|
|
|
@ -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')
|
||||
|
||||
/**
|
||||
|
@ -37,6 +42,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
|
||||
*
|
||||
|
@ -100,6 +128,7 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
getPlatform,
|
||||
getStack,
|
||||
getEnvironmentType,
|
||||
sufficientBalance,
|
||||
|
|
|
@ -49,6 +49,7 @@ 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')
|
||||
|
||||
module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
|
@ -87,6 +88,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
this.preferencesController = new PreferencesController({
|
||||
initState: initState.PreferencesController,
|
||||
initLangCode: opts.initLangCode,
|
||||
network: this.networkController,
|
||||
})
|
||||
|
||||
// currency controller
|
||||
|
@ -126,7 +128,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,
|
||||
|
@ -378,9 +380,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),
|
||||
|
@ -542,45 +542,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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -588,21 +600,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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -612,20 +612,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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -633,23 +622,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)
|
||||
}
|
||||
})
|
||||
|
@ -1468,7 +1451,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* A method for activating the retrieval of price data and auto detect tokens,
|
||||
* A method for activating the retrieval of price data,
|
||||
* which should only be fetched when the UI is visible.
|
||||
* @private
|
||||
* @param {boolean} active - True if price data should be getting fetched.
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// next version number
|
||||
const version = 28
|
||||
|
||||
/*
|
||||
|
||||
normalizes txParams on unconfirmed txs
|
||||
|
||||
*/
|
||||
const clone = require('clone')
|
||||
|
||||
module.exports = {
|
||||
version,
|
||||
|
||||
migrate: async function (originalVersionedData) {
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
const state = versionedData.data
|
||||
const newState = transformState(state)
|
||||
versionedData.data = newState
|
||||
return versionedData
|
||||
},
|
||||
}
|
||||
|
||||
function transformState (state) {
|
||||
const newState = state
|
||||
|
||||
if (newState.PreferencesController) {
|
||||
if (newState.PreferencesController.tokens && newState.PreferencesController.identities) {
|
||||
const identities = newState.PreferencesController.identities
|
||||
const tokens = newState.PreferencesController.tokens
|
||||
newState.PreferencesController.accountTokens = {}
|
||||
for (const identity in identities) {
|
||||
newState.PreferencesController.accountTokens[identity] = {'mainnet': tokens}
|
||||
}
|
||||
newState.PreferencesController.tokens = []
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
|
@ -37,4 +37,6 @@ module.exports = [
|
|||
require('./024'),
|
||||
require('./025'),
|
||||
require('./026'),
|
||||
require('./027'),
|
||||
require('./028'),
|
||||
]
|
||||
|
|
|
@ -45,8 +45,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}`
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ class ConfirmSeedScreen extends Component {
|
|||
key={i}
|
||||
className={classnames('backup-phrase__confirm-seed-option', {
|
||||
'backup-phrase__confirm-seed-option--selected': isSelected,
|
||||
'backup-phrase__confirm-seed-option--unselected': !isSelected,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isSelected) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {validateMnemonic} from 'bip39'
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
|
@ -39,8 +40,12 @@ class ImportSeedPhraseScreen extends Component {
|
|||
handleSeedPhraseChange (seedPhrase) {
|
||||
let seedPhraseError = null
|
||||
|
||||
if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
|
||||
seedPhraseError = this.context.t('seedPhraseReq')
|
||||
if (seedPhrase) {
|
||||
if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
|
||||
seedPhraseError = this.context.t('seedPhraseReq')
|
||||
} else if (!validateMnemonic(seedPhrase)) {
|
||||
seedPhraseError = this.context.t('invalidSeedPhrase')
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ seedPhrase, seedPhraseError })
|
||||
|
|
|
@ -340,6 +340,19 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.backup-phrase__tips-text--link {
|
||||
color: #2f9ae0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backup-phrase__tips-text--link:hover {
|
||||
color: #2f9ae0;
|
||||
}
|
||||
|
||||
.backup-phrase__tips-text--strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.backup-phrase__content-wrapper {
|
||||
flex-direction: column;
|
||||
|
|
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
|
|||
import {connect} from 'react-redux'
|
||||
import { withRouter, Switch, Route } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import CreatePasswordScreen from './create-password-screen'
|
||||
import UniqueImageScreen from './unique-image-screen'
|
||||
|
@ -44,28 +43,9 @@ class FirstTimeFlow extends Component {
|
|||
noActiveNotices: false,
|
||||
};
|
||||
|
||||
renderAppBar () {
|
||||
const { welcomeScreenSeen } = this.props
|
||||
|
||||
return (
|
||||
<div className="alpha-warning__container">
|
||||
<h2 className={classnames({
|
||||
'alpha-warning': welcomeScreenSeen,
|
||||
'alpha-warning-welcome-screen': !welcomeScreenSeen,
|
||||
})}
|
||||
>
|
||||
Please be aware that this version is still under development
|
||||
</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isPopup } = this.props
|
||||
|
||||
return (
|
||||
<div className="flex-column flex-grow">
|
||||
{ !isPopup && this.renderAppBar() }
|
||||
<div className="first-time-flow">
|
||||
<Switch>
|
||||
<Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} />
|
||||
|
|
|
@ -5,6 +5,7 @@ import classnames from 'classnames'
|
|||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import Identicon from '../../../../ui/app/components/identicon'
|
||||
import {exportAsFile} from '../../../../ui/app/util'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import LoadingScreen from './loading-screen'
|
||||
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
|
||||
|
@ -65,6 +66,12 @@ class BackupPhraseScreen extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
exportSeedWords = () => {
|
||||
const { seedWords } = this.props
|
||||
|
||||
exportAsFile('MetaMask Secret Backup Phrase', seedWords, 'text/plain')
|
||||
}
|
||||
|
||||
renderSecretWordsContainer () {
|
||||
const { isShowingSecret } = this.state
|
||||
|
||||
|
@ -111,7 +118,7 @@ class BackupPhraseScreen extends Component {
|
|||
<div className="backup-phrase__tips">
|
||||
<div className="backup-phrase__tips-text">Tips:</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
Store this phrase in a password manager like 1password.
|
||||
Store this phrase in a password manager like 1Password.
|
||||
</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations.
|
||||
|
@ -119,6 +126,13 @@ class BackupPhraseScreen extends Component {
|
|||
<div className="backup-phrase__tips-text">
|
||||
Memorize this phrase.
|
||||
</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
<strong>
|
||||
<a className="backup-phrase__tips-text--link backup-phrase__tips-text--strong" onClick={this.exportSeedWords}>
|
||||
Download this Secret Backup Phrase
|
||||
</a>
|
||||
</strong> and keep it stored safely on an external encrypted hard drive or storage medium.
|
||||
</div>
|
||||
</div>
|
||||
<div className="backup-phrase__next-button">
|
||||
<button
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
const PropTypes = require('prop-types')
|
||||
const {PureComponent} = require('react')
|
||||
const h = require('react-hyperscript')
|
||||
const {qrcode: qrCode} = require('qrcode-npm')
|
||||
const {connect} = require('react-redux')
|
||||
const {isHexPrefixed} = require('ethereumjs-util')
|
||||
const CopyButton = require('./components/copyButton')
|
||||
|
||||
class AccountQrScreen extends PureComponent {
|
||||
static defaultProps = {
|
||||
warning: null,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
buyView: PropTypes.any.isRequired,
|
||||
Qr: PropTypes.object.isRequired,
|
||||
warning: PropTypes.node,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {Qr, warning} = this.props
|
||||
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
|
||||
const qrImage = qrCode(4, 'M')
|
||||
|
||||
qrImage.addData(address)
|
||||
qrImage.make()
|
||||
|
||||
return h('.main-container.flex-column', {
|
||||
key: 'qr',
|
||||
style: {
|
||||
justifyContent: 'center',
|
||||
paddingBottom: '45px',
|
||||
paddingLeft: '45px',
|
||||
paddingRight: '45px',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
warning ? warning && h('span.error.flex-center', warning) : null,
|
||||
|
||||
h('#qr-container.flex-column', {
|
||||
style: {
|
||||
marginTop: '15px',
|
||||
},
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: qrImage.createTableTag(4),
|
||||
},
|
||||
}),
|
||||
h('.qr-header', Qr.message),
|
||||
h('.flex-row', [
|
||||
h('h3.ellip-address', {
|
||||
style: {
|
||||
width: '247px',
|
||||
},
|
||||
}, Qr.data),
|
||||
h(CopyButton, {
|
||||
value: Qr.data,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
Qr: state.appState.Qr,
|
||||
buyView: state.appState.buyView,
|
||||
warning: state.appState.warning,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(AccountQrScreen)
|
|
@ -14,6 +14,7 @@ const NewKeyChainScreen = require('./new-keychain')
|
|||
const UnlockScreen = require('./unlock')
|
||||
// accounts
|
||||
const AccountDetailScreen = require('./account-detail')
|
||||
const AccountQrScreen = require('./account-qr')
|
||||
const SendTransactionScreen = require('./send')
|
||||
const SendTokenScreen = require('./send-token')
|
||||
const ConfirmTxScreen = require('./conf-tx')
|
||||
|
@ -27,16 +28,12 @@ const ConfirmAddTokenScreen = require('./components/confirm-add-token')
|
|||
const RemoveTokenScreen = require('./remove-token')
|
||||
const Import = require('./accounts/import')
|
||||
const InfoScreen = require('./info')
|
||||
const AppBar = require('./components/app-bar')
|
||||
const Loading = require('./components/loading')
|
||||
const Dropdown = require('./components/dropdown').Dropdown
|
||||
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
|
||||
const NetworkIndicator = require('./components/network')
|
||||
const BuyView = require('./components/buy-button-subview')
|
||||
const QrView = require('./components/qr-code')
|
||||
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
|
||||
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
|
||||
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
|
||||
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
|
||||
const DeleteRpc = require('./components/delete-rpc')
|
||||
const DeleteImportedAccount = require('./components/delete-imported-account')
|
||||
const ConfirmChangePassword = require('./components/confirm-change-password')
|
||||
|
@ -93,8 +90,15 @@ function mapStateToProps (state) {
|
|||
|
||||
App.prototype.render = function () {
|
||||
var props = this.props
|
||||
const { isLoading, loadingMessage, transForward, network, provider } = props
|
||||
const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' && props.currentView.name !== 'delete-rpc'
|
||||
const {
|
||||
currentView,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
transForward,
|
||||
network,
|
||||
provider,
|
||||
} = props
|
||||
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' && currentView.name !== 'delete-rpc'
|
||||
const networkName = provider.type === 'rpc' ? `${this.getNetworkName()} (${provider.rpcTarget})` : this.getNetworkName()
|
||||
const loadMessage = loadingMessage || isLoadingNetwork ?
|
||||
`Connecting to ${networkName}` : null
|
||||
|
@ -110,12 +114,9 @@ App.prototype.render = function () {
|
|||
background: (props.isUnlocked || props.currentView.name === 'restoreVault' || props.currentView.name === 'config') ? 'white' : 'linear-gradient(rgb(84, 36, 147), rgb(104, 45, 182))',
|
||||
},
|
||||
}, [
|
||||
|
||||
// app bar
|
||||
this.renderAppBar(),
|
||||
this.renderNetworkDropdown(),
|
||||
this.renderDropdown(),
|
||||
|
||||
h(AppBar, {
|
||||
...this.props,
|
||||
}),
|
||||
this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
|
||||
|
||||
// panel content
|
||||
|
@ -130,358 +131,6 @@ App.prototype.render = function () {
|
|||
)
|
||||
}
|
||||
|
||||
App.prototype.changeState = function (isMainMenuOpen) {
|
||||
this.setState({
|
||||
isMainMenuOpen,
|
||||
sandwichClass: isMainMenuOpen ? 'sandwich-expando expanded' : 'sandwich-expando',
|
||||
})
|
||||
}
|
||||
|
||||
App.prototype.renderAppBar = function () {
|
||||
if (window.METAMASK_UI_TYPE === 'notification') {
|
||||
return null
|
||||
}
|
||||
|
||||
const props = this.props
|
||||
const state = this.state || {}
|
||||
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
|
||||
const {isMascara, isOnboarding} = props
|
||||
|
||||
// Do not render header if user is in mascara onboarding
|
||||
if (isMascara && isOnboarding) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Do not render header if user is in mascara buy ether
|
||||
if (isMascara && props.currentView.name === 'buyEth') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
h('.full-width', {
|
||||
height: '38px',
|
||||
}, [
|
||||
|
||||
h('.app-header.flex-row.flex-space-between', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
visibility: props.isUnlocked ? 'visible' : 'none',
|
||||
background: 'white',
|
||||
height: '38px',
|
||||
position: 'relative',
|
||||
zIndex: 12,
|
||||
},
|
||||
}, [
|
||||
|
||||
h('div.left-menu-section', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
// mini logo
|
||||
h('img', {
|
||||
height: 24,
|
||||
width: 24,
|
||||
src: './images/icon-128.png',
|
||||
}),
|
||||
|
||||
h(NetworkIndicator, {
|
||||
network: this.props.network,
|
||||
provider: this.props.provider,
|
||||
isUnlocked: this.props.isUnlocked,
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
|
||||
},
|
||||
}),
|
||||
|
||||
]),
|
||||
|
||||
props.isUnlocked && h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
h(AccountDropdowns, {
|
||||
style: {},
|
||||
enableAccountsSelector: true,
|
||||
identities: this.props.identities,
|
||||
selected: this.props.selectedAddress,
|
||||
network: this.props.network,
|
||||
keyrings: this.props.keyrings,
|
||||
}, []),
|
||||
|
||||
// hamburger
|
||||
h('div', {
|
||||
className: state.sandwichClass || 'sandwich-expando',
|
||||
style: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
padding: 0,
|
||||
},
|
||||
onClick: () => this.changeState(!state.isMainMenuOpen),
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
App.prototype.renderNetworkDropdown = function () {
|
||||
const props = this.props
|
||||
const { provider: { type: providerType } } = props
|
||||
const rpcList = props.frequentRpcList
|
||||
const state = this.state || {}
|
||||
const isOpen = state.isNetworkMenuOpen
|
||||
|
||||
return h(Dropdown, {
|
||||
useCssTransition: true,
|
||||
isOpen,
|
||||
onClickOutside: (event) => {
|
||||
const { classList } = event.target
|
||||
const isNotToggleElement = [
|
||||
classList.contains('menu-icon'),
|
||||
classList.contains('network-name'),
|
||||
classList.contains('network-indicator'),
|
||||
].filter(bool => bool).length === 0
|
||||
// classes from three constituent nodes of the toggle element
|
||||
|
||||
if (isNotToggleElement) {
|
||||
this.setState({ isNetworkMenuOpen: false })
|
||||
}
|
||||
},
|
||||
zIndex: 11,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
left: '2px',
|
||||
top: '38px',
|
||||
width: '270px',
|
||||
maxHeight: isOpen ? '524px' : '0px',
|
||||
},
|
||||
innerStyle: {
|
||||
padding: '2px 16px 2px 0px',
|
||||
},
|
||||
}, [
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'poa',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('poa')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'poa' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'poa' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(99),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'dai',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('dai')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'dai' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'dai' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(100),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'sokol',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('sokol')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'sokol' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'sokol' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(77),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'main',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('mainnet')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'mainnet' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'mainnet' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(1),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'ropsten',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('ropsten')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'ropsten' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'ropsten' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(3),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'kovan',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('kovan')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'kovan' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'kovan' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(42),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'rinkeby',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('rinkeby')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'rinkeby' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'rinkeby' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(4),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'default',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => {
|
||||
props.dispatch(actions.setRpcTarget('http://localhost:8545'))
|
||||
props.dispatch(actions.setProviderType('localhost'))
|
||||
},
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'localhost' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'localhost' ? 'div.selected-network' : ''),
|
||||
'Localhost 8545',
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => this.props.dispatch(actions.showConfigPage()),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: '#60db97',
|
||||
},
|
||||
},
|
||||
[
|
||||
'Custom RPC',
|
||||
]
|
||||
),
|
||||
|
||||
this.renderSelectedCustomOption(props.provider),
|
||||
this.renderCommonRpc(rpcList, props.provider),
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
App.prototype.renderDropdown = function () {
|
||||
const state = this.state || {}
|
||||
const isOpen = state.isMainMenuOpen
|
||||
const isMainMenuOpen = !isOpen
|
||||
|
||||
return h(Dropdown, {
|
||||
useCssTransition: true,
|
||||
isOpen: isOpen,
|
||||
zIndex: 11,
|
||||
constOverflow: true,
|
||||
onClickOutside: (event) => {
|
||||
const classList = event.target.classList
|
||||
const parentClassList = event.target.parentElement.classList
|
||||
|
||||
const isToggleElement = classList.contains('sandwich-expando') ||
|
||||
parentClassList.contains('sandwich-expando')
|
||||
|
||||
if (isOpen && !isToggleElement) {
|
||||
this.setState({
|
||||
isMainMenuOpen: false,
|
||||
sandwichClass: 'sandwich-expando',
|
||||
})
|
||||
}
|
||||
},
|
||||
style: {
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
top: '38px',
|
||||
width: '126px',
|
||||
maxHeight: isOpen ? '186px' : '0px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
innerStyle: {},
|
||||
}, [
|
||||
h(DropdownMenuItem, {
|
||||
closeMenu: () => this.changeState(isMainMenuOpen),
|
||||
onClick: () => { this.props.dispatch(actions.showConfigPage()) },
|
||||
}, 'Settings'),
|
||||
|
||||
h(DropdownMenuItem, {
|
||||
closeMenu: () => this.changeState(isMainMenuOpen),
|
||||
onClick: () => { this.props.dispatch(actions.lockMetamask()) },
|
||||
}, 'Log Out'),
|
||||
|
||||
h(DropdownMenuItem, {
|
||||
closeMenu: () => this.changeState(isMainMenuOpen),
|
||||
onClick: () => { this.props.dispatch(actions.showInfoPage()) },
|
||||
}, 'Info/Help'),
|
||||
])
|
||||
}
|
||||
|
||||
App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
|
||||
const { isMascara } = this.props
|
||||
|
||||
|
@ -493,25 +142,6 @@ App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork,
|
|||
})
|
||||
}
|
||||
|
||||
App.prototype.renderBackButton = function (style, justArrow = false) {
|
||||
var props = this.props
|
||||
return (
|
||||
h('.flex-row', {
|
||||
key: 'leftArrow',
|
||||
style: style,
|
||||
onClick: () => props.dispatch(actions.goBackToInitView()),
|
||||
}, [
|
||||
h('i.fa.fa-arrow-left.cursor-pointer'),
|
||||
justArrow ? null : h('div.cursor-pointer', {
|
||||
style: {
|
||||
marginLeft: '3px',
|
||||
},
|
||||
onClick: () => props.dispatch(actions.goBackToInitView()),
|
||||
}, 'BACK'),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
App.prototype.renderPrimary = function () {
|
||||
log.debug('rendering primary')
|
||||
var props = this.props
|
||||
|
@ -533,7 +163,6 @@ App.prototype.renderPrimary = function () {
|
|||
key: 'NoticeScreen',
|
||||
onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
|
||||
}),
|
||||
|
||||
])
|
||||
} else if (props.lostAccounts && props.lostAccounts.length > 0) {
|
||||
log.debug('rendering notice screen for lost accounts view.')
|
||||
|
@ -673,7 +302,9 @@ App.prototype.renderPrimary = function () {
|
|||
}, 'QR Code'),
|
||||
]),
|
||||
h('div', [
|
||||
h(QrView, {key: 'qr'}),
|
||||
h(AccountQrScreen, {
|
||||
key: 'account-qr',
|
||||
}),
|
||||
]),
|
||||
])
|
||||
case 'delete-rpc':
|
||||
|
@ -691,95 +322,7 @@ App.prototype.renderPrimary = function () {
|
|||
}
|
||||
}
|
||||
|
||||
App.prototype.toggleMetamaskActive = function () {
|
||||
if (!this.props.isUnlocked) {
|
||||
// currently inactive: redirect to password box
|
||||
var passwordBox = document.querySelector('input[type=password]')
|
||||
if (!passwordBox) return
|
||||
passwordBox.focus()
|
||||
} else {
|
||||
// currently active: deactivate
|
||||
this.props.dispatch(actions.lockMetamask(false))
|
||||
}
|
||||
}
|
||||
|
||||
App.prototype.renderSelectedCustomOption = function (provider) {
|
||||
const { rpcTarget, type } = provider
|
||||
const props = this.props
|
||||
if (type !== 'rpc') return null
|
||||
|
||||
// Concatenate long URLs
|
||||
let label = rpcTarget
|
||||
if (rpcTarget.length > 31) {
|
||||
label = label.substr(0, 34) + '...'
|
||||
}
|
||||
|
||||
switch (rpcTarget) {
|
||||
|
||||
default:
|
||||
return h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: rpcTarget,
|
||||
onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)),
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
[h('div.selected-network'),
|
||||
h('.span.custom-rpc', label),
|
||||
h('.remove', {
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: false })
|
||||
props.dispatch(actions.showDeleteRPC(label))
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
App.prototype.getNetworkName = function () {
|
||||
const { network } = this.props
|
||||
return ethNetProps.props.getNetworkDisplayName(network)
|
||||
}
|
||||
|
||||
App.prototype.renderCommonRpc = function (rpcList, provider) {
|
||||
const props = this.props
|
||||
const { rpcTarget, type } = provider
|
||||
|
||||
return rpcList.map((rpc) => {
|
||||
if (type === 'rpc' && rpc === rpcTarget) {
|
||||
return null
|
||||
} else {
|
||||
return h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: `common${rpc}`,
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
onClick: () => props.dispatch(actions.setRpcTarget(rpc)),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('.span.custom-rpc', rpc),
|
||||
h('.remove', {
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: false })
|
||||
props.dispatch(actions.showDeleteRPC(rpc))
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,599 @@
|
|||
const PropTypes = require('prop-types')
|
||||
const {Component} = require('react')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../../../ui/app/actions')
|
||||
const SandwichExpando = require('sandwich-expando')
|
||||
const {Dropdown} = require('./dropdown')
|
||||
const {DropdownMenuItem} = require('./dropdown')
|
||||
const NetworkIndicator = require('./network')
|
||||
const {AccountDropdowns} = require('./account-dropdowns')
|
||||
const ethNetProps = require('eth-net-props')
|
||||
|
||||
const LOCALHOST_RPC_URL = 'http://localhost:8545'
|
||||
|
||||
module.exports = class AppBar extends Component {
|
||||
static defaultProps = {
|
||||
selectedAddress: undefined,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
frequentRpcList: PropTypes.array.isRequired,
|
||||
isMascara: PropTypes.bool.isRequired,
|
||||
isOnboarding: PropTypes.bool.isRequired,
|
||||
identities: PropTypes.any.isRequired,
|
||||
selectedAddress: PropTypes.string,
|
||||
isUnlocked: PropTypes.bool.isRequired,
|
||||
network: PropTypes.any.isRequired,
|
||||
keyrings: PropTypes.any.isRequired,
|
||||
provider: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
static renderSpace () {
|
||||
return (
|
||||
h('span', {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: ' ',
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
state = {
|
||||
isNetworkMenuOpen: false,
|
||||
}
|
||||
|
||||
changeState (isMainMenuOpen) {
|
||||
this.setState({
|
||||
isMainMenuOpen,
|
||||
sandwichClass: isMainMenuOpen ? 'sandwich-expando expanded' : 'sandwich-expando',
|
||||
})
|
||||
}
|
||||
|
||||
renderAppBar () {
|
||||
if (window.METAMASK_UI_TYPE === 'notification') {
|
||||
return null
|
||||
}
|
||||
|
||||
const props = this.props
|
||||
const state = this.state || {}
|
||||
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
|
||||
const {isMascara, isOnboarding} = props
|
||||
|
||||
// Do not render header if user is in mascara onboarding
|
||||
if (isMascara && isOnboarding) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Do not render header if user is in mascara buy ether
|
||||
if (isMascara && props.currentView.name === 'buyEth') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
h('.full-width', {
|
||||
height: '38px',
|
||||
}, [
|
||||
|
||||
h('.app-header.flex-row.flex-space-between', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
visibility: props.isUnlocked ? 'visible' : 'none',
|
||||
background: 'white',
|
||||
height: '38px',
|
||||
position: 'relative',
|
||||
zIndex: 12,
|
||||
},
|
||||
}, [
|
||||
|
||||
h('div.left-menu-section', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
// mini logo
|
||||
h('img', {
|
||||
height: 24,
|
||||
width: 24,
|
||||
src: './images/icon-128.png',
|
||||
}),
|
||||
|
||||
h(NetworkIndicator, {
|
||||
network: this.props.network,
|
||||
provider: this.props.provider,
|
||||
isUnlocked: this.props.isUnlocked,
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
|
||||
},
|
||||
}),
|
||||
|
||||
]),
|
||||
|
||||
props.isUnlocked && h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
h(AccountDropdowns, {
|
||||
style: {},
|
||||
enableAccountsSelector: true,
|
||||
identities: this.props.identities,
|
||||
selected: this.props.selectedAddress,
|
||||
network: this.props.network,
|
||||
keyrings: this.props.keyrings,
|
||||
}, []),
|
||||
|
||||
// hamburger
|
||||
h('div', {
|
||||
className: state.sandwichClass || 'sandwich-expando',
|
||||
style: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
padding: 0,
|
||||
},
|
||||
onClick: () => this.changeState(!state.isMainMenuOpen),
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
renderAppBarAppHeader () {
|
||||
const {
|
||||
identities,
|
||||
selectedAddress,
|
||||
isUnlocked,
|
||||
network,
|
||||
keyrings,
|
||||
provider,
|
||||
} = this.props
|
||||
const {
|
||||
isNetworkMenuOpen,
|
||||
isMainMenuOpen,
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
h('.full-width', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '38px',
|
||||
},
|
||||
}, [
|
||||
h('.app-header.flex-row.flex-space-between', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
visibility: isUnlocked ? 'visible' : 'none',
|
||||
background: isUnlocked ? 'white' : 'none',
|
||||
height: '38px',
|
||||
position: 'relative',
|
||||
zIndex: 12,
|
||||
},
|
||||
}, [
|
||||
h('div.left-menu-section', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
// mini logo
|
||||
h('img', {
|
||||
height: 24,
|
||||
width: 24,
|
||||
src: './images/icon-128.png',
|
||||
}),
|
||||
h(NetworkIndicator, {
|
||||
network: network,
|
||||
provider: provider,
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
|
||||
},
|
||||
}),
|
||||
]),
|
||||
isUnlocked && h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
h(AccountDropdowns, {
|
||||
style: {},
|
||||
enableAccountsSelector: true,
|
||||
identities: identities,
|
||||
selected: selectedAddress,
|
||||
network,
|
||||
keyrings,
|
||||
}, []),
|
||||
h(SandwichExpando, {
|
||||
className: 'sandwich-expando',
|
||||
width: 16,
|
||||
barHeight: 2,
|
||||
padding: 0,
|
||||
isOpen: isMainMenuOpen,
|
||||
color: 'rgb(247,146,30)',
|
||||
onClick: () => {
|
||||
this.setState({
|
||||
isMainMenuOpen: !isMainMenuOpen,
|
||||
})
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
renderNetworkDropdown () {
|
||||
const props = this.props
|
||||
const { provider: { type: providerType } } = props
|
||||
const rpcList = props.frequentRpcList
|
||||
const state = this.state || {}
|
||||
const isOpen = state.isNetworkMenuOpen
|
||||
|
||||
return h(Dropdown, {
|
||||
useCssTransition: true,
|
||||
isOpen,
|
||||
onClickOutside: (event) => {
|
||||
const { classList } = event.target
|
||||
const isNotToggleElement = [
|
||||
classList.contains('menu-icon'),
|
||||
classList.contains('network-name'),
|
||||
classList.contains('network-indicator'),
|
||||
].filter(bool => bool).length === 0
|
||||
// classes from three constituent nodes of the toggle element
|
||||
|
||||
if (isNotToggleElement) {
|
||||
this.setState({ isNetworkMenuOpen: false })
|
||||
}
|
||||
},
|
||||
zIndex: 11,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
left: '2px',
|
||||
top: '38px',
|
||||
width: '270px',
|
||||
maxHeight: isOpen ? '524px' : '0px',
|
||||
},
|
||||
innerStyle: {
|
||||
padding: '2px 16px 2px 0px',
|
||||
},
|
||||
}, [
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'poa',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('poa')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'poa' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'poa' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(99),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'dai',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('dai')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'dai' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'dai' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(100),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'sokol',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('sokol')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'sokol' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'sokol' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(77),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'main',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('mainnet')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'mainnet' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'mainnet' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(1),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'ropsten',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('ropsten')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'ropsten' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'ropsten' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(3),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'kovan',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('kovan')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'kovan' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'kovan' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(42),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'rinkeby',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => props.dispatch(actions.setProviderType('rinkeby')),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'rinkeby' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'rinkeby' ? 'div.selected-network' : ''),
|
||||
ethNetProps.props.getNetworkDisplayName(4),
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: 'default',
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => {
|
||||
props.dispatch(actions.setRpcTarget('http://localhost:8545'))
|
||||
props.dispatch(actions.setProviderType('localhost'))
|
||||
},
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: providerType === 'localhost' ? 'white' : '',
|
||||
},
|
||||
},
|
||||
[h(providerType === 'localhost' ? 'div.selected-network' : ''),
|
||||
'Localhost 8545',
|
||||
]
|
||||
),
|
||||
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||
onClick: () => this.props.dispatch(actions.showConfigPage()),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: '#60db97',
|
||||
},
|
||||
},
|
||||
[
|
||||
'Custom RPC',
|
||||
]
|
||||
),
|
||||
|
||||
this.renderSelectedCustomOption(props.provider),
|
||||
this.renderCommonRpc(rpcList, props.provider),
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
renderCustomOption ({ rpcTarget, type }) {
|
||||
const {dispatch} = this.props
|
||||
|
||||
if (type !== 'rpc') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Concatenate long URLs
|
||||
let label = rpcTarget
|
||||
if (rpcTarget.length > 31) {
|
||||
label = label.substr(0, 34) + '...'
|
||||
}
|
||||
|
||||
switch (rpcTarget) {
|
||||
case LOCALHOST_RPC_URL:
|
||||
return null
|
||||
default:
|
||||
return h(DropdownMenuItem, {
|
||||
key: rpcTarget,
|
||||
onClick: () => dispatch(actions.setRpcTarget(rpcTarget)),
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
}, [
|
||||
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
|
||||
label,
|
||||
h('.check', '✓'),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
renderCommonRpc (rpcList, provider) {
|
||||
const props = this.props
|
||||
const { rpcTarget, type } = provider
|
||||
|
||||
return rpcList.map((rpc) => {
|
||||
if (type === 'rpc' && rpc === rpcTarget) {
|
||||
return null
|
||||
} else {
|
||||
return h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: `common${rpc}`,
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
onClick: () => props.dispatch(actions.setRpcTarget(rpc)),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('.span.custom-rpc', rpc),
|
||||
h('.remove', {
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: false })
|
||||
props.dispatch(actions.showDeleteRPC(rpc))
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
renderSelectedCustomOption (provider) {
|
||||
const { rpcTarget, type } = provider
|
||||
const props = this.props
|
||||
if (type !== 'rpc') return null
|
||||
|
||||
// Concatenate long URLs
|
||||
let label = rpcTarget
|
||||
if (rpcTarget.length > 31) {
|
||||
label = label.substr(0, 34) + '...'
|
||||
}
|
||||
|
||||
switch (rpcTarget) {
|
||||
|
||||
default:
|
||||
return h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
key: rpcTarget,
|
||||
onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)),
|
||||
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
|
||||
style: {
|
||||
paddingLeft: '20px',
|
||||
fontSize: '16px',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
[h('div.selected-network'),
|
||||
h('.span.custom-rpc', label),
|
||||
h('.remove', {
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.setState({ isNetworkMenuOpen: false })
|
||||
props.dispatch(actions.showDeleteRPC(label))
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderDropdown () {
|
||||
const state = this.state || {}
|
||||
const isOpen = state.isMainMenuOpen
|
||||
const isMainMenuOpen = !isOpen
|
||||
|
||||
return h(Dropdown, {
|
||||
useCssTransition: true,
|
||||
isOpen: isOpen,
|
||||
zIndex: 11,
|
||||
constOverflow: true,
|
||||
onClickOutside: (event) => {
|
||||
const classList = event.target.classList
|
||||
const parentClassList = event.target.parentElement.classList
|
||||
|
||||
const isToggleElement = classList.contains('sandwich-expando') ||
|
||||
parentClassList.contains('sandwich-expando')
|
||||
|
||||
if (isOpen && !isToggleElement) {
|
||||
this.setState({
|
||||
isMainMenuOpen: false,
|
||||
sandwichClass: 'sandwich-expando',
|
||||
})
|
||||
}
|
||||
},
|
||||
style: {
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
top: '38px',
|
||||
width: '126px',
|
||||
maxHeight: isOpen ? '186px' : '0px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
innerStyle: {},
|
||||
}, [
|
||||
h(DropdownMenuItem, {
|
||||
closeMenu: () => this.changeState(isMainMenuOpen),
|
||||
onClick: () => { this.props.dispatch(actions.showConfigPage()) },
|
||||
}, 'Settings'),
|
||||
|
||||
h(DropdownMenuItem, {
|
||||
closeMenu: () => this.changeState(isMainMenuOpen),
|
||||
onClick: () => { this.props.dispatch(actions.lockMetamask()) },
|
||||
}, 'Log Out'),
|
||||
|
||||
h(DropdownMenuItem, {
|
||||
closeMenu: () => this.changeState(isMainMenuOpen),
|
||||
onClick: () => { this.props.dispatch(actions.showInfoPage()) },
|
||||
}, 'Info/Help'),
|
||||
])
|
||||
}
|
||||
|
||||
render () {
|
||||
return h('div.full-width', [
|
||||
this.renderAppBar(),
|
||||
this.renderNetworkDropdown(),
|
||||
this.renderDropdown(),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const qrCode = require('qrcode-npm').qrcode
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const isHexPrefixed = require('ethereumjs-util').isHexPrefixed
|
||||
const CopyButton = require('./copyButton')
|
||||
|
||||
module.exports = connect(mapStateToProps)(QrCodeView)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
Qr: state.appState.Qr,
|
||||
buyView: state.appState.buyView,
|
||||
warning: state.appState.warning,
|
||||
}
|
||||
}
|
||||
|
||||
inherits(QrCodeView, Component)
|
||||
|
||||
function QrCodeView () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
QrCodeView.prototype.render = function () {
|
||||
const props = this.props
|
||||
const Qr = props.Qr
|
||||
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
|
||||
const qrImage = qrCode(4, 'M')
|
||||
qrImage.addData(address)
|
||||
qrImage.make()
|
||||
return h('.main-container.flex-column', {
|
||||
key: 'qr',
|
||||
style: {
|
||||
justifyContent: 'center',
|
||||
paddingBottom: '45px',
|
||||
paddingLeft: '45px',
|
||||
paddingRight: '45px',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
this.props.warning ? this.props.warning && h('span.error.flex-center',
|
||||
this.props.warning) : null,
|
||||
|
||||
h('#qr-container.flex-column', {
|
||||
style: {
|
||||
marginTop: '15px',
|
||||
},
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: qrImage.createTableTag(4),
|
||||
},
|
||||
}),
|
||||
Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message),
|
||||
h('.flex-row', [
|
||||
h('h3.ellip-address', {
|
||||
style: {
|
||||
width: '247px',
|
||||
},
|
||||
}, Qr.data),
|
||||
h(CopyButton, {
|
||||
value: Qr.data,
|
||||
}),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
QrCodeView.prototype.renderMultiMessage = function () {
|
||||
var Qr = this.props.Qr
|
||||
var multiMessage = Qr.message.map((message) => h('.qr-message', message))
|
||||
return multiMessage
|
||||
}
|
|
@ -3,7 +3,6 @@ const h = require('react-hyperscript')
|
|||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../../ui/app/actions')
|
||||
const Qr = require('./qr-code')
|
||||
const isValidAddress = require('../util').isValidAddress
|
||||
module.exports = connect(mapStateToProps)(ShapeshiftForm)
|
||||
|
||||
|
@ -11,7 +10,6 @@ function mapStateToProps (state) {
|
|||
return {
|
||||
warning: state.appState.warning,
|
||||
isSubLoading: state.appState.isSubLoading,
|
||||
qrRequested: state.appState.qrRequested,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +21,7 @@ function ShapeshiftForm () {
|
|||
}
|
||||
|
||||
ShapeshiftForm.prototype.render = function () {
|
||||
return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain()
|
||||
return this.renderMain()
|
||||
}
|
||||
|
||||
ShapeshiftForm.prototype.renderMain = function () {
|
||||
|
|
|
@ -36,14 +36,23 @@ TransactionListItem.prototype.showRetryButton = function () {
|
|||
return false
|
||||
}
|
||||
|
||||
let currentTxSharesEarliestNonce = false
|
||||
const currentNonce = txParams.nonce
|
||||
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
|
||||
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
|
||||
const currentSubmittedTxs = transactions.filter(tx => tx.status === 'submitted')
|
||||
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[0]
|
||||
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
|
||||
lastSubmittedTxWithCurrentNonce.id === transaction.id
|
||||
if (currentSubmittedTxs.length > 0) {
|
||||
const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => {
|
||||
if (tx1.submittedTime < tx2.submittedTime) return tx1
|
||||
return tx2
|
||||
})
|
||||
currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
|
||||
}
|
||||
|
||||
return currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000
|
||||
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000
|
||||
}
|
||||
|
||||
TransactionListItem.prototype.render = function () {
|
||||
|
|
|
@ -906,7 +906,109 @@ div.message-container > div:first-child {
|
|||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
//Notification Modal
|
||||
.new-ui-announcement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: white;
|
||||
color: #4D4D4D;
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.new-ui-announcement__announcement-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.new-ui-announcement__announcement-header a.close {
|
||||
cursor: pointer;
|
||||
font-size: 32px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.new-ui-announcement__announcement-header a.close:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.new-ui-announcement__announcement-header h1 {
|
||||
color: #33A4E7;
|
||||
text-transform: uppercase;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.new-ui-announcement__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
font-size: 10.5pt;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.new-ui-announcement__body h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.new-ui-announcement__body a {
|
||||
color: #33A4E7;
|
||||
}
|
||||
|
||||
.new-ui-announcement__body .updates-list {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.new-ui-announcement__body .updates-list h2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new-ui-announcement__body .updates-list ul {
|
||||
list-style: disc inside;
|
||||
}
|
||||
|
||||
.new-ui-announcement__footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.new-ui-announcement__footer h1 {
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new-ui-announcement__footer button:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.new-ui-announcement__footer button.positive {
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
background: #33A4E7;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
box-shadow: none;
|
||||
border-radius: 5px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.new-ui-announcement__footer button.negative {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: white;
|
||||
color: #33A4E7;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.notification-modal-wrapper {
|
||||
display: flex;
|
||||
|
@ -1036,4 +1138,4 @@ div.message-container > div:first-child {
|
|||
|
||||
input[disabled] {
|
||||
background: #FFFADE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
const PropTypes = require('prop-types')
|
||||
const {PureComponent} = require('react')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../../ui/app/actions')
|
||||
|
||||
module.exports = class NewUiAnnouncement extends PureComponent {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
close = async () => {
|
||||
await this.props.dispatch(actions.setFeatureFlag('skipAnnounceBetaUI', true))
|
||||
}
|
||||
|
||||
switchToNewUi = async () => {
|
||||
const flag = 'betaUI'
|
||||
const enabled = true
|
||||
await this.props.dispatch(actions.setFeatureFlag(
|
||||
flag,
|
||||
enabled,
|
||||
))
|
||||
await this.close()
|
||||
global.platform.openExtensionInBrowser()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
h('div.new-ui-announcement', [
|
||||
h('section.new-ui-announcement__announcement-header', [
|
||||
h('h1', 'Announcement'),
|
||||
h('a.close', {
|
||||
onClick: this.close,
|
||||
}, '×'),
|
||||
]),
|
||||
h('section.new-ui-announcement__body', [
|
||||
h('h1', 'A New Version of MetaMask'),
|
||||
h('p', [
|
||||
"We're excited to announce a brand-new version of MetaMask with enhanced features and functionality.",
|
||||
]),
|
||||
h('div.updates-list', [
|
||||
h('h2', 'Updates include'),
|
||||
h('ul', [
|
||||
h('li', 'New user interface'),
|
||||
h('li', 'Full-screen mode'),
|
||||
h('li', 'Better token support'),
|
||||
h('li', 'Better gas controls'),
|
||||
h('li', 'Advanced features for developers'),
|
||||
h('li', 'New confirmation screens'),
|
||||
h('li', 'And more!'),
|
||||
]),
|
||||
]),
|
||||
h('p', [
|
||||
'You can still use the current version of MetaMask. The new version is still in beta, ' +
|
||||
'however we encourage you to try it out as we transition into this exciting new update.',
|
||||
h('span', {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: ' ',
|
||||
},
|
||||
}),
|
||||
h('a', {
|
||||
href: 'https://medium.com/metamask/74dba32cc7f7',
|
||||
onClick ({target}) {
|
||||
const url = target.href
|
||||
global.platform.openWindow({
|
||||
url,
|
||||
})
|
||||
},
|
||||
}, [
|
||||
'Learn more.',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
h('section.new-ui-announcement__footer', [
|
||||
h('h1', 'Ready to try the new MetaMask?'),
|
||||
h('button.positive', {
|
||||
onClick: this.switchToNewUi,
|
||||
}, 'Try it now'),
|
||||
h('button.negative', {
|
||||
onClick: this.close,
|
||||
}, 'No thanks, maybe later'),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -74,7 +74,8 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^1.0.0",
|
||||
"@material-ui/core": "1.0.0",
|
||||
"@zxing/library": "^0.7.0",
|
||||
"abi-decoder": "^1.0.9",
|
||||
"asmcrypto.js": "0.22.0",
|
||||
"async": "^2.5.0",
|
||||
|
@ -97,6 +98,7 @@
|
|||
"debounce-stream": "^2.0.0",
|
||||
"deep-extend": "^0.5.1",
|
||||
"detect-node": "^2.0.3",
|
||||
"detectrtc": "^1.3.6",
|
||||
"disc": "^1.3.2",
|
||||
"dnode": "^1.2.2",
|
||||
"end-of-stream": "^1.1.0",
|
||||
|
@ -108,6 +110,7 @@
|
|||
"eth-hd-keyring": "^2.0.0",
|
||||
"eth-json-rpc-filters": "^3.0.1",
|
||||
"eth-json-rpc-infura": "^3.0.0",
|
||||
"eth-ledger-bridge-keyring": "^0.1.0",
|
||||
"eth-method-registry": "^1.0.0",
|
||||
"eth-net-props": "^1.0.7",
|
||||
"eth-phishing-detect": "^1.1.4",
|
||||
|
@ -200,7 +203,6 @@
|
|||
"semaphore": "^1.0.5",
|
||||
"semver": "^5.4.1",
|
||||
"shallow-copy": "0.0.1",
|
||||
"superstatic": "^5.0.2",
|
||||
"sw-controller": "^1.0.3",
|
||||
"sw-stream": "^2.0.2",
|
||||
"valid-url": "^1.0.9",
|
||||
|
@ -208,6 +210,7 @@
|
|||
"web3": "^0.20.1",
|
||||
"web3-provider-engine": "^14.0.5",
|
||||
"web3-stream-provider": "^3.0.1",
|
||||
"webrtc-adapter": "^6.3.0",
|
||||
"xtend": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -229,7 +232,7 @@
|
|||
"brfs": "^1.6.1",
|
||||
"browserify": "^16.1.1",
|
||||
"chai": "^4.1.0",
|
||||
"chromedriver": "2.36.0",
|
||||
"chromedriver": "^2.36.0",
|
||||
"clipboardy": "^1.2.3",
|
||||
"compression": "^1.7.1",
|
||||
"coveralls": "^3.0.0",
|
||||
|
@ -309,6 +312,7 @@
|
|||
"shell-parallel": "^1.0.3",
|
||||
"sinon": "^5.0.0",
|
||||
"source-map": "^0.7.2",
|
||||
"static-server": "^2.2.1",
|
||||
"style-loader": "^0.21.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"tape": "^4.5.1",
|
||||
|
|
|
@ -38,27 +38,34 @@ const transferTokens = document.getElementById('transferTokens')
|
|||
const approveTokens = document.getElementById('approveTokens')
|
||||
|
||||
deployButton.addEventListener('click', async function (event) {
|
||||
document.getElementById('contractStatus').innerHTML = 'Deploying'
|
||||
|
||||
var piggybank = await piggybankContract.new(
|
||||
{
|
||||
from: web3.eth.accounts[0],
|
||||
data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
|
||||
gas: '4700000',
|
||||
}, function (e, contract) {
|
||||
console.log(e, contract)
|
||||
if (e) {
|
||||
throw e
|
||||
}
|
||||
if (typeof contract.address !== 'undefined') {
|
||||
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
|
||||
|
||||
console.log(`contract`, contract)
|
||||
document.getElementById('contractStatus').innerHTML = 'Deployed'
|
||||
|
||||
depositButton.addEventListener('click', function (event) {
|
||||
document.getElementById('contractStatus').innerHTML = 'Deposit initiated'
|
||||
contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) {
|
||||
console.log(result)
|
||||
document.getElementById('contractStatus').innerHTML = 'Deposit completed'
|
||||
})
|
||||
})
|
||||
|
||||
withdrawButton.addEventListener('click', function (event) {
|
||||
contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
|
||||
console.log(result)
|
||||
document.getElementById('contractStatus').innerHTML = 'Withdrawn'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
<button id="depositButton">Deposit</button>
|
||||
<button id="withdrawButton">Withdraw</button>
|
||||
</div>
|
||||
<div id="contractStatus" style="display: flex; font-size: 1rem;">
|
||||
Not clicked
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Send eth</div>
|
||||
|
|
|
@ -12,9 +12,11 @@ const {
|
|||
} = require('../func')
|
||||
const {
|
||||
checkBrowserForConsoleErrors,
|
||||
closeAllWindowHandlesExcept,
|
||||
verboseReportOnFailure,
|
||||
findElement,
|
||||
findElements,
|
||||
loadExtension,
|
||||
} = require('./helpers')
|
||||
|
||||
|
||||
|
@ -25,6 +27,7 @@ describe('Using MetaMask with an existing account', function () {
|
|||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
|
||||
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
|
||||
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
|
||||
const tinyDelayMs = 500
|
||||
const regularDelayMs = 1000
|
||||
const largeDelayMs = regularDelayMs * 2
|
||||
|
||||
|
@ -74,37 +77,58 @@ describe('Using MetaMask with an existing account', function () {
|
|||
|
||||
describe('New UI setup', async function () {
|
||||
it('switches to first tab', async function () {
|
||||
await delay(tinyDelayMs)
|
||||
const [firstTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(firstTab)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('use the local network', async function () {
|
||||
const networkSelector = await findElement(driver, By.css('#network_component'))
|
||||
await networkSelector.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [localhost] = await findElements(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
|
||||
await localhost.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('selects the new UI option', async () => {
|
||||
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
|
||||
try {
|
||||
const overlay = await findElement(driver, By.css('.full-flex-height'))
|
||||
await driver.wait(until.stalenessOf(overlay))
|
||||
} catch (e) {}
|
||||
|
||||
let button
|
||||
try {
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(largeDelayMs)
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
}
|
||||
await button.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Close all other tabs
|
||||
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
|
||||
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(tab0)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const newUiOrInfoPage = newUi || infoPage
|
||||
await driver.switchTo().window(oldUi)
|
||||
await driver.close()
|
||||
if (infoPage !== newUiOrInfoPage) {
|
||||
await driver.switchTo().window(infoPage)
|
||||
await driver.close()
|
||||
let selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (tab0 && selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab0)
|
||||
} else if (tab1) {
|
||||
await driver.switchTo().window(tab1)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab1)
|
||||
} else if (tab2) {
|
||||
await driver.switchTo().window(tab2)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
|
||||
}
|
||||
} else {
|
||||
throw new Error('popup.html not found')
|
||||
}
|
||||
await driver.switchTo().window(newUiOrInfoPage)
|
||||
await delay(regularDelayMs)
|
||||
const [appTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(appTab)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
|
||||
|
@ -208,6 +232,16 @@ describe('Using MetaMask with an existing account', function () {
|
|||
})
|
||||
|
||||
describe('Add an account', () => {
|
||||
it('switches to localhost', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
|
||||
await localhost.click()
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('choose Create Account from the account menu', async () => {
|
||||
await driver.findElement(By.css('.account-menu__icon')).click()
|
||||
await delay(regularDelayMs)
|
||||
|
@ -332,7 +366,10 @@ describe('Using MetaMask with an existing account', function () {
|
|||
})
|
||||
|
||||
it('should open the TREZOR Connect popup', async () => {
|
||||
const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect to Trezor')]`))
|
||||
const trezorButton = await findElements(driver, By.css('.hw-connect__btn'))
|
||||
await trezorButton[1].click()
|
||||
await delay(regularDelayMs)
|
||||
const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
|
||||
await connectButtons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
const allWindows = await driver.getAllWindowHandles()
|
||||
|
|
|
@ -122,12 +122,14 @@ async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) {
|
|||
}
|
||||
|
||||
async function assertElementNotPresent (webdriver, driver, by) {
|
||||
let dataTab
|
||||
try {
|
||||
const dataTab = await findElement(driver, by, 4000)
|
||||
if (dataTab) {
|
||||
assert(false, 'Data tab should not be present')
|
||||
}
|
||||
dataTab = await findElement(driver, by, 4000)
|
||||
} catch (err) {
|
||||
assert(err instanceof webdriver.error.NoSuchElementError)
|
||||
console.log(err)
|
||||
assert(err instanceof webdriver.error.NoSuchElementError || err instanceof webdriver.error.TimeoutError)
|
||||
}
|
||||
if (dataTab) {
|
||||
assert(false, 'Data tab should not be present')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,30 +75,11 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
describe('New UI setup', async function () {
|
||||
let networkSelector
|
||||
it('switches to first tab', async function () {
|
||||
await delay(tinyDelayMs)
|
||||
const [firstTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(firstTab)
|
||||
await delay(regularDelayMs)
|
||||
try {
|
||||
networkSelector = await findElement(driver, By.css('#network_component'))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(largeDelayMs * 2)
|
||||
networkSelector = await findElement(driver, By.css('#network_component'))
|
||||
}
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('uses the local network', async function () {
|
||||
await networkSelector.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const networks = await findElements(driver, By.css('.dropdown-menu-item'))
|
||||
const localhost = networks[4]
|
||||
await driver.wait(until.elementTextMatches(localhost, /Localhost/))
|
||||
await localhost.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('selects the new UI option', async () => {
|
||||
|
@ -107,27 +88,47 @@ describe('MetaMask', function () {
|
|||
await driver.wait(until.stalenessOf(overlay))
|
||||
} catch (e) {}
|
||||
|
||||
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
|
||||
let button
|
||||
try {
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(largeDelayMs)
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
}
|
||||
await button.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Close all other tabs
|
||||
const [oldUi, tab1, tab2] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(oldUi)
|
||||
await driver.close()
|
||||
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(tab0)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
await driver.switchTo().window(tab1)
|
||||
const tab1Url = await driver.getCurrentUrl()
|
||||
if (tab1Url.match(/metamask.io/)) {
|
||||
await driver.switchTo().window(tab1)
|
||||
await driver.close()
|
||||
await driver.switchTo().window(tab2)
|
||||
} else if (tab2) {
|
||||
await driver.switchTo().window(tab2)
|
||||
await driver.close()
|
||||
let selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (tab0 && selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab0)
|
||||
} else if (tab1) {
|
||||
await driver.switchTo().window(tab1)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab1)
|
||||
} else if (tab2) {
|
||||
await driver.switchTo().window(tab2)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
|
||||
}
|
||||
} else {
|
||||
throw new Error('popup.html not found')
|
||||
}
|
||||
await delay(regularDelayMs)
|
||||
const [appTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(appTab)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
|
||||
await continueBtn.click()
|
||||
|
@ -201,7 +202,16 @@ describe('MetaMask', function () {
|
|||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function retypeSeedPhrase (words, wasReloaded) {
|
||||
async function clickWordAndWait (word) {
|
||||
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
|
||||
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${word}')]`
|
||||
const word0 = await findElement(driver, By.xpath(xpath), 10000)
|
||||
|
||||
await word0.click()
|
||||
await delay(tinyDelayMs)
|
||||
}
|
||||
|
||||
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
|
||||
try {
|
||||
if (wasReloaded) {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
|
@ -215,67 +225,26 @@ describe('MetaMask', function () {
|
|||
await delay(regularDelayMs)
|
||||
}
|
||||
|
||||
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
|
||||
await clickWordAndWait(words[0])
|
||||
await clickWordAndWait(words[1])
|
||||
await clickWordAndWait(words[2])
|
||||
await clickWordAndWait(words[3])
|
||||
await clickWordAndWait(words[4])
|
||||
await clickWordAndWait(words[5])
|
||||
await clickWordAndWait(words[6])
|
||||
await clickWordAndWait(words[7])
|
||||
await clickWordAndWait(words[8])
|
||||
await clickWordAndWait(words[9])
|
||||
await clickWordAndWait(words[10])
|
||||
await clickWordAndWait(words[11])
|
||||
|
||||
await word0.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`), 10000)
|
||||
|
||||
await word1.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000)
|
||||
|
||||
await word2.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000)
|
||||
|
||||
await word3.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000)
|
||||
|
||||
await word4.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000)
|
||||
|
||||
await word5.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000)
|
||||
|
||||
await word6.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`), 10000)
|
||||
|
||||
await word7.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`), 10000)
|
||||
|
||||
await word8.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`), 10000)
|
||||
|
||||
await word9.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`), 10000)
|
||||
|
||||
await word10.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`), 10000)
|
||||
await word11.click()
|
||||
await delay(tinyDelayMs)
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await retypeSeedPhrase(words, true)
|
||||
if (count > 2) {
|
||||
throw e
|
||||
} else {
|
||||
await loadExtension(driver, extensionId)
|
||||
await retypeSeedPhrase(words, true, count + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,12 +352,22 @@ describe('MetaMask', function () {
|
|||
const passwordInputs = await driver.findElements(By.css('input'))
|
||||
await delay(regularDelayMs)
|
||||
|
||||
passwordInputs[0].sendKeys('correct horse battery staple')
|
||||
passwordInputs[1].sendKeys('correct horse battery staple')
|
||||
await passwordInputs[0].sendKeys('correct horse battery staple')
|
||||
await passwordInputs[1].sendKeys('correct horse battery staple')
|
||||
await driver.findElement(By.css('.first-time-flow__button')).click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('switches to localhost', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
|
||||
await localhost.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
|
||||
it('balance renders', async () => {
|
||||
const balance = await findElement(driver, By.css('.balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextMatches(balance, /100.+ETH/))
|
||||
|
@ -466,7 +445,7 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(windowHandles[2])
|
||||
await delay(regularDelayMs)
|
||||
|
||||
assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
await assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000)
|
||||
await confirmButton.click()
|
||||
|
@ -481,6 +460,11 @@ describe('MetaMask', function () {
|
|||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
await findElement(driver, By.xpath(`//span[contains(text(), 'Submitted')]`))
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
|
||||
})
|
||||
|
@ -512,7 +496,7 @@ describe('MetaMask', function () {
|
|||
|
||||
it('displays the contract creation data', async () => {
|
||||
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
dataTab.click()
|
||||
await dataTab.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
|
||||
|
@ -522,7 +506,7 @@ describe('MetaMask', function () {
|
|||
assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/))
|
||||
|
||||
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
|
||||
detailsTab.click()
|
||||
await detailsTab.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
|
@ -531,6 +515,8 @@ describe('MetaMask', function () {
|
|||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await findElement(driver, By.xpath(`//span[contains(text(), 'Submitted')]`))
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
|
@ -543,9 +529,15 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
let contractStatus = await findElement(driver, By.css('#contractStatus'))
|
||||
await driver.wait(until.elementTextMatches(contractStatus, /Deployed/), 15000)
|
||||
|
||||
const depositButton = await findElement(driver, By.css('#depositButton'))
|
||||
await depositButton.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
contractStatus = await findElement(driver, By.css('#contractStatus'))
|
||||
await driver.wait(until.elementTextMatches(contractStatus, /Deposit\sinitiated/), 10000)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(largeDelayMs)
|
||||
|
@ -561,8 +553,8 @@ describe('MetaMask', function () {
|
|||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal'))
|
||||
await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
|
||||
const gasModal = await findElement(driver, By.css('span .modal'))
|
||||
await driver.wait(until.elementLocated(By.css('.customize-gas__title')), 10000)
|
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
|
||||
await gasPriceInput.clear()
|
||||
|
|
|
@ -6,5 +6,5 @@ set -o pipefail
|
|||
|
||||
export PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
||||
|
|
|
@ -1754,4 +1754,4 @@ describe('Metamask popup page', async function () {
|
|||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
|
@ -27,6 +27,11 @@ async function runFirstTimeUsageTest(assert, done) {
|
|||
|
||||
const app = $('#app-content')
|
||||
|
||||
// Selects new ui
|
||||
const tryNewUIButton = (await findAsync(app, 'button.negative'))[0]
|
||||
tryNewUIButton.click()
|
||||
await timeout()
|
||||
|
||||
// recurse notices
|
||||
while (true) {
|
||||
const button = await findAsync(app, 'button')
|
||||
|
|
|
@ -14,6 +14,11 @@ QUnit.skip('renders list items successfully', (assert) => {
|
|||
})
|
||||
})
|
||||
|
||||
global.ethQuery = global.ethQuery || {}
|
||||
global.ethQuery.getTransactionCount = (_, cb) => {
|
||||
cb(null, '0x3')
|
||||
}
|
||||
|
||||
async function runTxListItemsTest (assert, done) {
|
||||
console.log('*** start runTxListItemsTest')
|
||||
const selectState = await queryAsync($, 'select')
|
||||
|
|
|
@ -7,10 +7,11 @@ const PreferencesController = require('../../../../app/scripts/controllers/prefe
|
|||
|
||||
describe('DetectTokensController', () => {
|
||||
const sandbox = sinon.createSandbox()
|
||||
let clock
|
||||
let keyringMemStore
|
||||
before(async () => {
|
||||
let clock, keyringMemStore, network, preferences
|
||||
beforeEach(async () => {
|
||||
keyringMemStore = new ObservableStore({ isUnlocked: false})
|
||||
network = new NetworkController({ provider: { type: 'mainnet' }})
|
||||
preferences = new PreferencesController({ network })
|
||||
})
|
||||
after(() => {
|
||||
sandbox.restore()
|
||||
|
@ -25,9 +26,7 @@ describe('DetectTokensController', () => {
|
|||
|
||||
it('should be called on every polling period', async () => {
|
||||
clock = sandbox.useFakeTimers()
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('mainnet')
|
||||
const preferences = new PreferencesController()
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.isUnlocked = true
|
||||
|
@ -45,9 +44,7 @@ describe('DetectTokensController', () => {
|
|||
})
|
||||
|
||||
it('should not check tokens while in test network', async () => {
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('rinkeby')
|
||||
const preferences = new PreferencesController()
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.isUnlocked = true
|
||||
|
@ -61,9 +58,7 @@ describe('DetectTokensController', () => {
|
|||
})
|
||||
|
||||
it('should only check and add tokens while in main network', async () => {
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('mainnet')
|
||||
const preferences = new PreferencesController()
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.isUnlocked = true
|
||||
|
@ -79,30 +74,28 @@ describe('DetectTokensController', () => {
|
|||
{address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK', network: 1}])
|
||||
})
|
||||
|
||||
it('should not detect same token while in main network', async () => {
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('mainnet')
|
||||
const preferences = new PreferencesController()
|
||||
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8, 1)
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.isUnlocked = true
|
||||
//todo: doesn't work
|
||||
// it('should not detect same token while in main network', async () => {
|
||||
// network.setProviderType('mainnet')
|
||||
// const preferences = new PreferencesController()
|
||||
// preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8, 1)
|
||||
// const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
// controller.isOpen = true
|
||||
// controller.isUnlocked = true
|
||||
|
||||
sandbox.stub(controller, 'detectTokenBalance')
|
||||
.withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4')
|
||||
.returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8, 1))
|
||||
.withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388')
|
||||
.returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18, 1))
|
||||
// sandbox.stub(controller, 'detectTokenBalance')
|
||||
// .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4')
|
||||
// .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8, 1))
|
||||
// .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388')
|
||||
// .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18, 1))
|
||||
|
||||
await controller.detectNewTokens()
|
||||
assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T', network: 1},
|
||||
{address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK', network: 1}])
|
||||
})
|
||||
// await controller.detectNewTokens()
|
||||
// assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T', network: 1},
|
||||
// {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK', network: 1}])
|
||||
// })
|
||||
|
||||
it('should trigger detect new tokens when change address', async () => {
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('mainnet')
|
||||
const preferences = new PreferencesController()
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.isUnlocked = true
|
||||
|
@ -112,9 +105,7 @@ describe('DetectTokensController', () => {
|
|||
})
|
||||
|
||||
it('should trigger detect new tokens when submit password', async () => {
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('mainnet')
|
||||
const preferences = new PreferencesController()
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.selectedAddress = '0x0'
|
||||
|
@ -124,9 +115,7 @@ describe('DetectTokensController', () => {
|
|||
})
|
||||
|
||||
it('should not trigger detect new tokens when not open or not unlocked', async () => {
|
||||
const network = new NetworkController()
|
||||
network.setProviderType('mainnet')
|
||||
const preferences = new PreferencesController()
|
||||
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
|
||||
controller.isOpen = true
|
||||
controller.isUnlocked = false
|
||||
|
|
|
@ -226,9 +226,9 @@ describe('MetaMaskController', function () {
|
|||
|
||||
it('should throw if it receives an unknown device name', async function () {
|
||||
try {
|
||||
await metamaskController.connectHardware('Some random device name', 0)
|
||||
await metamaskController.connectHardware('Some random device name', 0, `m/44/0'/0'`)
|
||||
} catch (e) {
|
||||
assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device')
|
||||
assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -242,14 +242,24 @@ describe('MetaMaskController', function () {
|
|||
assert.equal(keyrings.length, 1)
|
||||
})
|
||||
|
||||
it('should add the Ledger Hardware keyring', async function () {
|
||||
sinon.spy(metamaskController.keyringController, 'addNewKeyring')
|
||||
await metamaskController.connectHardware('ledger', 0).catch((e) => null)
|
||||
const keyrings = await metamaskController.keyringController.getKeyringsByType(
|
||||
'Ledger Hardware'
|
||||
)
|
||||
assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Ledger Hardware')
|
||||
assert.equal(keyrings.length, 1)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('checkHardwareStatus', function () {
|
||||
it('should throw if it receives an unknown device name', async function () {
|
||||
try {
|
||||
await metamaskController.checkHardwareStatus('Some random device name')
|
||||
await metamaskController.checkHardwareStatus('Some random device name', `m/44/0'/0'`)
|
||||
} catch (e) {
|
||||
assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device')
|
||||
assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -265,7 +275,7 @@ describe('MetaMaskController', function () {
|
|||
try {
|
||||
await metamaskController.forgetDevice('Some random device name')
|
||||
} catch (e) {
|
||||
assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device')
|
||||
assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -282,7 +292,7 @@ describe('MetaMaskController', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('unlockTrezorAccount', function () {
|
||||
describe('unlockHardwareWalletAccount', function () {
|
||||
let accountToUnlock
|
||||
let windowOpenStub
|
||||
let addNewAccountStub
|
||||
|
@ -305,16 +315,20 @@ describe('MetaMaskController', function () {
|
|||
sinon.spy(metamaskController.preferencesController, 'setAddresses')
|
||||
sinon.spy(metamaskController.preferencesController, 'setSelectedAddress')
|
||||
sinon.spy(metamaskController.preferencesController, 'setAccountLabel')
|
||||
await metamaskController.connectHardware('trezor', 0).catch((e) => null)
|
||||
await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null)
|
||||
await metamaskController.connectHardware('trezor', 0, `m/44/0'/0'`).catch((e) => null)
|
||||
await metamaskController.unlockHardwareWalletAccount(accountToUnlock, 'trezor', `m/44/0'/0'`)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
metamaskController.keyringController.addNewAccount.restore()
|
||||
window.open.restore()
|
||||
metamaskController.keyringController.addNewAccount.restore()
|
||||
metamaskController.keyringController.getAccounts.restore()
|
||||
metamaskController.preferencesController.setAddresses.restore()
|
||||
metamaskController.preferencesController.setSelectedAddress.restore()
|
||||
metamaskController.preferencesController.setAccountLabel.restore()
|
||||
})
|
||||
|
||||
it('should set accountToUnlock in the keyring', async function () {
|
||||
it('should set unlockedAccount in the keyring', async function () {
|
||||
const keyrings = await metamaskController.keyringController.getKeyringsByType(
|
||||
'Trezor Hardware'
|
||||
)
|
||||
|
@ -322,7 +336,7 @@ describe('MetaMaskController', function () {
|
|||
})
|
||||
|
||||
|
||||
it('should call keyringController.addNewAccount', async function () {
|
||||
it('should call keyringController.addNewAccount', async function () {
|
||||
assert(metamaskController.keyringController.addNewAccount.calledOnce)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
const assert = require('assert')
|
||||
const ObservableStore = require('obs-store')
|
||||
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
|
||||
|
||||
describe('preferences controller', function () {
|
||||
let preferencesController
|
||||
let network
|
||||
|
||||
beforeEach(() => {
|
||||
preferencesController = new PreferencesController()
|
||||
network = {providerStore: new ObservableStore({ type: 'mainnet' })}
|
||||
preferencesController = new PreferencesController({ network })
|
||||
})
|
||||
|
||||
describe('setAddresses', function () {
|
||||
|
@ -28,6 +31,20 @@ describe('preferences controller', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('should create account tokens for each account in the store', function () {
|
||||
preferencesController.setAddresses([
|
||||
'0xda22le',
|
||||
'0x7e57e2',
|
||||
])
|
||||
|
||||
const accountTokens = preferencesController.store.getState().accountTokens
|
||||
|
||||
assert.deepEqual(accountTokens, {
|
||||
'0xda22le': {},
|
||||
'0x7e57e2': {},
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace its list of addresses', function () {
|
||||
preferencesController.setAddresses([
|
||||
'0xda22le',
|
||||
|
@ -64,6 +81,17 @@ describe('preferences controller', function () {
|
|||
assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
|
||||
})
|
||||
|
||||
it('should remove an address from state and respective tokens', function () {
|
||||
preferencesController.setAddresses([
|
||||
'0xda22le',
|
||||
'0x7e57e2',
|
||||
])
|
||||
|
||||
preferencesController.removeAddress('0xda22le')
|
||||
|
||||
assert.equal(preferencesController.store.getState().accountTokens['0xda22le'], undefined)
|
||||
})
|
||||
|
||||
it('should switch accounts if the selected address is removed', function () {
|
||||
preferencesController.setAddresses([
|
||||
'0xda22le',
|
||||
|
@ -161,6 +189,42 @@ describe('preferences controller', function () {
|
|||
await preferencesController.addToken(address, symbol, decimals, network)
|
||||
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
|
||||
})
|
||||
|
||||
it('should add token per account', async function () {
|
||||
const addressFirst = '0xabcdef1234567'
|
||||
const addressSecond = '0xabcdef1234568'
|
||||
const symbolFirst = 'ABBR'
|
||||
const symbolSecond = 'ABBB'
|
||||
const decimals = 5
|
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2')
|
||||
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
|
||||
const tokensFirstAddress = preferencesController.getTokens()
|
||||
|
||||
await preferencesController.setSelectedAddress('0xda22le')
|
||||
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
|
||||
const tokensSeconAddress = preferencesController.getTokens()
|
||||
|
||||
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two account and tokens are equal')
|
||||
})
|
||||
|
||||
it('should add token per network', async function () {
|
||||
const addressFirst = '0xabcdef1234567'
|
||||
const addressSecond = '0xabcdef1234568'
|
||||
const symbolFirst = 'ABBR'
|
||||
const symbolSecond = 'ABBB'
|
||||
const decimals = 5
|
||||
|
||||
network.providerStore.updateState({ type: 'mainnet' })
|
||||
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
|
||||
const tokensFirstAddress = preferencesController.getTokens()
|
||||
|
||||
network.providerStore.updateState({ type: 'rinkeby' })
|
||||
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
|
||||
const tokensSeconAddress = preferencesController.getTokens()
|
||||
|
||||
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two networks and tokens are equal')
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeToken', function () {
|
||||
|
@ -211,6 +275,98 @@ describe('preferences controller', function () {
|
|||
const updatedRpcList = preferencesController.getFrequentRpcList()
|
||||
assert.equal(updatedRpcList.length, 0, 'one rpc url removed')
|
||||
})
|
||||
|
||||
it('should remove a token from its state on corresponding address', async function () {
|
||||
await preferencesController.setSelectedAddress('0x7e57e2')
|
||||
await preferencesController.addToken('0xa', 'A', 4, 1)
|
||||
await preferencesController.addToken('0xb', 'B', 5, 1)
|
||||
await preferencesController.setSelectedAddress('0x7e57e3')
|
||||
await preferencesController.addToken('0xa', 'A', 4, 1)
|
||||
await preferencesController.addToken('0xb', 'B', 5, 1)
|
||||
const initialTokensSecond = preferencesController.getTokens()
|
||||
await preferencesController.setSelectedAddress('0x7e57e2')
|
||||
await preferencesController.removeToken('0xa')
|
||||
|
||||
const tokensFirst = preferencesController.getTokens()
|
||||
assert.equal(tokensFirst.length, 1, 'one token removed in account')
|
||||
|
||||
const [token1] = tokensFirst
|
||||
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5, network: 1})
|
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e3')
|
||||
const tokensSecond = preferencesController.getTokens()
|
||||
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for account')
|
||||
})
|
||||
|
||||
it('should remove a token from its state on corresponding network', async function () {
|
||||
network.providerStore.updateState({ type: 'mainnet' })
|
||||
await preferencesController.addToken('0xa', 'A', 4, 1)
|
||||
await preferencesController.addToken('0xb', 'B', 5, 1)
|
||||
network.providerStore.updateState({ type: 'rinkeby' })
|
||||
await preferencesController.addToken('0xa', 'A', 4, 1)
|
||||
await preferencesController.addToken('0xb', 'B', 5, 1)
|
||||
const initialTokensSecond = preferencesController.getTokens()
|
||||
network.providerStore.updateState({ type: 'mainnet' })
|
||||
await preferencesController.removeToken('0xa')
|
||||
|
||||
const tokensFirst = preferencesController.getTokens()
|
||||
assert.equal(tokensFirst.length, 1, 'one token removed in network')
|
||||
|
||||
const [token1] = tokensFirst
|
||||
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5, network: 1})
|
||||
|
||||
network.providerStore.updateState({ type: 'rinkeby' })
|
||||
const tokensSecond = preferencesController.getTokens()
|
||||
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for network')
|
||||
})
|
||||
})
|
||||
|
||||
describe('on setSelectedAddress', function () {
|
||||
it('should update tokens from its state on corresponding address', async function () {
|
||||
await preferencesController.setSelectedAddress('0x7e57e2')
|
||||
await preferencesController.addToken('0xa', 'A', 4)
|
||||
await preferencesController.addToken('0xb', 'B', 5)
|
||||
await preferencesController.setSelectedAddress('0x7e57e3')
|
||||
await preferencesController.addToken('0xa', 'C', 4)
|
||||
await preferencesController.addToken('0xb', 'D', 5)
|
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2')
|
||||
const initialTokensFirst = preferencesController.getTokens()
|
||||
await preferencesController.setSelectedAddress('0x7e57e3')
|
||||
const initialTokensSecond = preferencesController.getTokens()
|
||||
|
||||
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different accounts and tokens')
|
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2')
|
||||
const tokensFirst = preferencesController.getTokens()
|
||||
await preferencesController.setSelectedAddress('0x7e57e3')
|
||||
const tokensSecond = preferencesController.getTokens()
|
||||
|
||||
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same account')
|
||||
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same account')
|
||||
})
|
||||
})
|
||||
|
||||
describe('on updateStateNetworkType', function () {
|
||||
it('should remove a token from its state on corresponding network', async function () {
|
||||
network.providerStore.updateState({ type: 'mainnet' })
|
||||
await preferencesController.addToken('0xa', 'A', 4)
|
||||
await preferencesController.addToken('0xb', 'B', 5)
|
||||
const initialTokensFirst = preferencesController.getTokens()
|
||||
network.providerStore.updateState({ type: 'rinkeby' })
|
||||
await preferencesController.addToken('0xa', 'C', 4)
|
||||
await preferencesController.addToken('0xb', 'D', 5)
|
||||
const initialTokensSecond = preferencesController.getTokens()
|
||||
|
||||
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different networks and tokens')
|
||||
|
||||
network.providerStore.updateState({ type: 'mainnet' })
|
||||
const tokensFirst = preferencesController.getTokens()
|
||||
network.providerStore.updateState({ type: 'rinkeby' })
|
||||
const tokensSecond = preferencesController.getTokens()
|
||||
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same network')
|
||||
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
const assert = require('assert')
|
||||
const migration28 = require('../../../app/scripts/migrations/028')
|
||||
|
||||
const oldStorage = {
|
||||
'meta': {},
|
||||
'data': {
|
||||
'PreferencesController': {
|
||||
'tokens': [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}],
|
||||
'identities': {
|
||||
'0x6d14': {},
|
||||
'0x3695': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('migration #28', () => {
|
||||
it('should add corresponding tokens to accountTokens', (done) => {
|
||||
migration28.migrate(oldStorage)
|
||||
.then((newStorage) => {
|
||||
const newTokens = newStorage.data.PreferencesController.tokens
|
||||
const newAccountTokens = newStorage.data.PreferencesController.accountTokens
|
||||
|
||||
const testTokens = [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}]
|
||||
assert.equal(newTokens.length, 0, 'tokens is expected to have the length of 0')
|
||||
assert.equal(newAccountTokens['0x6d14']['mainnet'].length, 2, 'tokens for address is expected to have the length of 2')
|
||||
assert.equal(newAccountTokens['0x3695']['mainnet'].length, 2, 'tokens for address is expected to have the length of 2')
|
||||
assert.equal(Object.keys(newAccountTokens).length, 2, 'account tokens should be created for all identities')
|
||||
assert.deepEqual(newAccountTokens['0x6d14']['mainnet'], testTokens, 'tokens for address should be the same than before')
|
||||
assert.deepEqual(newAccountTokens['0x3695']['mainnet'], testTokens, 'tokens for address should be the same than before')
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
})
|
||||
|
||||
it('should successfully migrate first time state', (done) => {
|
||||
migration28.migrate({
|
||||
meta: {},
|
||||
data: require('../../../app/scripts/first-time-state'),
|
||||
})
|
||||
.then((migratedData) => {
|
||||
assert.equal(migratedData.meta.version, migration28.version)
|
||||
done()
|
||||
}).catch(done)
|
||||
})
|
||||
})
|
|
@ -11,6 +11,7 @@ const ethUtil = require('ethereumjs-util')
|
|||
const { fetchLocale } = require('../i18n-helper')
|
||||
const log = require('loglevel')
|
||||
const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util')
|
||||
const WebcamUtils = require('../lib/webcam-utils')
|
||||
|
||||
var actions = {
|
||||
_setBackgroundConnection: _setBackgroundConnection,
|
||||
|
@ -32,6 +33,8 @@ var actions = {
|
|||
ALERT_CLOSE: 'UI_ALERT_CLOSE',
|
||||
showAlert: showAlert,
|
||||
hideAlert: hideAlert,
|
||||
QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED',
|
||||
qrCodeDetected,
|
||||
// network dropdown open
|
||||
NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
|
||||
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
|
||||
|
@ -87,7 +90,7 @@ var actions = {
|
|||
connectHardware,
|
||||
checkHardwareStatus,
|
||||
forgetDevice,
|
||||
unlockTrezorAccount,
|
||||
unlockHardwareWalletAccount,
|
||||
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
|
||||
navigateToNewAccountScreen,
|
||||
resetAccount,
|
||||
|
@ -125,7 +128,8 @@ var actions = {
|
|||
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
|
||||
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
|
||||
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
|
||||
setCurrentCurrency: setCurrentCurrency,
|
||||
showQrScanner,
|
||||
setCurrentCurrency,
|
||||
setCurrentAccountTab,
|
||||
// account detail screen
|
||||
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
|
||||
|
@ -143,6 +147,8 @@ var actions = {
|
|||
exportAccountComplete,
|
||||
SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
|
||||
setAccountLabel,
|
||||
updateNetworkNonce,
|
||||
SET_NETWORK_NONCE: 'SET_NETWORK_NONCE',
|
||||
// tx conf screen
|
||||
COMPLETED_TX: 'COMPLETED_TX',
|
||||
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
|
||||
|
@ -233,6 +239,8 @@ var actions = {
|
|||
UPDATE_TOKENS: 'UPDATE_TOKENS',
|
||||
setRpcTarget: setRpcTarget,
|
||||
setProviderType: setProviderType,
|
||||
SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
|
||||
setHardwareWalletDefaultHdPath,
|
||||
updateProviderType,
|
||||
// loading overlay
|
||||
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
|
||||
|
@ -671,12 +679,12 @@ function addNewAccount () {
|
|||
}
|
||||
}
|
||||
|
||||
function checkHardwareStatus (deviceName) {
|
||||
log.debug(`background.checkHardwareStatus`, deviceName)
|
||||
function checkHardwareStatus (deviceName, hdPath) {
|
||||
log.debug(`background.checkHardwareStatus`, deviceName, hdPath)
|
||||
return (dispatch, getState) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
return new Promise((resolve, reject) => {
|
||||
background.checkHardwareStatus(deviceName, (err, unlocked) => {
|
||||
background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
|
@ -713,12 +721,12 @@ function forgetDevice (deviceName) {
|
|||
}
|
||||
}
|
||||
|
||||
function connectHardware (deviceName, page) {
|
||||
log.debug(`background.connectHardware`, deviceName, page)
|
||||
function connectHardware (deviceName, page, hdPath) {
|
||||
log.debug(`background.connectHardware`, deviceName, page, hdPath)
|
||||
return (dispatch, getState) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
return new Promise((resolve, reject) => {
|
||||
background.connectHardware(deviceName, page, (err, accounts) => {
|
||||
background.connectHardware(deviceName, page, hdPath, (err, accounts) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
|
@ -734,12 +742,12 @@ function connectHardware (deviceName, page) {
|
|||
}
|
||||
}
|
||||
|
||||
function unlockTrezorAccount (index) {
|
||||
log.debug(`background.unlockTrezorAccount`, index)
|
||||
function unlockHardwareWalletAccount (index, deviceName, hdPath) {
|
||||
log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath)
|
||||
return (dispatch, getState) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
return new Promise((resolve, reject) => {
|
||||
background.unlockTrezorAccount(index, (err, accounts) => {
|
||||
background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
|
@ -759,6 +767,28 @@ function showInfoPage () {
|
|||
}
|
||||
}
|
||||
|
||||
function showQrScanner (ROUTE) {
|
||||
return (dispatch, getState) => {
|
||||
return WebcamUtils.checkStatus()
|
||||
.then(status => {
|
||||
if (!status.environmentReady) {
|
||||
// We need to switch to fullscreen mode to ask for permission
|
||||
global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`)
|
||||
} else {
|
||||
dispatch(actions.showModal({
|
||||
name: 'QR_SCANNER',
|
||||
}))
|
||||
}
|
||||
}).catch(e => {
|
||||
dispatch(actions.showModal({
|
||||
name: 'QR_SCANNER',
|
||||
error: true,
|
||||
errorType: e.type,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentCurrency (currencyCode) {
|
||||
return (dispatch) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
@ -1516,11 +1546,12 @@ function showAccountDetail (address) {
|
|||
return (dispatch) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
log.debug(`background.setSelectedAddress`)
|
||||
background.setSelectedAddress(address, (err) => {
|
||||
background.setSelectedAddress(address, (err, tokens) => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
if (err) {
|
||||
return dispatch(actions.displayWarning(err.message))
|
||||
}
|
||||
dispatch(updateTokens(tokens))
|
||||
dispatch({
|
||||
type: actions.SHOW_ACCOUNT_DETAIL,
|
||||
value: address,
|
||||
|
@ -1872,6 +1903,17 @@ function hideAlert () {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action will receive two types of values via qrCodeData
|
||||
* an object with the following structure {type, values}
|
||||
* or null (used to clear the previous value)
|
||||
*/
|
||||
function qrCodeDetected (qrCodeData) {
|
||||
return {
|
||||
type: actions.QR_CODE_DETECTED,
|
||||
value: qrCodeData,
|
||||
}
|
||||
}
|
||||
|
||||
function showLoadingIndication (message) {
|
||||
return {
|
||||
|
@ -1880,6 +1922,13 @@ function showLoadingIndication (message) {
|
|||
}
|
||||
}
|
||||
|
||||
function setHardwareWalletDefaultHdPath ({ device, path }) {
|
||||
return {
|
||||
type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
|
||||
value: {device, path},
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoadingIndication () {
|
||||
return {
|
||||
type: actions.HIDE_LOADING,
|
||||
|
@ -2183,6 +2232,24 @@ function updateFeatureFlags (updatedFeatureFlags) {
|
|||
}
|
||||
}
|
||||
|
||||
function setNetworkNonce (networkNonce) {
|
||||
return {
|
||||
type: actions.SET_NETWORK_NONCE,
|
||||
value: networkNonce,
|
||||
}
|
||||
}
|
||||
|
||||
function updateNetworkNonce (address) {
|
||||
return (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
global.ethQuery.getTransactionCount(address, (err, data) => {
|
||||
dispatch(setNetworkNonce(data))
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setMouseUserState (isMouseUser) {
|
||||
return {
|
||||
type: actions.SET_MOUSE_USER_STATE,
|
||||
|
|
|
@ -229,6 +229,7 @@ AccountMenu.prototype.renderKeyringType = function (keyring) {
|
|||
let label
|
||||
switch (type) {
|
||||
case 'Trezor Hardware':
|
||||
case 'Ledger Hardware':
|
||||
label = this.context.t('hardware')
|
||||
break
|
||||
case 'Simple Key Pair':
|
||||
|
|
|
@ -15,14 +15,21 @@
|
|||
&__details {
|
||||
flex: 1;
|
||||
text-align: end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__fiat {
|
||||
font-size: 1.5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__eth {
|
||||
color: $oslo-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__header-text {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import ConfirmDetailRow from '../confirm-detail-row.component.js'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const propsMethodSpies = {
|
||||
onHeaderClick: sinon.spy(),
|
||||
}
|
||||
|
||||
describe('Confirm Detail Row Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ConfirmDetailRow
|
||||
errorType={'mockErrorType'}
|
||||
label={'mockLabel'}
|
||||
showError={false}
|
||||
fiatText = {'mockFiatText'}
|
||||
ethText = {'mockEthText'}
|
||||
fiatTextColor= {'mockColor'}
|
||||
onHeaderClick= {propsMethodSpies.onHeaderClick}
|
||||
headerText = {'mockHeaderText'}
|
||||
headerTextClassName = {'mockHeaderClass'}
|
||||
/>)
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a div with a confirm-detail-row class', () => {
|
||||
assert.equal(wrapper.find('div.confirm-detail-row').length, 1)
|
||||
})
|
||||
|
||||
it('should render the label as a child of the confirm-detail-row__label', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row > .confirm-detail-row__label').childAt(0).text(), 'mockLabel')
|
||||
})
|
||||
|
||||
it('should render the headerText as a child of the confirm-detail-row__header-text', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText')
|
||||
})
|
||||
|
||||
it('should render the fiatText as a child of the confirm-detail-row__fiat', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText')
|
||||
})
|
||||
|
||||
it('should render the ethText as a child of the confirm-detail-row__eth', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText')
|
||||
})
|
||||
|
||||
it('should set the fiatTextColor on confirm-detail-row__fiat', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor')
|
||||
})
|
||||
|
||||
it('should assure the confirm-detail-row__header-text classname is correct', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__header-text').props().className, 'confirm-detail-row__header-text mockHeaderClass')
|
||||
})
|
||||
|
||||
it('should call onHeaderClick when headerText div gets clicked', () => {
|
||||
wrapper.find('.confirm-detail-row__header-text').props().onClick()
|
||||
assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1))
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
|
@ -43,7 +43,7 @@ export default class ConfirmPageContainer extends Component {
|
|||
// Footer
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
valid: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -54,7 +54,7 @@ export default class ConfirmPageContainer extends Component {
|
|||
fromAddress,
|
||||
toName,
|
||||
toAddress,
|
||||
valid,
|
||||
disabled,
|
||||
errorKey,
|
||||
errorMessage,
|
||||
contentComponent,
|
||||
|
@ -110,7 +110,7 @@ export default class ConfirmPageContainer extends Component {
|
|||
onSubmit={() => onSubmit()}
|
||||
submitText={this.context.t('confirm')}
|
||||
submitButtonType="confirm"
|
||||
disabled={!valid}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -87,7 +87,6 @@ class DropdownMenuItem extends Component {
|
|||
padding: '8px 0px',
|
||||
fontSize: '18px',
|
||||
fontStyle: 'normal',
|
||||
fontFamily: 'Montserrat Regular',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
|
|
|
@ -71,7 +71,6 @@ NetworkDropdown.prototype.render = function () {
|
|||
const rpcList = props.frequentRpcList
|
||||
const isOpen = this.props.networkDropdownOpen
|
||||
const dropdownMenuItemStyle = {
|
||||
fontFamily: 'DIN OT',
|
||||
fontSize: '16px',
|
||||
lineHeight: '20px',
|
||||
padding: '12px 0',
|
||||
|
@ -310,7 +309,6 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
|
|||
closeMenu: () => this.props.hideNetworkDropdown(),
|
||||
onClick: () => props.setRpcTarget(rpc),
|
||||
style: {
|
||||
fontFamily: 'DIN OT',
|
||||
fontSize: '16px',
|
||||
lineHeight: '20px',
|
||||
padding: '12px 0',
|
||||
|
@ -349,7 +347,6 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) {
|
|||
onClick: () => props.setRpcTarget(rpcTarget),
|
||||
closeMenu: () => this.props.hideNetworkDropdown(),
|
||||
style: {
|
||||
fontFamily: 'DIN OT',
|
||||
fontSize: '16px',
|
||||
lineHeight: '20px',
|
||||
padding: '12px 0',
|
||||
|
|
|
@ -27,6 +27,7 @@ function EnsInput () {
|
|||
}
|
||||
|
||||
EnsInput.prototype.onChange = function (recipient) {
|
||||
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
|
||||
|
@ -54,6 +55,7 @@ EnsInput.prototype.render = function () {
|
|||
const opts = extend(props, {
|
||||
list: 'addresses',
|
||||
onChange: this.onChange.bind(this),
|
||||
qrScanner: true,
|
||||
})
|
||||
return h('div', {
|
||||
style: { width: '100%', position: 'relative' },
|
||||
|
|
|
@ -14,6 +14,7 @@ function mapStateToProps (state) {
|
|||
return {
|
||||
network: state.metamask.network,
|
||||
selectedIdentity: getSelectedIdentity(state),
|
||||
keyrings: state.metamask.keyrings,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,9 +51,20 @@ AccountDetailsModal.prototype.render = function () {
|
|||
network,
|
||||
showExportPrivateKeyModal,
|
||||
setAccountLabel,
|
||||
keyrings,
|
||||
} = this.props
|
||||
const { name, address } = selectedIdentity
|
||||
|
||||
const keyring = keyrings.find((kr) => {
|
||||
return kr.accounts.includes(address)
|
||||
})
|
||||
|
||||
let exportPrivateKeyFeatureEnabled = true
|
||||
// This feature is disabled for hardware wallets
|
||||
if (keyring.type.search('Hardware') !== -1) {
|
||||
exportPrivateKeyFeatureEnabled = false
|
||||
}
|
||||
|
||||
return h(AccountModalContainer, {}, [
|
||||
h(EditableLabel, {
|
||||
className: 'account-modal__name',
|
||||
|
@ -73,9 +85,9 @@ AccountDetailsModal.prototype.render = function () {
|
|||
}, this.context.t('etherscanView')),
|
||||
|
||||
// Holding on redesign for Export Private Key functionality
|
||||
h('button.btn-primary.account-modal__button', {
|
||||
exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', {
|
||||
onClick: () => showExportPrivateKeyModal(),
|
||||
}, this.context.t('exportPrivateKey')),
|
||||
}, this.context.t('exportPrivateKey')) : null,
|
||||
|
||||
])
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@import './customize-gas/index';
|
||||
|
||||
@import './qr-scanner/index';
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -21,6 +21,7 @@ const CustomizeGasModal = require('../customize-gas-modal')
|
|||
const NotifcationModal = require('./notification-modal')
|
||||
const ConfirmResetAccount = require('./confirm-reset-account')
|
||||
const ConfirmRemoveAccount = require('./confirm-remove-account')
|
||||
const QRScanner = require('./qr-scanner')
|
||||
const TransactionConfirmed = require('./transaction-confirmed')
|
||||
const WelcomeBeta = require('./welcome-beta')
|
||||
const Notification = require('./notification')
|
||||
|
@ -346,6 +347,18 @@ const MODALS = {
|
|||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
QR_SCANNER: {
|
||||
contents: h(QRScanner),
|
||||
mobileModalStyle: {
|
||||
...modalContainerMobileStyle,
|
||||
},
|
||||
laptopModalStyle: {
|
||||
...modalContainerLaptopStyle,
|
||||
},
|
||||
contentStyle: {
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
|
||||
DEFAULT: {
|
||||
contents: [],
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import QrScanner from './qr-scanner.container'
|
||||
module.exports = QrScanner
|
|
@ -0,0 +1,83 @@
|
|||
.qr-scanner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-radius: 8px;
|
||||
|
||||
&__title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
|
||||
&__video-wrapper {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 275px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
video {
|
||||
transform: scaleX(-1);
|
||||
width: auto;
|
||||
height: 275px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
padding: 16px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__error {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 20px;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
button:last-of-type {
|
||||
margin-right: 0;
|
||||
background-color: #009eec;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__close::after {
|
||||
content: '\00D7';
|
||||
font-size: 35px;
|
||||
color: #9b9b9b;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 20px;
|
||||
cursor: pointer;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { BrowserQRCodeReader } from '@zxing/library'
|
||||
import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars
|
||||
import Spinner from '../../spinner'
|
||||
import WebcamUtils from '../../../../lib/webcam-utils'
|
||||
import PageContainerFooter from '../../page-container/page-container-footer/page-container-footer.component'
|
||||
|
||||
export default class QrScanner extends Component {
|
||||
static propTypes = {
|
||||
hideModal: PropTypes.func.isRequired,
|
||||
qrCodeDetected: PropTypes.func,
|
||||
scanQrCode: PropTypes.func,
|
||||
error: PropTypes.bool,
|
||||
errorType: PropTypes.string,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor (props, context) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
ready: false,
|
||||
msg: context.t('accessingYourCamera'),
|
||||
}
|
||||
this.codeReader = null
|
||||
this.permissionChecker = null
|
||||
this.needsToReinit = false
|
||||
|
||||
// Clear pre-existing qr code data before scanning
|
||||
this.props.qrCodeDetected(null)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.initCamera()
|
||||
}
|
||||
|
||||
async checkPermisisions () {
|
||||
const { permissions } = await WebcamUtils.checkStatus()
|
||||
if (permissions) {
|
||||
clearTimeout(this.permissionChecker)
|
||||
// Let the video stream load first...
|
||||
setTimeout(_ => {
|
||||
this.setState({
|
||||
ready: true,
|
||||
msg: this.context.t('scanInstructions'),
|
||||
})
|
||||
if (this.needsToReinit) {
|
||||
this.initCamera()
|
||||
this.needsToReinit = false
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
// Keep checking for permissions
|
||||
this.permissionChecker = setTimeout(_ => {
|
||||
this.checkPermisisions()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this.permissionChecker)
|
||||
if (this.codeReader) {
|
||||
this.codeReader.reset()
|
||||
}
|
||||
}
|
||||
|
||||
initCamera () {
|
||||
this.codeReader = new BrowserQRCodeReader()
|
||||
this.codeReader.getVideoInputDevices()
|
||||
.then(videoInputDevices => {
|
||||
clearTimeout(this.permissionChecker)
|
||||
this.checkPermisisions()
|
||||
this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
|
||||
.then(content => {
|
||||
const result = this.parseContent(content.text)
|
||||
if (result.type !== 'unknown') {
|
||||
this.props.qrCodeDetected(result)
|
||||
this.stopAndClose()
|
||||
} else {
|
||||
this.setState({msg: this.context.t('unknownQrCode')})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.name === 'NotAllowedError') {
|
||||
this.setState({msg: this.context.t('youNeedToAllowCameraAccess')})
|
||||
clearTimeout(this.permissionChecker)
|
||||
this.needsToReinit = true
|
||||
this.checkPermisisions()
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err)
|
||||
})
|
||||
}
|
||||
|
||||
parseContent (content) {
|
||||
let type = 'unknown'
|
||||
let values = {}
|
||||
|
||||
// Here we could add more cases
|
||||
// To parse other type of links
|
||||
// For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
|
||||
|
||||
|
||||
// Ethereum address links - fox ex. ethereum:0x.....1111
|
||||
if (content.split('ethereum:').length > 1) {
|
||||
|
||||
type = 'address'
|
||||
values = {'address': content.split('ethereum:')[1] }
|
||||
|
||||
// Regular ethereum addresses - fox ex. 0x.....1111
|
||||
} else if (content.substring(0, 2).toLowerCase() === '0x') {
|
||||
|
||||
type = 'address'
|
||||
values = {'address': content }
|
||||
|
||||
}
|
||||
return {type, values}
|
||||
}
|
||||
|
||||
|
||||
stopAndClose = () => {
|
||||
if (this.codeReader) {
|
||||
this.codeReader.reset()
|
||||
}
|
||||
this.setState({ ready: false })
|
||||
this.props.hideModal()
|
||||
}
|
||||
|
||||
tryAgain = () => {
|
||||
// close the modal
|
||||
this.stopAndClose()
|
||||
// wait for the animation and try again
|
||||
setTimeout(_ => {
|
||||
this.props.scanQrCode()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
renderVideo () {
|
||||
return (
|
||||
<div className={'qr-scanner__content__video-wrapper'}>
|
||||
<video
|
||||
id="video"
|
||||
style={{
|
||||
display: this.state.ready ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
{ !this.state.ready ? <Spinner color={'#F7C06C'} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderErrorModal () {
|
||||
let title, msg
|
||||
|
||||
if (this.props.error) {
|
||||
if (this.props.errorType === 'NO_WEBCAM_FOUND') {
|
||||
title = this.context.t('noWebcamFoundTitle')
|
||||
msg = this.context.t('noWebcamFound')
|
||||
} else {
|
||||
title = this.context.t('unknownCameraErrorTitle')
|
||||
msg = this.context.t('unknownCameraError')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
<div className="qr-scanner__close" onClick={this.stopAndClose}></div>
|
||||
|
||||
<div className="qr-scanner__image">
|
||||
<img src={'images/webcam.svg'} width={70} height={70} />
|
||||
</div>
|
||||
<div className="qr-scanner__title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className={'qr-scanner__error'}>
|
||||
{msg}
|
||||
</div>
|
||||
<PageContainerFooter
|
||||
onCancel={this.stopAndClose}
|
||||
onSubmit={this.tryAgain}
|
||||
cancelText={this.context.t('cancel')}
|
||||
submitText={this.context.t('tryAgain')}
|
||||
submitButtonType="confirm"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
|
||||
if (this.props.error) {
|
||||
return this.renderErrorModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
<div className="qr-scanner__close" onClick={this.stopAndClose}></div>
|
||||
<div className="qr-scanner__title">
|
||||
{ `${t('scanQrCode')}` }
|
||||
</div>
|
||||
<div className="qr-scanner__content">
|
||||
{ this.renderVideo() }
|
||||
</div>
|
||||
<div className={'qr-scanner__status'}>
|
||||
{this.state.msg}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux'
|
||||
import QrScanner from './qr-scanner.component'
|
||||
|
||||
const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions')
|
||||
import {
|
||||
SEND_ROUTE,
|
||||
} from '../../../routes'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
error: state.appState.modal.modalState.props.error,
|
||||
errorType: state.appState.modal.modalState.props.errorType,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
hideModal: () => dispatch(hideModal()),
|
||||
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
|
||||
scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QrScanner)
|
|
@ -5,6 +5,7 @@ import {
|
|||
formatCurrency,
|
||||
convertTokenToFiat,
|
||||
addFiat,
|
||||
roundExponential,
|
||||
} from '../../../helpers/confirm-transaction/util'
|
||||
|
||||
export default class ConfirmTokenTransactionBase extends Component {
|
||||
|
@ -42,7 +43,8 @@ export default class ConfirmTokenTransactionBase extends Component {
|
|||
return this.context.t('noConversionRateAvailable')
|
||||
} else {
|
||||
const fiatTransactionAmount = this.getFiatTransactionAmount()
|
||||
return formatCurrency(fiatTransactionAmount, currentCurrency)
|
||||
const roundedFiatTransactionAmount = roundExponential(fiatTransactionAmount)
|
||||
return formatCurrency(roundedFiatTransactionAmount, currentCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +56,8 @@ export default class ConfirmTokenTransactionBase extends Component {
|
|||
} else {
|
||||
const fiatTransactionAmount = this.getFiatTransactionAmount()
|
||||
const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
|
||||
return formatCurrency(fiatTotal, currentCurrency)
|
||||
const roundedFiatTotal = roundExponential(fiatTotal)
|
||||
return formatCurrency(roundedFiatTotal, currentCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,10 @@ export default class ConfirmTransactionBase extends Component {
|
|||
warning: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
submitting: false,
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const {
|
||||
transactionStatus,
|
||||
|
@ -120,7 +124,7 @@ export default class ConfirmTransactionBase extends Component {
|
|||
|
||||
if (simulationFails) {
|
||||
return {
|
||||
valid: false,
|
||||
valid: true,
|
||||
errorKey: TRANSACTION_ERROR_KEY,
|
||||
}
|
||||
}
|
||||
|
@ -258,15 +262,25 @@ export default class ConfirmTransactionBase extends Component {
|
|||
|
||||
handleSubmit () {
|
||||
const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
|
||||
const { submitting } = this.state
|
||||
|
||||
if (submitting) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ submitting: true })
|
||||
|
||||
if (onSubmit) {
|
||||
onSubmit(txData)
|
||||
Promise.resolve(onSubmit(txData))
|
||||
.then(this.setState({ submitting: false }))
|
||||
} else {
|
||||
sendTransaction(txData)
|
||||
.then(() => {
|
||||
clearConfirmTransaction()
|
||||
this.setState({ submitting: false })
|
||||
history.push(DEFAULT_ROUTE)
|
||||
})
|
||||
.catch(() => this.setState({ submitting: false }))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,7 +294,7 @@ export default class ConfirmTransactionBase extends Component {
|
|||
methodData,
|
||||
ethTransactionAmount,
|
||||
fiatTransactionAmount,
|
||||
valid: propsValid,
|
||||
valid: propsValid = true,
|
||||
errorMessage,
|
||||
errorKey: propsErrorKey,
|
||||
currentCurrency,
|
||||
|
@ -295,6 +309,7 @@ export default class ConfirmTransactionBase extends Component {
|
|||
nonce,
|
||||
warning,
|
||||
} = this.props
|
||||
const { submitting } = this.state
|
||||
|
||||
const { name } = methodData
|
||||
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
|
||||
|
@ -320,7 +335,7 @@ export default class ConfirmTransactionBase extends Component {
|
|||
errorMessage={errorMessage}
|
||||
errorKey={propsErrorKey || errorKey}
|
||||
warning={warning}
|
||||
valid={propsValid || valid}
|
||||
disabled={!propsValid || !valid || submitting}
|
||||
onEdit={() => this.handleEdit()}
|
||||
onCancel={() => this.handleCancel()}
|
||||
onSubmit={() => this.handleSubmit()}
|
||||
|
|
|
@ -2,16 +2,75 @@ const { Component } = require('react')
|
|||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const ethNetProps = require('eth-net-props')
|
||||
const Select = require('react-select').default
|
||||
|
||||
class AccountList extends Component {
|
||||
constructor (props, context) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
getHdPaths () {
|
||||
return [
|
||||
{
|
||||
label: `Ledger Live`,
|
||||
value: `m/44'/60'/0'/0/0`,
|
||||
},
|
||||
{
|
||||
label: `Legacy (MEW / MyCrypto)`,
|
||||
value: `m/44'/60'/0'`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
goToNextPage = () => {
|
||||
// If we have < 5 accounts, it's restricted by BIP-44
|
||||
if (this.props.accounts.length === 5) {
|
||||
this.props.getPage(this.props.device, 1, this.props.selectedPath)
|
||||
} else {
|
||||
this.props.onAccountRestriction()
|
||||
}
|
||||
}
|
||||
|
||||
goToPreviousPage = () => {
|
||||
this.props.getPage(this.props.device, -1, this.props.selectedPath)
|
||||
}
|
||||
|
||||
renderHdPathSelector () {
|
||||
const { onPathChange, selectedPath } = this.props
|
||||
|
||||
const options = this.getHdPaths()
|
||||
return h('div', [
|
||||
h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')),
|
||||
h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')),
|
||||
h('div.hw-connect__hdPath', [
|
||||
h(Select, {
|
||||
className: 'hw-connect__hdPath__select',
|
||||
name: 'hd-path-select',
|
||||
clearable: false,
|
||||
value: selectedPath,
|
||||
options,
|
||||
onChange: (opt) => {
|
||||
onPathChange(opt.value)
|
||||
},
|
||||
}),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
capitalizeDevice (device) {
|
||||
return device.slice(0, 1).toUpperCase() + device.slice(1)
|
||||
}
|
||||
|
||||
renderHeader () {
|
||||
const { device } = this.props
|
||||
return (
|
||||
h('div.hw-connect', [
|
||||
h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')),
|
||||
|
||||
h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`),
|
||||
|
||||
device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null,
|
||||
|
||||
h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')),
|
||||
h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
|
||||
])
|
||||
)
|
||||
|
@ -61,7 +120,7 @@ class AccountList extends Component {
|
|||
h(
|
||||
'button.hw-list-pagination__button',
|
||||
{
|
||||
onClick: () => this.props.getPage(-1),
|
||||
onClick: this.goToPreviousPage,
|
||||
},
|
||||
`< ${this.context.t('prev')}`
|
||||
),
|
||||
|
@ -69,7 +128,7 @@ class AccountList extends Component {
|
|||
h(
|
||||
'button.hw-list-pagination__button',
|
||||
{
|
||||
onClick: () => this.props.getPage(1),
|
||||
onClick: this.goToNextPage,
|
||||
},
|
||||
`${this.context.t('next')} >`
|
||||
),
|
||||
|
@ -95,7 +154,7 @@ class AccountList extends Component {
|
|||
h(
|
||||
`button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
|
||||
{
|
||||
onClick: this.props.onUnlockAccount.bind(this),
|
||||
onClick: this.props.onUnlockAccount.bind(this, this.props.device),
|
||||
...buttonProps,
|
||||
},
|
||||
[this.context.t('unlock')]
|
||||
|
@ -106,7 +165,7 @@ class AccountList extends Component {
|
|||
renderForgetDevice () {
|
||||
return h('div.hw-forget-device-container', {}, [
|
||||
h('a', {
|
||||
onClick: this.props.onForgetDevice.bind(this),
|
||||
onClick: this.props.onForgetDevice.bind(this, this.props.device),
|
||||
}, this.context.t('forgetDevice')),
|
||||
])
|
||||
}
|
||||
|
@ -125,6 +184,9 @@ class AccountList extends Component {
|
|||
|
||||
|
||||
AccountList.propTypes = {
|
||||
onPathChange: PropTypes.func.isRequired,
|
||||
selectedPath: PropTypes.string.isRequired,
|
||||
device: PropTypes.string.isRequired,
|
||||
accounts: PropTypes.array.isRequired,
|
||||
onAccountChange: PropTypes.func.isRequired,
|
||||
onForgetDevice: PropTypes.func.isRequired,
|
||||
|
@ -134,6 +196,7 @@ AccountList.propTypes = {
|
|||
history: PropTypes.object,
|
||||
onUnlockAccount: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onAccountRestriction: PropTypes.func,
|
||||
}
|
||||
|
||||
AccountList.contextTypes = {
|
||||
|
|
|
@ -5,6 +5,52 @@ const h = require('react-hyperscript')
|
|||
class ConnectScreen extends Component {
|
||||
constructor (props, context) {
|
||||
super(props)
|
||||
this.state = {
|
||||
selectedDevice: null,
|
||||
}
|
||||
}
|
||||
|
||||
connect = () => {
|
||||
if (this.state.selectedDevice) {
|
||||
this.props.connectToHardwareWallet(this.state.selectedDevice)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
renderConnectToTrezorButton () {
|
||||
return h(
|
||||
`button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`,
|
||||
{ onClick: _ => this.setState({selectedDevice: 'trezor'}) },
|
||||
h('img.hw-connect__btn__img', {
|
||||
src: 'images/trezor-logo.svg',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
renderConnectToLedgerButton () {
|
||||
return h(
|
||||
`button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`,
|
||||
{ onClick: _ => this.setState({selectedDevice: 'ledger'}) },
|
||||
h('img.hw-connect__btn__img', {
|
||||
src: 'images/ledger-logo.svg',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
renderButtons () {
|
||||
return (
|
||||
h('div', {}, [
|
||||
h('div.hw-connect__btn-wrapper', {}, [
|
||||
this.renderConnectToLedgerButton(),
|
||||
this.renderConnectToTrezorButton(),
|
||||
]),
|
||||
h(
|
||||
`button.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`,
|
||||
{ onClick: this.connect },
|
||||
this.context.t('connect')
|
||||
),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
renderUnsupportedBrowser () {
|
||||
|
@ -12,7 +58,7 @@ class ConnectScreen extends Component {
|
|||
h('div.new-account-connect-form.unsupported-browser', {}, [
|
||||
h('div.hw-connect', [
|
||||
h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
|
||||
h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')),
|
||||
h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
|
||||
]),
|
||||
h(
|
||||
'button.btn-primary.btn--large',
|
||||
|
@ -30,29 +76,31 @@ class ConnectScreen extends Component {
|
|||
renderHeader () {
|
||||
return (
|
||||
h('div.hw-connect__header', {}, [
|
||||
h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)),
|
||||
h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)),
|
||||
h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)),
|
||||
h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
getAffiliateLinks () {
|
||||
const links = {
|
||||
trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`,
|
||||
ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`,
|
||||
}
|
||||
|
||||
const text = this.context.t('orderOneHere')
|
||||
const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger)
|
||||
|
||||
return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }})
|
||||
}
|
||||
|
||||
renderTrezorAffiliateLink () {
|
||||
return h('div.hw-connect__get-trezor', {}, [
|
||||
h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)),
|
||||
h('a.hw-connect__get-trezor__link', {
|
||||
href: 'https://shop.trezor.io/?a=metamask',
|
||||
target: '_blank',
|
||||
}, this.context.t('orderOneHere')),
|
||||
return h('div.hw-connect__get-hw', {}, [
|
||||
h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)),
|
||||
this.getAffiliateLinks(),
|
||||
])
|
||||
}
|
||||
|
||||
renderConnectToTrezorButton () {
|
||||
return h(
|
||||
'button.btn-primary.btn--large',
|
||||
{ onClick: this.props.connectToTrezor.bind(this) },
|
||||
this.props.btnText
|
||||
)
|
||||
}
|
||||
|
||||
scrollToTutorial = (e) => {
|
||||
if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
|
||||
|
@ -102,7 +150,7 @@ class ConnectScreen extends Component {
|
|||
return (
|
||||
h('div.hw-connect__footer', {}, [
|
||||
h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
|
||||
this.renderConnectToTrezorButton(),
|
||||
this.renderButtons(),
|
||||
h('p.hw-connect__footer__msg', {}, [
|
||||
this.context.t(`havingTroubleConnecting`),
|
||||
h('a.hw-connect__footer__link', {
|
||||
|
@ -118,8 +166,8 @@ class ConnectScreen extends Component {
|
|||
return (
|
||||
h('div.new-account-connect-form', {}, [
|
||||
this.renderHeader(),
|
||||
this.renderButtons(),
|
||||
this.renderTrezorAffiliateLink(),
|
||||
this.renderConnectToTrezorButton(),
|
||||
this.renderLearnMore(),
|
||||
this.renderTutorialSteps(),
|
||||
this.renderFooter(),
|
||||
|
@ -136,8 +184,7 @@ class ConnectScreen extends Component {
|
|||
}
|
||||
|
||||
ConnectScreen.propTypes = {
|
||||
connectToTrezor: PropTypes.func.isRequired,
|
||||
btnText: PropTypes.string.isRequired,
|
||||
connectToHardwareWallet: PropTypes.func.isRequired,
|
||||
browserSupported: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -7,17 +7,19 @@ const ConnectScreen = require('./connect-screen')
|
|||
const AccountList = require('./account-list')
|
||||
const { DEFAULT_ROUTE } = require('../../../../routes')
|
||||
const { formatBalance } = require('../../../../util')
|
||||
const { getPlatform } = require('../../../../../../app/scripts/lib/util')
|
||||
const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums')
|
||||
|
||||
class ConnectHardwareForm extends Component {
|
||||
constructor (props, context) {
|
||||
super(props)
|
||||
this.state = {
|
||||
error: null,
|
||||
btnText: context.t('connectToTrezor'),
|
||||
selectedAccount: null,
|
||||
accounts: [],
|
||||
browserSupported: true,
|
||||
unlocked: false,
|
||||
device: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,25 +40,44 @@ class ConnectHardwareForm extends Component {
|
|||
}
|
||||
|
||||
async checkIfUnlocked () {
|
||||
const unlocked = await this.props.checkHardwareStatus('trezor')
|
||||
if (unlocked) {
|
||||
this.setState({unlocked: true})
|
||||
this.getPage(0)
|
||||
}
|
||||
['trezor', 'ledger'].forEach(async device => {
|
||||
const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device])
|
||||
if (unlocked) {
|
||||
this.setState({unlocked: true})
|
||||
this.getPage(device, 0, this.props.defaultHdPaths[device])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connectToTrezor = () => {
|
||||
connectToHardwareWallet = (device) => {
|
||||
// None of the hardware wallets are supported
|
||||
// At least for now
|
||||
if (getPlatform() === PLATFORM_FIREFOX) {
|
||||
this.setState({ browserSupported: false, error: null})
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.state.accounts.length) {
|
||||
return null
|
||||
}
|
||||
this.setState({ btnText: this.context.t('connecting')})
|
||||
this.getPage(0)
|
||||
|
||||
// Default values
|
||||
this.getPage(device, 0, this.props.defaultHdPaths[device])
|
||||
}
|
||||
|
||||
onPathChange = (path) => {
|
||||
this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path})
|
||||
this.getPage(this.state.device, 0, path)
|
||||
}
|
||||
|
||||
onAccountChange = (account) => {
|
||||
this.setState({selectedAccount: account.toString(), error: null})
|
||||
}
|
||||
|
||||
onAccountRestriction = () => {
|
||||
this.setState({error: this.context.t('ledgerAccountRestriction') })
|
||||
}
|
||||
|
||||
showTemporaryAlert () {
|
||||
this.props.showAlert(this.context.t('hardwareWalletConnected'))
|
||||
// Autohide the alert after 5 seconds
|
||||
|
@ -65,9 +86,9 @@ class ConnectHardwareForm extends Component {
|
|||
}, 5000)
|
||||
}
|
||||
|
||||
getPage = (page) => {
|
||||
getPage = (device, page, hdPath) => {
|
||||
this.props
|
||||
.connectHardware('trezor', page)
|
||||
.connectHardware(device, page, hdPath)
|
||||
.then(accounts => {
|
||||
if (accounts.length) {
|
||||
|
||||
|
@ -77,7 +98,7 @@ class ConnectHardwareForm extends Component {
|
|||
this.showTemporaryAlert()
|
||||
}
|
||||
|
||||
const newState = { unlocked: true }
|
||||
const newState = { unlocked: true, device, error: null }
|
||||
// Default to the first account
|
||||
if (this.state.selectedAccount === null) {
|
||||
accounts.forEach((a, i) => {
|
||||
|
@ -104,18 +125,18 @@ class ConnectHardwareForm extends Component {
|
|||
})
|
||||
.catch(e => {
|
||||
if (e === 'Window blocked') {
|
||||
this.setState({ browserSupported: false })
|
||||
this.setState({ browserSupported: false, error: null})
|
||||
} else if (e !== 'Window closed') {
|
||||
this.setState({ error: e.toString() })
|
||||
}
|
||||
this.setState({ btnText: this.context.t('connectToTrezor') })
|
||||
})
|
||||
}
|
||||
|
||||
onForgetDevice = () => {
|
||||
this.props.forgetDevice('trezor')
|
||||
onForgetDevice = (device) => {
|
||||
this.props.forgetDevice(device)
|
||||
.then(_ => {
|
||||
this.setState({
|
||||
error: null,
|
||||
btnText: this.context.t('connectToTrezor'),
|
||||
selectedAccount: null,
|
||||
accounts: [],
|
||||
unlocked: false,
|
||||
|
@ -125,13 +146,13 @@ class ConnectHardwareForm extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
onUnlockAccount = () => {
|
||||
onUnlockAccount = (device) => {
|
||||
|
||||
if (this.state.selectedAccount === null) {
|
||||
this.setState({ error: this.context.t('accountSelectionRequired') })
|
||||
}
|
||||
|
||||
this.props.unlockTrezorAccount(this.state.selectedAccount)
|
||||
this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
|
||||
.then(_ => {
|
||||
this.props.history.push(DEFAULT_ROUTE)
|
||||
}).catch(e => {
|
||||
|
@ -145,20 +166,22 @@ class ConnectHardwareForm extends Component {
|
|||
|
||||
renderError () {
|
||||
return this.state.error
|
||||
? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
|
||||
? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error)
|
||||
: null
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
if (!this.state.accounts.length) {
|
||||
return h(ConnectScreen, {
|
||||
connectToTrezor: this.connectToTrezor,
|
||||
btnText: this.state.btnText,
|
||||
connectToHardwareWallet: this.connectToHardwareWallet,
|
||||
browserSupported: this.state.browserSupported,
|
||||
})
|
||||
}
|
||||
|
||||
return h(AccountList, {
|
||||
onPathChange: this.onPathChange,
|
||||
selectedPath: this.props.defaultHdPaths[this.state.device],
|
||||
device: this.state.device,
|
||||
accounts: this.state.accounts,
|
||||
selectedAccount: this.state.selectedAccount,
|
||||
onAccountChange: this.onAccountChange,
|
||||
|
@ -168,6 +191,7 @@ class ConnectHardwareForm extends Component {
|
|||
onUnlockAccount: this.onUnlockAccount,
|
||||
onForgetDevice: this.onForgetDevice,
|
||||
onCancel: this.onCancel,
|
||||
onAccountRestriction: this.onAccountRestriction,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -188,13 +212,15 @@ ConnectHardwareForm.propTypes = {
|
|||
forgetDevice: PropTypes.func,
|
||||
showAlert: PropTypes.func,
|
||||
hideAlert: PropTypes.func,
|
||||
unlockTrezorAccount: PropTypes.func,
|
||||
unlockHardwareWalletAccount: PropTypes.func,
|
||||
setHardwareWalletDefaultHdPath: PropTypes.func,
|
||||
numberOfExistingAccounts: PropTypes.number,
|
||||
history: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
network: PropTypes.string,
|
||||
accounts: PropTypes.object,
|
||||
address: PropTypes.string,
|
||||
defaultHdPaths: PropTypes.object,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
@ -202,28 +228,35 @@ const mapStateToProps = state => {
|
|||
metamask: { network, selectedAddress, identities = {}, accounts = [] },
|
||||
} = state
|
||||
const numberOfExistingAccounts = Object.keys(identities).length
|
||||
const {
|
||||
appState: { defaultHdPaths },
|
||||
} = state
|
||||
|
||||
return {
|
||||
network,
|
||||
accounts,
|
||||
address: selectedAddress,
|
||||
numberOfExistingAccounts,
|
||||
defaultHdPaths,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
connectHardware: (deviceName, page) => {
|
||||
return dispatch(actions.connectHardware(deviceName, page))
|
||||
setHardwareWalletDefaultHdPath: ({device, path}) => {
|
||||
return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
|
||||
},
|
||||
checkHardwareStatus: (deviceName) => {
|
||||
return dispatch(actions.checkHardwareStatus(deviceName))
|
||||
connectHardware: (deviceName, page, hdPath) => {
|
||||
return dispatch(actions.connectHardware(deviceName, page, hdPath))
|
||||
},
|
||||
checkHardwareStatus: (deviceName, hdPath) => {
|
||||
return dispatch(actions.checkHardwareStatus(deviceName, hdPath))
|
||||
},
|
||||
forgetDevice: (deviceName) => {
|
||||
return dispatch(actions.forgetDevice(deviceName))
|
||||
},
|
||||
unlockTrezorAccount: index => {
|
||||
return dispatch(actions.unlockTrezorAccount(index))
|
||||
unlockHardwareWalletAccount: (index, deviceName, hdPath) => {
|
||||
return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath))
|
||||
},
|
||||
showImportPage: () => dispatch(actions.showImportPage()),
|
||||
showConnectPage: () => dispatch(actions.showConnectPage()),
|
||||
|
|
|
@ -11,6 +11,7 @@ export default class SendContent extends Component {
|
|||
|
||||
static propTypes = {
|
||||
updateGas: PropTypes.func,
|
||||
scanQrCode: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
|
@ -18,7 +19,10 @@ export default class SendContent extends Component {
|
|||
<PageContainerContent>
|
||||
<div className="send-v2__form">
|
||||
<SendFromRow />
|
||||
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
|
||||
<SendToRow
|
||||
updateGas={(updateData) => this.props.updateGas(updateData)}
|
||||
scanQrCode={ _ => this.props.scanQrCode()}
|
||||
/>
|
||||
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
|
||||
<SendGasRow />
|
||||
<SendHexDataRow />
|
||||
|
|
|
@ -17,6 +17,7 @@ export default class SendToRow extends Component {
|
|||
updateGas: PropTypes.func,
|
||||
updateSendTo: PropTypes.func,
|
||||
updateSendToError: PropTypes.func,
|
||||
scanQrCode: PropTypes.func,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -51,6 +52,7 @@ export default class SendToRow extends Component {
|
|||
showError={inError}
|
||||
>
|
||||
<EnsInput
|
||||
scanQrCode={_ => this.props.scanQrCode()}
|
||||
accounts={toAccounts}
|
||||
closeDropdown={() => closeToDropdown()}
|
||||
dropdownOpen={toDropdownOpen}
|
||||
|
|
|
@ -38,12 +38,30 @@ export default class SendTransactionScreen extends PersistentForm {
|
|||
updateAndSetGasTotal: PropTypes.func,
|
||||
updateSendErrors: PropTypes.func,
|
||||
updateSendTokenBalance: PropTypes.func,
|
||||
scanQrCode: PropTypes.func,
|
||||
qrCodeDetected: PropTypes.func,
|
||||
qrCodeData: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.qrCodeData) {
|
||||
if (nextProps.qrCodeData.type === 'address') {
|
||||
const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase()
|
||||
const currentAddress = this.props.to && this.props.to.toLowerCase()
|
||||
if (currentAddress !== scannedAddress) {
|
||||
this.props.updateSendTo(scannedAddress)
|
||||
this.updateGas({ to: scannedAddress })
|
||||
// Clean up QR code data after handling
|
||||
this.props.qrCodeDetected(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateGas ({ to: updatedToAddress, amount: value } = {}) {
|
||||
const {
|
||||
amount,
|
||||
|
@ -158,6 +176,16 @@ export default class SendTransactionScreen extends PersistentForm {
|
|||
address,
|
||||
})
|
||||
this.updateGas()
|
||||
|
||||
// Show QR Scanner modal if ?scan=true
|
||||
if (window.location.search === '?scan=true') {
|
||||
this.props.scanQrCode()
|
||||
|
||||
// Clear the queryString param after showing the modal
|
||||
const cleanUrl = location.href.split('?')[0]
|
||||
history.pushState({}, null, `${cleanUrl}`)
|
||||
window.location.hash = '#send'
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -170,7 +198,10 @@ export default class SendTransactionScreen extends PersistentForm {
|
|||
return (
|
||||
<div className="page-container">
|
||||
<SendHeader history={history}/>
|
||||
<SendContent updateGas={(updateData) => this.updateGas(updateData)}/>
|
||||
<SendContent
|
||||
updateGas={(updateData) => this.updateGas(updateData)}
|
||||
scanQrCode={_ => this.props.scanQrCode()}
|
||||
/>
|
||||
<SendFooter history={history}/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -21,11 +21,15 @@ import {
|
|||
getSendFromObject,
|
||||
getSendTo,
|
||||
getTokenBalance,
|
||||
getQrCodeData,
|
||||
} from './send.selectors'
|
||||
import {
|
||||
updateSendTo,
|
||||
updateSendTokenBalance,
|
||||
updateGasData,
|
||||
setGasTotal,
|
||||
showQrScanner,
|
||||
qrCodeDetected,
|
||||
} from '../../actions'
|
||||
import {
|
||||
resetSendState,
|
||||
|
@ -35,6 +39,10 @@ import {
|
|||
calcGasTotal,
|
||||
} from './send.utils.js'
|
||||
|
||||
import {
|
||||
SEND_ROUTE,
|
||||
} from '../../routes'
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
|
@ -60,6 +68,7 @@ function mapStateToProps (state) {
|
|||
tokenBalance: getTokenBalance(state),
|
||||
tokenContract: getSelectedTokenContract(state),
|
||||
tokenToFiatRate: getSelectedTokenToFiatRate(state),
|
||||
qrCodeData: getQrCodeData(state),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,5 +98,8 @@ function mapDispatchToProps (dispatch) {
|
|||
},
|
||||
updateSendErrors: newError => dispatch(updateSendErrors(newError)),
|
||||
resetSendState: () => dispatch(resetSendState()),
|
||||
scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
|
||||
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
|
||||
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ const selectors = {
|
|||
getTokenExchangeRate,
|
||||
getUnapprovedTxs,
|
||||
transactionsSelector,
|
||||
getQrCodeData,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
|
@ -282,3 +283,7 @@ function transactionsSelector (state) {
|
|||
: txsToRender
|
||||
.sort((a, b) => b.time - a.time)
|
||||
}
|
||||
|
||||
function getQrCodeData (state) {
|
||||
return state.appState.qrCodeData
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ proxyquire('../send.container.js', {
|
|||
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
|
||||
getSendFromObject: (s) => `mockFrom:${s}`,
|
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`,
|
||||
getQrCodeData: (s) => `mockQrCodeData:${s}`,
|
||||
},
|
||||
'../../actions': actionSpies,
|
||||
'../../ducks/send.duck': duckActionSpies,
|
||||
|
@ -76,6 +77,7 @@ describe('send container', () => {
|
|||
tokenBalance: 'mockTokenBalance:mockState',
|
||||
tokenContract: 'mockTokenContract:mockState',
|
||||
tokenToFiatRate: 'mockTokenToFiatRate:mockState',
|
||||
qrCodeData: 'mockQrCodeData:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ const h = require('react-hyperscript')
|
|||
const inherits = require('util').inherits
|
||||
const AccountListItem = require('../account-list-item/account-list-item.component').default
|
||||
const connect = require('react-redux').connect
|
||||
const Tooltip = require('../../tooltip')
|
||||
|
||||
ToAutoComplete.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
|
@ -94,11 +95,12 @@ ToAutoComplete.prototype.render = function () {
|
|||
dropdownOpen,
|
||||
onChange,
|
||||
inError,
|
||||
qrScanner,
|
||||
} = this.props
|
||||
|
||||
return h('div.send-v2__to-autocomplete', {}, [
|
||||
|
||||
h('input.send-v2__to-autocomplete__input', {
|
||||
h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
|
||||
placeholder: this.context.t('recipientAddress'),
|
||||
className: inError ? `send-v2__error-border` : '',
|
||||
value: to,
|
||||
|
@ -108,7 +110,13 @@ ToAutoComplete.prototype.render = function () {
|
|||
borderColor: inError ? 'red' : null,
|
||||
},
|
||||
}),
|
||||
|
||||
qrScanner && h(Tooltip, {
|
||||
title: this.context.t('scanQrCode'),
|
||||
position: 'bottom',
|
||||
}, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, {
|
||||
style: { color: '#33333' },
|
||||
onClick: () => this.props.scanQrCode(),
|
||||
})),
|
||||
!to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, {
|
||||
style: { color: '#dedede' },
|
||||
onClick: () => this.handleInputEvent(),
|
||||
|
|
|
@ -35,6 +35,7 @@ function mapStateToProps (state) {
|
|||
currentCurrency: getCurrentCurrency(state),
|
||||
contractExchangeRates: state.metamask.contractExchangeRates,
|
||||
selectedAddressTxList: state.metamask.selectedAddressTxList,
|
||||
networkNonce: state.appState.networkNonce,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,18 +210,24 @@ TxListItem.prototype.showRetryButton = function () {
|
|||
selectedAddressTxList,
|
||||
transactionId,
|
||||
txParams,
|
||||
networkNonce,
|
||||
} = this.props
|
||||
if (!txParams) {
|
||||
return false
|
||||
}
|
||||
let currentTxSharesEarliestNonce = false
|
||||
const currentNonce = txParams.nonce
|
||||
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
|
||||
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
|
||||
const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted')
|
||||
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
|
||||
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
|
||||
lastSubmittedTxWithCurrentNonce.id === transactionId
|
||||
if (currentSubmittedTxs.length > 0) {
|
||||
currentTxSharesEarliestNonce = currentNonce === networkNonce
|
||||
}
|
||||
|
||||
return currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
|
||||
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
|
||||
}
|
||||
|
||||
TxListItem.prototype.setSelectedToken = function (tokenAddress) {
|
||||
|
|
|
@ -8,7 +8,7 @@ const selectors = require('../selectors')
|
|||
const TxListItem = require('./tx-list-item')
|
||||
const ShiftListItem = require('./shift-list-item')
|
||||
const { formatDate } = require('../util')
|
||||
const { showConfTxPage } = require('../actions')
|
||||
const { showConfTxPage, updateNetworkNonce } = require('../actions')
|
||||
const classnames = require('classnames')
|
||||
const { tokenInfoGetter } = require('../token-util')
|
||||
const { withRouter } = require('react-router-dom')
|
||||
|
@ -28,12 +28,14 @@ function mapStateToProps (state) {
|
|||
return {
|
||||
txsToRender: selectors.transactionsSelector(state),
|
||||
conversionRate: selectors.conversionRateSelector(state),
|
||||
selectedAddress: selectors.getSelectedAddress(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
|
||||
updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +46,20 @@ function TxList () {
|
|||
|
||||
TxList.prototype.componentWillMount = function () {
|
||||
this.tokenInfoGetter = tokenInfoGetter()
|
||||
this.props.updateNetworkNonce(this.props.selectedAddress)
|
||||
}
|
||||
|
||||
TxList.prototype.componentDidUpdate = function (prevProps) {
|
||||
const oldTxsToRender = prevProps.txsToRender
|
||||
const {
|
||||
txsToRender: newTxsToRender,
|
||||
selectedAddress,
|
||||
updateNetworkNonce,
|
||||
} = this.props
|
||||
|
||||
if (newTxsToRender.length > oldTxsToRender.length) {
|
||||
updateNetworkNonce(selectedAddress)
|
||||
}
|
||||
}
|
||||
|
||||
TxList.prototype.render = function () {
|
||||
|
|
|
@ -117,8 +117,18 @@ WalletView.prototype.render = function () {
|
|||
return kr.accounts.includes(selectedAddress)
|
||||
})
|
||||
|
||||
const type = keyring.type
|
||||
const isLoose = type !== 'HD Key Tree'
|
||||
let label = ''
|
||||
let type
|
||||
if (keyring) {
|
||||
type = keyring.type
|
||||
if (type !== 'HD Key Tree') {
|
||||
if (type.toLowerCase().search('hardware') !== -1) {
|
||||
label = this.context.t('hardware')
|
||||
} else {
|
||||
label = this.context.t('imported')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
|
||||
style: {},
|
||||
|
@ -132,7 +142,7 @@ WalletView.prototype.render = function () {
|
|||
onClick: hideSidebar,
|
||||
}),
|
||||
|
||||
h('div.wallet-view__keyring-label.allcaps', isLoose ? this.context.t('imported') : ''),
|
||||
h('div.wallet-view__keyring-label.allcaps', label),
|
||||
|
||||
h('div.flex-column.flex-center.wallet-view__name-container', {
|
||||
style: { margin: '0 auto' },
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds'
|
||||
export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'
|
||||
export const TRANSACTION_ERROR = 'transactionError'
|
||||
export const TRANSACTION_ERROR_KEY = 'transactionError'
|
||||
|
|
|
@ -46,7 +46,7 @@ const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
|
|||
// Setter Maps
|
||||
const toBigNumber = {
|
||||
hex: n => new BigNumber(stripHexPrefix(n), 16),
|
||||
dec: n => new BigNumber(n, 10),
|
||||
dec: n => new BigNumber(String(n), 10),
|
||||
BN: n => new BigNumber(n.toString(16), 16),
|
||||
}
|
||||
const toNormalizedDenomination = {
|
||||
|
@ -154,7 +154,7 @@ const subtractCurrencies = (a, b, options = {}) => {
|
|||
bBase,
|
||||
...conversionOptions
|
||||
} = options
|
||||
const value = (new BigNumber(a, aBase)).minus(b, bBase)
|
||||
const value = (new BigNumber(String(a), aBase)).minus(b, bBase)
|
||||
|
||||
return converter({
|
||||
value,
|
||||
|
|
|
@ -80,7 +80,6 @@
|
|||
}
|
||||
|
||||
.network-name-item {
|
||||
font-weight: 100;
|
||||
flex: 1;
|
||||
color: $dusty-gray;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -162,19 +162,99 @@
|
|||
}
|
||||
|
||||
.hw-connect {
|
||||
width: 100%;
|
||||
|
||||
&__header {
|
||||
&__title {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__msg {
|
||||
font-size: 14px;
|
||||
color: #9b9b9b;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-wrapper {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__connect-btn {
|
||||
background-color: #259De5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
width: 315px;
|
||||
min-height: 54px;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__connect-btn.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
background: #fbfbfb;
|
||||
border: 1px solid #e5e5e5;
|
||||
height: 100px;
|
||||
width: 150px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
|
||||
&__img {
|
||||
width: 95px;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn.selected {
|
||||
border: 2px solid #00a8ee;
|
||||
width: 149px;
|
||||
}
|
||||
|
||||
&__btn:first-child {
|
||||
margin-right: 15px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&__btn:last-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&__hdPath {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&__select {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,6 +281,13 @@
|
|||
font-size: 18px;
|
||||
}
|
||||
|
||||
&__unlock-title {
|
||||
padding-top: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 22px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__msg {
|
||||
font-size: 14px;
|
||||
color: #9b9b9b;
|
||||
|
@ -213,8 +300,6 @@
|
|||
}
|
||||
|
||||
&__footer {
|
||||
width: 100%;
|
||||
|
||||
&__title {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 12px;
|
||||
|
@ -228,6 +313,9 @@
|
|||
color: #9b9b9b;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 27px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&__link {
|
||||
|
@ -236,10 +324,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__get-trezor {
|
||||
&__get-hw {
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
|
||||
&__msg {
|
||||
font-size: 14px;
|
||||
|
@ -390,6 +478,8 @@
|
|||
|
||||
&.account-list {
|
||||
height: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
|
@ -412,6 +502,7 @@
|
|||
min-height: 54px;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
&__button.unlock {
|
||||
|
|
|
@ -626,6 +626,23 @@
|
|||
top: 18px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
&__qr-code {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
right: 33px;
|
||||
cursor: pointer;
|
||||
padding: 8px 5px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&__qr-code:hover {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
&__input.with-qr {
|
||||
padding-right: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
&__to-autocomplete, &__memo-text-area, &__hex-data {
|
||||
|
|
|
@ -124,7 +124,7 @@ class InitializeMenuScreen extends Component {
|
|||
textDecoration: 'underline',
|
||||
marginTop: '32px',
|
||||
},
|
||||
}, 'Use classic interface'),
|
||||
}, this.context.t('classicInterface')),
|
||||
]),
|
||||
|
||||
])
|
||||
|
|
|
@ -3,6 +3,7 @@ import currencies from 'currency-formatter/currencies'
|
|||
import abi from 'human-standard-token-abi'
|
||||
import abiDecoder from 'abi-decoder'
|
||||
import ethUtil from 'ethereumjs-util'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
abiDecoder.addABI(abi)
|
||||
|
||||
|
@ -137,3 +138,11 @@ export function convertTokenToFiat ({
|
|||
export function hasUnconfirmedTransactions (state) {
|
||||
return unconfirmedTransactionsCountSelector(state) > 0
|
||||
}
|
||||
|
||||
export function roundExponential (value) {
|
||||
const PRECISION = 4
|
||||
const bigNumberValue = new BigNumber(String(value))
|
||||
|
||||
// In JS, numbers with exponentials greater than 20 get displayed as an exponential.
|
||||
return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ function reduceApp (state, action) {
|
|||
sidebarOpen: false,
|
||||
alertOpen: false,
|
||||
alertMessage: null,
|
||||
qrCodeData: null,
|
||||
networkDropdownOpen: false,
|
||||
currentView: seedWords ? seedConfView : defaultView,
|
||||
accountDetail: {
|
||||
|
@ -65,6 +66,11 @@ function reduceApp (state, action) {
|
|||
buyView: {},
|
||||
isMouseUser: false,
|
||||
gasIsLoading: false,
|
||||
networkNonce: null,
|
||||
defaultHdPaths: {
|
||||
trezor: `m/44'/60'/0'/0`,
|
||||
ledger: `m/44'/60'/0'/0/0`,
|
||||
},
|
||||
}, state.appState)
|
||||
|
||||
let curPendingTxIndex = appState.currentView.pendingTxIndex || 0
|
||||
|
@ -92,7 +98,7 @@ function reduceApp (state, action) {
|
|||
sidebarOpen: false,
|
||||
})
|
||||
|
||||
// sidebar methods
|
||||
// alert methods
|
||||
case actions.ALERT_OPEN:
|
||||
return extend(appState, {
|
||||
alertOpen: true,
|
||||
|
@ -105,6 +111,13 @@ function reduceApp (state, action) {
|
|||
alertMessage: null,
|
||||
})
|
||||
|
||||
// qr scanner methods
|
||||
case actions.QR_CODE_DETECTED:
|
||||
return extend(appState, {
|
||||
qrCodeData: action.value,
|
||||
})
|
||||
|
||||
|
||||
// modal methods:
|
||||
case actions.MODAL_OPEN:
|
||||
const { name, ...modalProps } = action.payload
|
||||
|
@ -539,6 +552,15 @@ function reduceApp (state, action) {
|
|||
warning: '',
|
||||
})
|
||||
|
||||
case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH:
|
||||
const { device, path } = action.value
|
||||
const newDefaults = {...appState.defaultHdPaths}
|
||||
newDefaults[device] = path
|
||||
|
||||
return extend(appState, {
|
||||
defaultHdPaths: newDefaults,
|
||||
})
|
||||
|
||||
case actions.SHOW_LOADING:
|
||||
return extend(appState, {
|
||||
isLoading: true,
|
||||
|
@ -751,6 +773,11 @@ function reduceApp (state, action) {
|
|||
},
|
||||
})
|
||||
|
||||
case actions.SET_NETWORK_NONCE:
|
||||
return extend(appState, {
|
||||
networkNonce: action.value,
|
||||
})
|
||||
|
||||
default:
|
||||
return appState
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createSelector } from 'reselect'
|
||||
import txHelper from '../../lib/tx-helper'
|
||||
import { calcTokenAmount } from '../token-util'
|
||||
import { roundExponential } from '../helpers/confirm-transaction/util'
|
||||
|
||||
const unapprovedTxsSelector = state => state.metamask.unapprovedTxs
|
||||
const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
|
||||
|
@ -133,7 +134,8 @@ export const tokenAmountAndToAddressSelector = createSelector(
|
|||
const toParam = params.find(param => param.name === TOKEN_PARAM_TO)
|
||||
const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE)
|
||||
toAddress = toParam ? toParam.value : params[0].value
|
||||
tokenAmount = valueParam ? Number(valueParam.value) : Number(params[1].value)
|
||||
const value = valueParam ? Number(valueParam.value) : Number(params[1].value)
|
||||
tokenAmount = roundExponential(value)
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -145,13 +147,20 @@ export const tokenAmountAndToAddressSelector = createSelector(
|
|||
|
||||
export const approveTokenAmountAndToAddressSelector = createSelector(
|
||||
tokenDataParamsSelector,
|
||||
params => {
|
||||
tokenDecimalsSelector,
|
||||
(params, tokenDecimals) => {
|
||||
let toAddress = ''
|
||||
let tokenAmount = 0
|
||||
|
||||
if (params && params.length) {
|
||||
toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
|
||||
tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
|
||||
const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
|
||||
|
||||
if (tokenDecimals) {
|
||||
tokenAmount = calcTokenAmount(value, tokenDecimals)
|
||||
}
|
||||
|
||||
tokenAmount = roundExponential(tokenAmount)
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -170,11 +179,13 @@ export const sendTokenTokenAmountAndToAddressSelector = createSelector(
|
|||
|
||||
if (params && params.length) {
|
||||
toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value
|
||||
tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
|
||||
let value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
|
||||
|
||||
if (tokenDecimals) {
|
||||
tokenAmount = calcTokenAmount(tokenAmount, tokenDecimals)
|
||||
value = calcTokenAmount(value, tokenDecimals)
|
||||
}
|
||||
|
||||
tokenAmount = roundExponential(value)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue