1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge branch 'develop' into trezor-v5

This commit is contained in:
brunobar79 2018-11-13 17:21:15 -05:00
commit 484aa6801e
46 changed files with 4605 additions and 481 deletions

View File

@ -5,7 +5,9 @@ about: Using MetaMask, but it's not working as you expect?
---
<!--
BEFORE SUBMITTING: PLEASE SEARCH TO MAKE SURE THIS ISSUE HAS NOT BEEN SUBMITTED
BEFORE SUBMITTING:
1) Please search to make sure this issue has not been opened already
2) If this is a implementation question or trouble with your personal project, please post on StackExchange. This will get your question answered more quickly and make it easier for other devs to find the answer in the future.
-->
**Describe the bug**
@ -25,10 +27,9 @@ A clear description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Browser details (please complete the following information):**
- OS: [e.g. iOS]
- OS: [e.g. OS X, Windows]
- Browser [e.g. chrome, safari]
- MetaMask Version [e.g. 4.9.0]
- Old UI or New / Beta UI?
- MetaMask Version [e.g. 5.0.2]
**Additional context**
Add any other context about the problem here.

View File

@ -2,6 +2,15 @@
## Current Develop Branch
## 5.0.2 Friday November 9 2018
- Fixed bug that caused accounts to update slowly to sites. #5717
- Fixed bug that could lead to some sites crashing. #5709
## 5.0.1 Wednesday November 7 2018
- Fixed bug in privacy mode that made it not work correctly on Firefox.
## 5.0.0 Tuesday November 6 2018
- Implements EIP 1102 as a user-activated "Privacy Mode".

View File

@ -3,7 +3,7 @@
## Support
If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/).
If you're a user seeking support, [here is our support site](https://metamask.zendesk.com/hc/en-us).
## Introduction

View File

@ -785,6 +785,9 @@
"noWebcamFound": {
"message": "Your computer's webcam was not found. Please try again."
},
"ofTextNofM": {
"message": "of"
},
"oldUI": {
"message": "Old UI"
},
@ -932,6 +935,9 @@
"restoreAccountWithSeed": {
"message": "Restore your Account with Seed Phrase"
},
"requestsAwaitingAcknowledgement": {
"message": "requests waiting to be acknowledged"
},
"required": {
"message": "Required"
},

View File

@ -14,10 +14,11 @@
{ "code": "pl", "name": "Polskie" },
{ "code": "pt", "name": "Português" },
{ "code": "ru", "name": "Русский" },
{ "code": "sk", "name": "Slovenčina" },
{ "code": "sl", "name": "Slovenščina" },
{ "code": "th", "name": "ไทย" },
{ "code": "tml", "name": "தமிழ்" },
{ "code": "tr", "name": "Türkçe" },
{ "code": "tr", "name": "Türkçe" },
{ "code": "vi", "name": "Tiếng Việt" },
{ "code": "zh_CN", "name": "中文(简体)" },
{ "code": "zh_TW", "name": "中文(繁體)" }

View File

@ -0,0 +1,957 @@
{
"privacyMode": {
"message": "Režim súkromia"
},
"privacyModeDescription": {
"message": "Webové stránky musia požiadať o prístup k zobrazeniu informácií o vašom účte."
},
"exposeAccounts": {
"message": "Vystavte účty"
},
"exposeDescription": {
"message": "Vystavte účty na aktuální webové stránky. Užitečné pro starší dappy."
},
"confirmExpose": {
"message": "Opravdu chcete své účty vystavit na stávajícím webu?"
},
"confirmClear": {
"message": "Naozaj chcete vymazať schválené webové stránky?"
},
"clearApprovalDataSuccess": {
"message": "Schválené údaje webových stránek byly úspěšně zrušeny."
},
"approvalData": {
"message": "Údaje o schválení"
},
"approvalDataDescription": {
"message": "Vymažte schválené údaje webových stránek, aby všechny weby znovu požádaly o schválení."
},
"clearApprovalData": {
"message": "Jasné údaje o schválení"
},
"approve": {
"message": "Schválit"
},
"reject": {
"message": "Odmítnout"
},
"providerAPIRequest": {
"message": "Požadavek API Ethereum"
},
"reviewProviderRequest": {
"message": "Přečtěte si prosím tuto žádost API Ethereum."
},
"providerRequestInfo": {
"message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě."
},
"accept": {
"message": "Přijmout"
},
"account": {
"message": "Účet"
},
"accountDetails": {
"message": "Detaily účtu"
},
"accountName": {
"message": "Název účtu"
},
"address": {
"message": "Adresa"
},
"addCustomToken": {
"message": "Přidat vlastní token"
},
"addToken": {
"message": "Přidat token"
},
"addTokens": {
"message": "Přidat tokeny"
},
"amount": {
"message": "Částka"
},
"amountPlusGas": {
"message": "Částka + palivo"
},
"appDescription": {
"message": "Ethereum rozšíření prohlížeče",
"description": "The description of the application"
},
"appName": {
"message": "MetaMask",
"description": "The name of the application"
},
"approved": {
"message": "Schváleno"
},
"attemptingConnect": {
"message": "Pokouším se připojit k blockchainu."
},
"attributions": {
"message": "Zásluhy"
},
"available": {
"message": "Dostupné"
},
"back": {
"message": "Zpět"
},
"balance": {
"message": "Zůstatek:"
},
"balances": {
"message": "Zůstatek tokenu"
},
"balanceIsInsufficientGas": {
"message": "Nedostatek prostředků pro aktuální množství paliva"
},
"beta": {
"message": "BETA"
},
"betweenMinAndMax": {
"message": "musí být větší nebo roven $1 a menší nebo roven $2.",
"description": "helper for inputting hex as decimal input"
},
"blockiesIdenticon": {
"message": "Použít Blockies Identicon"
},
"borrowDharma": {
"message": "Pújčit si přes Dharma (Beta)"
},
"builtInCalifornia": {
"message": "MetaMask je navržen a vytvořen v Kalifornii."
},
"buy": {
"message": "Koupit"
},
"buyCoinbase": {
"message": "Nákup na Coinbase"
},
"buyCoinbaseExplainer": {
"message": "Coinbase je světově nejoblíbenější místo k nákupu a prodeji bitcoinu, etherea nebo litecoinu."
},
"ok": {
"message": "Ok"
},
"cancel": {
"message": "Zrušit"
},
"classicInterface": {
"message": "Použít klasické rozhraní"
},
"clickCopy": {
"message": "Kliknutím zkopírovat"
},
"confirm": {
"message": "Potvrdit"
},
"confirmed": {
"message": "Potvrzeno"
},
"confirmContract": {
"message": "Potvrdit kontrakt"
},
"confirmPassword": {
"message": "Potvrdit heslo"
},
"confirmTransaction": {
"message": "Potvrdit transakci"
},
"continue": {
"message": "Pokračovat"
},
"continueToCoinbase": {
"message": "Přejít na Coinbase"
},
"contractDeployment": {
"message": "Nasazení kontraktu"
},
"conversionProgress": {
"message": "Provádí se převod"
},
"copiedButton": {
"message": "Zkopírováno"
},
"copiedClipboard": {
"message": "Zkopírováno do schránky"
},
"copiedExclamation": {
"message": "Zkopírováno!"
},
"copiedSafe": {
"message": "Zkopíroval jsem to na bezpečné místo"
},
"copy": {
"message": "Kopírovat"
},
"copyToClipboard": {
"message": "Kopírovat do schránky"
},
"copyButton": {
"message": " Kopírovat "
},
"copyPrivateKey": {
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
},
"create": {
"message": "Vytvořit"
},
"createAccount": {
"message": "Vytvořit účet"
},
"createDen": {
"message": "Vytvořit"
},
"crypto": {
"message": "Krypto",
"description": "Exchange type (cryptocurrencies)"
},
"currentConversion": {
"message": "Aktuální převod"
},
"currentNetwork": {
"message": "Aktuální síť"
},
"customGas": {
"message": "Nastavit palivo"
},
"customToken": {
"message": "Vlastní token"
},
"customize": {
"message": "Nastavit"
},
"customRPC": {
"message": "Vlastní RPC"
},
"decimalsMustZerotoTen": {
"message": "Desetinných míst musí být od 0 do 36."
},
"decimal": {
"message": "Počet desetinných míst přesnosti"
},
"defaultNetwork": {
"message": "Výchozí síť pro Etherové transakce je Main Net."
},
"denExplainer": {
"message": "Váš DEN je heslem šifrované uložiště v MetaMasku."
},
"deposit": {
"message": "Vklad"
},
"depositBTC": {
"message": "Vložte BTC na níže uvedenou adresu:"
},
"depositCoin": {
"message": "Vložte $1 na níže uvedenou adresu",
"description": "Tells the user what coin they have selected to deposit with shapeshift"
},
"depositEth": {
"message": "Vložit Eth"
},
"depositEther": {
"message": "Vložit Ether"
},
"depositFiat": {
"message": "Vklad s fiat měnou"
},
"depositFromAccount": {
"message": "Vložte z jiného účtu"
},
"depositShapeShift": {
"message": "Vklad přes ShapeShift"
},
"depositShapeShiftExplainer": {
"message": "Pokud vlastníte jiné kryptoměny, můžete je směnit Ether a vložit ho přímo do peněženky MetaMask. Bez založení účtu."
},
"details": {
"message": "Podrobnosti"
},
"directDeposit": {
"message": "Přímý vklad"
},
"directDepositEther": {
"message": "Vložit Ether přímo"
},
"directDepositEtherExplainer": {
"message": "Pokud už vlastníte nějaký Ether, nejrychleji ho dostanete do peněženky přímým vkladem."
},
"done": {
"message": "Hotovo"
},
"downloadStateLogs": {
"message": "Stáhnout stavové protokoly"
},
"dropped": {
"message": "Zrušeno"
},
"edit": {
"message": "Upravit"
},
"editAccountName": {
"message": "Upravit název účtu"
},
"emailUs": {
"message": "Napište nám e-mail!"
},
"encryptNewDen": {
"message": "Zašifrujte svůj nový DEN"
},
"enterPassword": {
"message": "Zadejte heslo"
},
"enterPasswordConfirm": {
"message": "Zadejte heslo k potvrzení"
},
"passwordNotLongEnough": {
"message": "Heslo není dost dlouhé"
},
"passwordsDontMatch": {
"message": "Hesla nejsou stejná"
},
"etherscanView": {
"message": "Prohlédněte si účet na Etherscan"
},
"exchangeRate": {
"message": "Směnný kurz"
},
"exportPrivateKey": {
"message": "Exportovat privátní klíč"
},
"exportPrivateKeyWarning": {
"message": "Exportujte privátní klíč na vlastní riziko."
},
"failed": {
"message": "Neúspěšné"
},
"fiat": {
"message": "FIAT",
"description": "Exchange type"
},
"fileImportFail": {
"message": "Import souboru nefunguje? Klikněte sem!",
"description": "Helps user import their account from a JSON file"
},
"followTwitter": {
"message": "Sledujte nás na Twitteru"
},
"from": {
"message": "Od"
},
"fromToSame": {
"message": "Adresy odesílatele a příjemce nemohou být stejné"
},
"fromShapeShift": {
"message": "Z ShapeShift"
},
"gas": {
"message": "Palivo",
"description": "Short indication of gas cost"
},
"gasFee": {
"message": "Poplatek za palivo"
},
"gasLimit": {
"message": "Limit paliva"
},
"gasLimitCalculation": {
"message": "Počítáme doporučený limit paliva na základě úspěšnosti v síti."
},
"gasLimitRequired": {
"message": "Limit paliva je povinný"
},
"gasLimitTooLow": {
"message": "Limit paliva musí být alespoň 21000"
},
"generatingSeed": {
"message": "Generuji klíčovou frázi..."
},
"gasPrice": {
"message": "Cena paliva (GWEI)"
},
"gasPriceCalculation": {
"message": "Počítáme doporučenou cenu paliva na základě úspěšnosti v síti."
},
"gasPriceRequired": {
"message": "Cena paliva je povinná"
},
"getEther": {
"message": "Získejte Ether"
},
"getEtherFromFaucet": {
"message": "Získejte Ether z faucetu za $1.",
"description": "Displays network name for Ether faucet"
},
"greaterThanMin": {
"message": "musí být větší nebo roven $1.",
"description": "helper for inputting hex as decimal input"
},
"here": {
"message": "zde",
"description": "as in -click here- for more information (goes with troubleTokenBalances)"
},
"hereList": {
"message": "Tady je seznam!!!!"
},
"hide": {
"message": "Skrýt"
},
"hideToken": {
"message": "Skrýt token"
},
"hideTokenPrompt": {
"message": "Skrýt token?"
},
"howToDeposit": {
"message": "Jakým způsobem chcete vložit Ether?"
},
"holdEther": {
"message": "Dovoluje vám držet ether a tokeny a slouží jako most k decentralizovaným aplikacím."
},
"import": {
"message": "Import",
"description": "Button to import an account from a selected file"
},
"importAccount": {
"message": "Import účtu"
},
"importAccountMsg": {
"message": "Importované účty nebudou spojeny s vaší původní MetaMaskovou klíčovou frází. Zjistěte více o importovaných účtech "
},
"importAnAccount": {
"message": "Import účtu"
},
"importDen": {
"message": "Import existujícího DEN"
},
"imported": {
"message": "Importováno",
"description": "status showing that an account has been fully loaded into the keyring"
},
"infoHelp": {
"message": "Informace a nápověda"
},
"insufficientFunds": {
"message": "Nedostatek finančních prostředků."
},
"insufficientTokens": {
"message": "Nedostatek tokenů."
},
"invalidAddress": {
"message": "Neplatná adresa"
},
"invalidAddressRecipient": {
"message": "Adresa příjemce je neplatná"
},
"invalidGasParams": {
"message": "Neplatná parametry paliva"
},
"invalidInput": {
"message": "Neplatný vstup."
},
"invalidRequest": {
"message": "Neplatný požadavek"
},
"invalidRPC": {
"message": "Neplatné RPC URI"
},
"jsonFail": {
"message": "Něco se pokazilo. Prosím, ujistěte se, že váš JSON soubor má správný formát."
},
"jsonFile": {
"message": "JSON soubor",
"description": "format for importing an account"
},
"keepTrackTokens": {
"message": "Udržujte si záznamy o tokenech, které jste koupili s účtem v MetaMasku."
},
"kovan": {
"message": "Kovan Test Network"
},
"knowledgeDataBase": {
"message": "Navštivte naši Knowledge Base"
},
"max": {
"message": "Max"
},
"learnMore": {
"message": "Zjistěte více."
},
"lessThanMax": {
"message": "musí být menší nebo roven $1.",
"description": "helper for inputting hex as decimal input"
},
"likeToAddTokens": {
"message": "Chcete přidat tyto tokeny?"
},
"links": {
"message": "Odkazy"
},
"limit": {
"message": "Limit"
},
"loading": {
"message": "Načítám..."
},
"loadingTokens": {
"message": "Načítám tokeny..."
},
"localhost": {
"message": "Localhost 8545"
},
"login": {
"message": "Přihlásit"
},
"logout": {
"message": "Odhlásit"
},
"loose": {
"message": "Nevázané"
},
"loweCaseWords": {
"message": "slova klíčové fráze mají pouze malá písmena"
},
"mainnet": {
"message": "Main Ethereum Network"
},
"message": {
"message": "Zpráva"
},
"metamaskDescription": {
"message": "MetaMask je bezpečný osobní trezor pro Ethereum."
},
"min": {
"message": "Minimum"
},
"myAccounts": {
"message": "Moje účty"
},
"mustSelectOne": {
"message": "Musíte zvolit aspoň 1 token."
},
"needEtherInWallet": {
"message": "Potřebujete Ether v peněžence, abyste mohli pomocí MetaMasku interagovat s decentralizovanými aplikacemi."
},
"needImportFile": {
"message": "Musíte zvolit soubor k importu.",
"description": "User is important an account and needs to add a file to continue"
},
"needImportPassword": {
"message": "Musíte zadat heslo pro zvolený soubor.",
"description": "Password and file needed to import an account"
},
"negativeETH": {
"message": "Nelze odeslat zápornou částku ETH."
},
"networks": {
"message": "Sítě"
},
"newAccount": {
"message": "Nový účet"
},
"newAccountNumberName": {
"message": "Účet $1",
"description": "Default name of next account to be created on create account screen"
},
"newContract": {
"message": "Nový kontrakt"
},
"newPassword": {
"message": "Nové heslo (min 8 znaků)"
},
"newRecipient": {
"message": "Nový příjemce"
},
"newRPC": {
"message": "Nová RPC URL"
},
"next": {
"message": "Další"
},
"noAddressForName": {
"message": "Pro toto jméno nebyla nastavena žádná adresa."
},
"noDeposits": {
"message": "Žádný vklad"
},
"noTransactionHistory": {
"message": "Žádná historie transakcí."
},
"noTransactions": {
"message": "Žádné transakce"
},
"notStarted": {
"message": "Nezačalo"
},
"oldUI": {
"message": "Staré rozhraní"
},
"oldUIMessage": {
"message": "Vrátili jste se ke starému rozhraní. Můžete přepnout na nové rozhraní v nastavení v pravém horním menu."
},
"or": {
"message": "nebo",
"description": "choice between creating or importing a new account"
},
"passwordCorrect": {
"message": "Ujistěte se, že je vaše heslo správně."
},
"passwordMismatch": {
"message": "hesla nesouhlasí",
"description": "in password creation process, the two new password fields did not match"
},
"passwordShort": {
"message": "heslo je krátké",
"description": "in password creation process, the password is not long enough to be secure"
},
"pastePrivateKey": {
"message": "Vložte zde svůj privátní klíč:",
"description": "For importing an account from a private key"
},
"pasteSeed": {
"message": "Svou klíčovou frázi vložte zde!"
},
"personalAddressDetected": {
"message": "Detekována osobní adresa. Zadejte adresu kontraktu tokenu."
},
"pleaseReviewTransaction": {
"message": "Zkontrolujte si transakci."
},
"popularTokens": {
"message": "Oblíbené tokeny"
},
"privacyMsg": {
"message": "Zásady ochrany osobních údajů"
},
"privateKey": {
"message": "Privátní klíč",
"description": "select this type of file to use to import an account"
},
"privateKeyWarning": {
"message": "Upozornění: Nikdy nezveřejněte tento klíč. Kdokoli může s vaším privátním klíčem odcizit vaše aktiva z účtu."
},
"privateNetwork": {
"message": "Soukromá síť"
},
"qrCode": {
"message": "Ukázat QR kód"
},
"readdToken": {
"message": "Tento token můžete v budoucnu přidat zpět s „Přidat token“ v nastavení účtu."
},
"readMore": {
"message": "Přečtěte si více zde."
},
"readMore2": {
"message": "Přečtěte si více."
},
"receive": {
"message": "Obrdžet"
},
"recipientAddress": {
"message": "Adresa příjemce"
},
"refundAddress": {
"message": "Adresa pro vrácení peněz"
},
"rejected": {
"message": "Odmítnuto"
},
"resetAccount": {
"message": "Resetovat účet"
},
"restoreFromSeed": {
"message": "Obnovit z seed fráze"
},
"restoreVault": {
"message": "Obnovit trezor"
},
"required": {
"message": "Povinné"
},
"retryWithMoreGas": {
"message": "Opakujte s vyšší cenou paliva"
},
"walletSeed": {
"message": "Klíčová fráze peněženky"
},
"revealSeedWords": {
"message": "Zobrazit slova klíčové fráze"
},
"revealSeedWordsWarning": {
"message": "Nebnovujte slova klíčové fráze na veřejnosti! Tato slova mohou být použita k odcizení veškerých vyašich účtů."
},
"revert": {
"message": "Zvrátit"
},
"rinkeby": {
"message": "Rinkeby Test Network"
},
"ropsten": {
"message": "Ropsten Test Network"
},
"currentRpc": {
"message": "Současné RPC"
},
"connectingToMainnet": {
"message": "Připojuji se k Main Ethereum Network"
},
"connectingToRopsten": {
"message": "Připojuji se k Ropsten Test Network"
},
"connectingToKovan": {
"message": "Připojuji se k Kovan Test Network"
},
"connectingToRinkeby": {
"message": "Připojuji se k Rinkeby Test Network"
},
"connectingToUnknown": {
"message": "Připojuji se k neznámé síti"
},
"sampleAccountName": {
"message": "Např. můj nový účet",
"description": "Help user understand concept of adding a human-readable name to their account"
},
"save": {
"message": "Uložit"
},
"reprice_title": {
"message": "Změnit cenu transakce"
},
"reprice_subtitle": {
"message": "Navyšte cenu paliva ve snaze k přepsání a urychlení vyší transakce"
},
"saveAsFile": {
"message": "Uložit do souboru",
"description": "Account export process"
},
"saveSeedAsFile": {
"message": "Uložit slova klíčové fráze do souboru"
},
"search": {
"message": "Hledat"
},
"secretPhrase": {
"message": "Zadejte svých 12 slov tajné fráze k obnovení trezoru."
},
"newPassword8Chars": {
"message": "Nové heslo (min 8 znaků)"
},
"seedPhraseReq": {
"message": "klíčové fráze mají 12 slov"
},
"select": {
"message": "Vybrat"
},
"selectCurrency": {
"message": "Vybrat měnu"
},
"selectService": {
"message": "Vybrat službu"
},
"selectType": {
"message": "Vybrat typ"
},
"send": {
"message": "Odeslat"
},
"sendETH": {
"message": "Odeslat ETH"
},
"sendTokens": {
"message": "Odeslat tokeny"
},
"onlySendToEtherAddress": {
"message": "Posílejte jen ETH na Ethereum adresu."
},
"searchTokens": {
"message": "Hledat tokeny"
},
"sendTokensAnywhere": {
"message": "Posílejte tokeny komukoli s Ethereum účtem"
},
"settings": {
"message": "Nastavení"
},
"info": {
"message": "Informace"
},
"shapeshiftBuy": {
"message": "Nakoupit na ShapeShift"
},
"showPrivateKeys": {
"message": "Zobrazit privátní klíče"
},
"showQRCode": {
"message": "Zobrazit QR kód"
},
"sign": {
"message": "Podepsat"
},
"signed": {
"message": "Podepsáno"
},
"signMessage": {
"message": "Podepsat zprávu"
},
"signNotice": {
"message": "Podepsání zprávy může mít \nnebezpečný vedlejší učinek. Podepisujte zprávy pouze ze \nstránek, kterým plně důvěřujete celým svým účtem.\n Tato nebezpečná metoda bude odebrána v budoucí verzi. "
},
"sigRequest": {
"message": "Požadavek podpisu"
},
"sigRequested": {
"message": "Požádáno o podpis"
},
"spaceBetween": {
"message": "mezi slovy může být pouze mezera"
},
"status": {
"message": "Stav"
},
"stateLogs": {
"message": "Stavové protokoly"
},
"stateLogsDescription": {
"message": "Stavové protokoly obsahují vaše veřejné adresy účtů a odeslané transakce."
},
"stateLogError": {
"message": "Chyba během získávání stavových protokolů."
},
"submit": {
"message": "Odeslat"
},
"submitted": {
"message": "Odesláno"
},
"supportCenter": {
"message": "Navštivte naše centrum podpory"
},
"symbolBetweenZeroTen": {
"message": "Symbol musí být mezi 0 a 10 znaky."
},
"takesTooLong": {
"message": "Trvá to dlouho?"
},
"terms": {
"message": "Podmínky použití"
},
"testFaucet": {
"message": "Testovací faucet"
},
"to": {
"message": "Komu"
},
"toETHviaShapeShift": {
"message": "$1 na ETH přes ShapeShift",
"description": "system will fill in deposit type in start of message"
},
"tokenAddress": {
"message": "Adresa tokenu"
},
"tokenAlreadyAdded": {
"message": "Token byl už přidán."
},
"tokenBalance": {
"message": "Váš zůstatek tokenu je:"
},
"tokenSelection": {
"message": "Vyhledejte token nebo je vyberte z našeho seznamu oblíbených tokenů."
},
"tokenSymbol": {
"message": "Symbol tokenu"
},
"tokenWarning1": {
"message": "Mějte přehled o tokenech, které jste koupili s účtem MetaMasku. Pokud jste koupili tokeny s jiným účtem, tyto tokeny se zde nezobrazí."
},
"total": {
"message": "Celkem"
},
"transactions": {
"message": "transakce"
},
"transactionError": {
"message": "Chyba transakce. Vyhozena výjimka v kódu kontraktu."
},
"transactionMemo": {
"message": "Poznámka transakce (nepovinné)"
},
"transactionNumber": {
"message": "Číslo transakce"
},
"transfers": {
"message": "Převody"
},
"troubleTokenBalances": {
"message": "Měli jsme problém s načtením vašich tokenových zůstatků. Můžete je vidět ",
"description": "Followed by a link (here) to view token balances"
},
"twelveWords": {
"message": "Těchto 12 slov je jedinou možností, jak obnovit MetaMask účet. \nUložte je na bezpečné a neveřejné místo."
},
"typePassword": {
"message": "Zadejte své heslo"
},
"uiWelcome": {
"message": "Vítejte v novém rozhraní (Beta)"
},
"uiWelcomeMessage": {
"message": "Používáte nyní nové rozhraní MetaMasku. Rozhlédněte se kolem, vyzkoušejte nové funkce, jako jsou zasílání tokenů, a dejte nám vědět, pokud narazíte na problém."
},
"unapproved": {
"message": "Neschváleno"
},
"unavailable": {
"message": "Nedostupné"
},
"unknown": {
"message": "Neznámé"
},
"unknownNetwork": {
"message": "Neznámá soukromá síť"
},
"unknownNetworkId": {
"message": "Neznámé ID sítě"
},
"uriErrorMsg": {
"message": "URI vyžadují korektní HTTP/HTTPS prefix."
},
"usaOnly": {
"message": "jen v USA",
"description": "Using this exchange is limited to people inside the USA"
},
"usedByClients": {
"message": "Používána různými klienty"
},
"useOldUI": {
"message": "Použijte staré rozhraní"
},
"validFileImport": {
"message": "Musíte vybrat validní soubor k importu."
},
"vaultCreated": {
"message": "Trezor vytvořen"
},
"viewAccount": {
"message": "Zobrazit účet"
},
"visitWebSite": {
"message": "Navštivte naši stránku"
},
"warning": {
"message": "Varování"
},
"welcomeBeta": {
"message": "Vítejte v MetaMask Beta"
},
"whatsThis": {
"message": "Co to je?"
},
"yourSigRequested": {
"message": "Je vyžadován váš podpis"
},
"youSign": {
"message": "Podepisujete"
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="8px" viewBox="0 0 12 8" 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>first/last</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="first/last" fill="#5F5C5D">
<polygon id="Path-12-Copy" points="12 0 12 8 6 4"></polygon>
<polygon id="Path-12-Copy-2" points="6 0 6 8 0 4"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="6px" height="8px" viewBox="0 0 6 8" 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>previous/next</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="previous/next" fill="#5F5C5D" points="6 0 6 8 0 4"></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

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

View File

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

View File

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

View File

@ -448,6 +448,7 @@ function triggerUi () {
const currentlyActiveMetamaskTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id]))
if (!popupIsOpen && !currentlyActiveMetamaskTab && !notificationIsOpen) {
notificationManager.showPopup()
notificationIsOpen = true
}
})
}

View File

@ -147,24 +147,28 @@ function listenForProviderRequest () {
}
})
extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked }) => {
extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked, selectedAddress }) => {
switch (action) {
case 'approve-provider-request':
isEnabled = true
injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: {}}))`)
window.postMessage({ type: 'ethereumprovider', selectedAddress }, '*')
break
case 'approve-legacy-provider-request':
isEnabled = true
window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*')
break
case 'reject-provider-request':
injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`)
window.postMessage({ type: 'ethereumprovider', error: 'User rejected provider access' }, '*')
break
case 'answer-is-approved':
injectScript(`window.dispatchEvent(new CustomEvent('ethereumisapproved', { detail: { isApproved: ${isApproved}, caching: ${caching}}}))`)
window.postMessage({ type: 'ethereumisapproved', isApproved, caching }, '*')
break
case 'answer-is-unlocked':
injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`)
window.postMessage({ type: 'metamaskisunlocked', isUnlocked }, '*')
break
case 'metamask-set-locked':
isEnabled = false
injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`)
window.postMessage({ type: 'metamasksetlocked' }, '*')
break
}
})

View File

@ -88,7 +88,10 @@ class ProviderApprovalController {
_handlePrivacyRequest () {
const privacyMode = this.preferencesController.getFeatureFlags().privacyMode
if (!privacyMode) {
this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true })
this.platform && this.platform.sendMessage({
action: 'approve-legacy-provider-request',
selectedAddress: this.publicConfigStore.getState().selectedAddress,
}, { active: true })
this.publicConfigStore.emit('update', this.publicConfigStore.getState())
}
}
@ -101,7 +104,10 @@ class ProviderApprovalController {
approveProviderRequest (origin) {
this.closePopup && this.closePopup()
const requests = this.store.getState().providerRequests || []
this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true })
this.platform && this.platform.sendMessage({
action: 'approve-provider-request',
selectedAddress: this.publicConfigStore.getState().selectedAddress,
}, { active: true })
this.publicConfigStore.emit('update', this.publicConfigStore.getState())
const providerRequests = requests.filter(request => request.origin !== origin)
this.store.updateState({ providerRequests })

View File

@ -14,8 +14,9 @@ class TokenRatesController {
*
* @param {Object} [config] - Options to configure controller
*/
constructor ({ interval = DEFAULT_INTERVAL, preferences } = {}) {
constructor ({ interval = DEFAULT_INTERVAL, currency, preferences } = {}) {
this.store = new ObservableStore()
this.currency = currency
this.preferences = preferences
this.interval = interval
}
@ -26,32 +27,23 @@ class TokenRatesController {
async updateExchangeRates () {
if (!this.isActive) { return }
const contractExchangeRates = {}
// copy array to ensure its not modified during iteration
const tokens = this._tokens.slice()
for (const token of tokens) {
if (!token) return log.error(`TokenRatesController - invalid tokens state:\n${JSON.stringify(tokens, null, 2)}`)
const address = token.address
contractExchangeRates[address] = await this.fetchExchangeRate(address)
const nativeCurrency = this.currency ? this.currency.getState().nativeCurrency.toUpperCase() : 'ETH'
const pairs = this._tokens.map(token => `pairs[]=${token.address}/${nativeCurrency}`)
const query = pairs.join('&')
if (this._tokens.length > 0) {
try {
const response = await fetch(`https://exchanges.balanc3.net/pie?${query}&autoConversion=true`)
const { prices = [] } = await response.json()
prices.forEach(({ pair, price }) => {
contractExchangeRates[pair.split('/')[0]] = typeof price === 'number' ? price : 0
})
} catch (error) {
log.warn(`MetaMask - TokenRatesController exchange rate fetch failed.`, error)
}
}
this.store.putState({ contractExchangeRates })
}
/**
* Fetches a token exchange rate by address
*
* @param {String} address - Token contract address
*/
async fetchExchangeRate (address) {
try {
const response = await fetch(`https://metamask.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
const json = await response.json()
return json && json.length ? json[0].averagePrice : 0
} catch (error) {
log.warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
return 0
}
}
/**
* @type {Number}
*/

View File

@ -22,6 +22,21 @@ console.warn('ATTENTION: In an effort to improve user privacy, MetaMask ' +
'accounts. Please see https://bit.ly/2QQHXvF for complete information and up-to-date ' +
'example code.')
/**
* Adds a postMessage listener for a specific message type
*
* @param {string} messageType - postMessage type to listen for
* @param {Function} handler - event handler
* @param {boolean} remove - removes this handler after being triggered
*/
function onMessage(messageType, handler, remove) {
window.addEventListener('message', function ({ data }) {
if (!data || data.type !== messageType) { return }
remove && window.removeEventListener('message', handler)
handler.apply(window, arguments)
})
}
//
// setup plugin communication
//
@ -39,52 +54,36 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream)
inpageProvider.setMaxListeners(100)
// set up a listener for when MetaMask is locked
window.addEventListener('metamasksetlocked', () => {
isEnabled = false
})
onMessage('metamasksetlocked', () => { isEnabled = false })
// set up a listener for privacy mode responses
onMessage('ethereumproviderlegacy', ({ data: { selectedAddress } }) => {
isEnabled = true
inpageProvider.publicConfigStore.updateState({ selectedAddress })
}, true)
// augment the provider with its enable method
inpageProvider.enable = function ({ force } = {}) {
return new Promise((resolve, reject) => {
window.removeEventListener('ethereumprovider', providerHandle)
providerHandle = ({ detail }) => {
if (typeof detail.error !== 'undefined') {
reject(detail.error)
providerHandle = ({ data: { error, selectedAddress } }) => {
if (typeof error !== 'undefined') {
reject(error)
} else {
// wait for the publicConfig store to populate with an account
const publicConfig = new Promise((resolve) => {
const { selectedAddress } = inpageProvider.publicConfigStore.getState()
inpageProvider._metamask.isUnlocked().then(unlocked => {
if (!unlocked || selectedAddress) {
resolve()
} else {
inpageProvider.publicConfigStore.on('update', ({ selectedAddress }) => {
selectedAddress && resolve()
})
}
})
})
window.removeEventListener('message', providerHandle)
inpageProvider.publicConfigStore.updateState({ selectedAddress })
// wait for the background to update with an account
const ethAccounts = new Promise((resolveAccounts, rejectAccounts) => {
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
if (error) {
rejectAccounts(error)
} else {
resolveAccounts(response.result)
}
})
})
Promise.all([ethAccounts, publicConfig])
.then(([selectedAddress]) => {
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
if (error) {
reject(error)
} else {
isEnabled = true
resolve(selectedAddress)
})
.catch(reject)
resolve(response.result)
}
})
}
}
window.addEventListener('ethereumprovider', providerHandle)
onMessage('ethereumprovider', providerHandle, true)
window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*')
})
}
@ -106,20 +105,15 @@ inpageProvider._metamask = new Proxy({
* @returns {Promise<boolean>} - Promise resolving to true if this domain has been previously approved
*/
isApproved: function() {
return new Promise((resolve, reject) => {
window.removeEventListener('ethereumisapproved', isApprovedHandle)
isApprovedHandle = ({ detail }) => {
if (typeof detail.error !== 'undefined') {
reject(detail.error)
return new Promise((resolve) => {
isApprovedHandle = ({ data: { caching, isApproved } }) => {
if (caching) {
resolve(!!isApproved)
} else {
if (detail.caching) {
resolve(!!detail.isApproved)
} else {
resolve(false)
}
resolve(false)
}
}
window.addEventListener('ethereumisapproved', isApprovedHandle)
onMessage('ethereumisapproved', isApprovedHandle, true)
window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*')
})
},
@ -130,16 +124,11 @@ inpageProvider._metamask = new Proxy({
* @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked
*/
isUnlocked: function () {
return new Promise((resolve, reject) => {
window.removeEventListener('metamaskisunlocked', isUnlockedHandle)
isUnlockedHandle = ({ detail }) => {
if (typeof detail.error !== 'undefined') {
reject(detail.error)
} else {
resolve(!!detail.isUnlocked)
}
return new Promise((resolve) => {
isUnlockedHandle = ({ data: { isUnlocked } }) => {
resolve(!!isUnlocked)
}
window.addEventListener('metamaskisunlocked', isUnlockedHandle)
onMessage('metamaskisunlocked', isUnlockedHandle, true)
window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*')
})
},

View File

@ -117,6 +117,7 @@ module.exports = class MetamaskController extends EventEmitter {
// token exchange rate tracker
this.tokenRatesController = new TokenRatesController({
currency: this.currencyController.store,
preferences: this.preferencesController.store,
})

View File

@ -0,0 +1,323 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isAccountMenuOpen": false,
"isMascara": false,
"isPopup": false,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x8cf82b5aa41ff2282427be151dd328568684007a": {
"address": "0x8cf82b5aa41ff2282427be151dd328568684007a",
"name": "Account 3"
},
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {
"address": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"name": "Account 2"
},
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": {
"address": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"name": "Account 1"
}
},
"unapprovedTxs": {
"2389644572638771": {
"estimatedGas": "0x8544",
"gasLimitSpecified": true,
"gasPriceSpecified": true,
"history": [],
"id": 2389644572638771,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "MetaMask",
"status": "unapproved",
"time": 1538844175144,
"txParams": {
"data": "0xa9059cbb000000000000000000000000be1a00e10ec68b154adb84e8119167146a71c9a20000000000000000000000000000000000000000000000000000000000000000",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x8544",
"gasPrice": "0x3b9aca00",
"to": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a",
"value": "0x0"
},
"type": "standard"
},
"2389644572638772": {
"estimatedGas": "0x5208",
"gasLimitSpecified": true,
"gasPriceSpecified": true,
"history": [],
"id": 2389644572638772,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "MetaMask",
"status": "unapproved",
"time": 1538844178492,
"txParams": {
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x5208",
"gasPrice": "0x3b9aca00",
"to": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"value": "0x0"
},
"type": "standard"
},
"2389644572638773": {
"estimatedGas": {
"length": 1,
"negative": 0,
"red": null,
"words": [
34061,
null
]
},
"gasLimitSpecified": false,
"gasPriceSpecified": true,
"history": [],
"id": 2389644572638773,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "localhost",
"status": "unapproved",
"time": 1538844204724,
"txParams": {
"data": "0xdfb29935000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320746865206970667320686173680000000000000000000000",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0xc793",
"gasPrice": "0x3b9aca00",
"to": "0xb7ec370c889b3b48ec537e0b2c887faedceb254a",
"value": "0x0"
},
"type": "standard"
},
"2389644572638774": {
"estimatedGas": "0x38f53",
"gasLimitSpecified": true,
"gasPriceSpecified": false,
"history": [],
"id": 2389644572638774,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "remix.ethereum.org",
"status": "unapproved",
"time": 1538844223352,
"txParams": {
"data": "0x608060405234801561001057600080fd5b506102a7806100206000396000f30060806040526004361061004b5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d13319c48114610050578063dfb29935146100da575b600080fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f578181015183820152602001610087565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b60008054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f10610196576101008083540402835291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b82800160010185558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675600a165627a7a72305820cf4282c534b8f2faad947d592afa109b907e4e6b2f52335b361b69c24fedb9580029",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x38f53",
"gasPrice": "0x3b9aca00",
"value": "0x0"
},
"type": "standard"
}
},
"noActiveNotices": true,
"frequentRpcList": [],
"addressBook": [],
"selectedTokenAddress": null,
"contractExchangeRates": {},
"tokenExchangeRates": {},
"tokens": [
{
"address": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a",
"decimals": 9,
"symbol": "DGD"
}
],
"pendingTokens": {},
"send": {
"gasLimit": null,
"gasPrice": null,
"gasTotal": null,
"tokenBalance": null,
"from": "",
"to": "",
"amount": "0x0",
"memo": "",
"errors": {},
"editingTransactionId": null,
"forceGasMin": null
},
"coinOptions": {},
"useBlockie": false,
"featureFlags": {
"betaUI": true,
"skipAnnounceBetaUI": true
},
"isRevealingSeedWords": false,
"welcomeScreenSeen": false,
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
},
"provider": {
"type": "rinkeby"
},
"network": "4",
"accounts": {
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": {
"address": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"balance": "0x36aabfb2a0190c00"
},
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {
"address": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"balance": "0x7b3ef08c294a000"
},
"0x8cf82b5aa41ff2282427be151dd328568684007a": {
"address": "0x8cf82b5aa41ff2282427be151dd328568684007a",
"balance": "0x0"
}
},
"currentBlockGasLimit": "0x731e25",
"selectedAddressTxList": [],
"computedBalances": {},
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0,
"unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0,
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree",
"Trezor Hardware",
"Ledger Hardware"
],
"keyrings": [
{
"type": "HD Key Tree",
"accounts": [
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"0x8cf82b5aa41ff2282427be151dd328568684007a"
]
}
],
"currentAccountTab": "history",
"accountTokens": {
"0x8cf82b5aa41ff2282427be151dd328568684007a": {},
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {},
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": {
"rinkeby": [
{
"address": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a",
"decimals": 9,
"symbol": "DGD"
},
{
"address": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359",
"decimals": 18,
"symbol": "DAI"
}
]
}
},
"assetImages": {
"0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359": null,
"0xe0b7927c4af23765cb51314a0e0521a9645f0e2a": null
},
"suggestedTokens": {},
"lostIdentities": {},
"seedWords": null,
"forgottenPassword": false,
"selectedAddress": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"recentBlocks": [],
"currentCurrency": "usd",
"conversionRate": 225.23,
"conversionDate": 1538859376,
"shapeShiftTxList": [],
"infuraNetworkStatus": {
"kovan": "ok",
"mainnet": "ok",
"rinkeby": "ok",
"ropsten": "ok"
},
"lostAccounts": []
},
"appState": {
"shouldClose": false,
"menuOpen": false,
"modal": {
"open": false,
"modalState": {
"name": null,
"props": {}
},
"previousModalState": {
"name": null
}
},
"sidebar": {
"isOpen": false,
"transitionName": "",
"type": ""
},
"alertOpen": false,
"alertMessage": null,
"qrCodeData": null,
"networkDropdownOpen": false,
"currentView": {
"name": "confTx",
"context": 0
},
"accountDetail": {
"subview": "transactions"
},
"transForward": false,
"isLoading": false,
"warning": null,
"buyView": {},
"isMouseUser": true,
"gasIsLoading": false,
"networkNonce": "0x92",
"defaultHdPaths": {
"trezor": "m/44'/60'/0'/0",
"ledger": "m/44'/60'/0'/0/0"
}
},
"localeMessages": {},
"send": {
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
},
"confirmTransaction": {
"txData": {
"estimatedGas": "0x38f53",
"gasLimitSpecified": true,
"gasPriceSpecified": false,
"history": [],
"id": 2389644572638774,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "remix.ethereum.org",
"status": "unapproved",
"time": 1538844223352,
"txParams": {
"data": "0x608060405234801561001057600080fd5b506102a7806100206000396000f30060806040526004361061004b5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d13319c48114610050578063dfb29935146100da575b600080fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f578181015183820152602001610087565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b60008054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f10610196576101008083540402835291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b82800160010185558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675600a165627a7a72305820cf4282c534b8f2faad947d592afa109b907e4e6b2f52335b361b69c24fedb9580029",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x38f53",
"gasPrice": "0x3b9aca00",
"value": "0x0"
},
"type": "standard"
},
"tokenData": {},
"methodData": {},
"tokenProps": {
"tokenDecimals": "",
"tokenSymbol": ""
},
"fiatTransactionAmount": "0",
"fiatTransactionFee": "0.05",
"fiatTransactionTotal": "0.05",
"ethTransactionAmount": "0",
"ethTransactionFee": "0.000233",
"ethTransactionTotal": "0.000233",
"hexGasTotal": "0xd42f28057e00",
"nonce": "",
"toSmartContract": false,
"fetchingData": false
}
}

View File

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

View File

@ -59,7 +59,7 @@ AccountImportSubview.prototype.render = function () {
},
onClick: () => {
global.platform.openWindow({
url: 'https://metamask.helpscoutdocs.com/article/17-what-are-loose-accounts',
url: 'https://metamask.zendesk.com/hc/en-us/articles/360015289592-What-Are-Loose-Accounts-',
})
},
}, 'here.'),

View File

@ -288,7 +288,7 @@ ConfigScreen.prototype.render = function () {
}, [
'Resetting is for developer use only. ',
h('a', {
href: 'http://metamask.helpscoutdocs.com/article/36-resetting-an-account',
href: 'https://metamask.zendesk.com/hc/en-us/articles/360015489231-Resetting-an-Account-Old-UI-',
target: '_blank',
}, 'Read more.'),
]),

View File

@ -159,6 +159,7 @@ describe('Using MetaMask with an existing account', function () {
it('clicks through the ToS', async () => {
// terms of use
await delay(largeDelayMs)
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))

View File

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

View File

@ -271,17 +271,6 @@ describe('MetaMask', function () {
await driver.wait(until.stalenessOf(accountModal))
await delay(regularDelayMs)
})
it('show account details dropdown menu', async () => {
const {width, height} = await driver.manage().window().getSize()
driver.manage().window().setSize(320, 480)
await driver.findElement(By.css('div.menu-bar__open-in-browser')).click()
const options = await driver.findElements(By.css('div.menu.account-details-dropdown div.menu__item'))
assert.equal(options.length, 3) // HD Wallet type does not have to show the Remove Account option
await delay(regularDelayMs)
driver.manage().window().setSize(width, height)
})
})
describe('Enable privacy mode', () => {
@ -495,6 +484,142 @@ describe('MetaMask', function () {
})
})
describe('Navigate transactions', () => {
it('adds multiple transactions', async () => {
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 2)
const windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
const dapp = windowHandles[1]
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
await send3eth.click()
await delay(regularDelayMs)
const contractDeployment = await findElement(driver, By.xpath(`//button[contains(text(), 'Deploy Contract')]`), 10000)
await contractDeployment.click()
await delay(regularDelayMs)
await send3eth.click()
await contractDeployment.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const transactions = await findElements(driver, By.css('.transaction-list-item'))
await transactions[3].click()
await delay(regularDelayMs)
})
it('navigates the transactions', async () => {
let navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
assert.equal(navigateTxButtons.length, 4, 'navigation button present')
await navigateTxButtons[2].click()
let navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
let navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('2'), true, 'changed transaction right')
navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
await navigateTxButtons[2].click()
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('3'), true, 'changed transaction right')
navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
await navigateTxButtons[2].click()
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('4'), true, 'changed transaction right')
navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
await navigateTxButtons[0].click()
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('1'), true, 'navigate to first transaction')
navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
await navigateTxButtons[3].click()
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.split('4').length, 3, 'navigate to last transaction')
navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
await navigateTxButtons[1].click()
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('3'), true, 'changed transaction left')
navigateTxButtons = await findElements(driver, By.css('.confirm-page-container-navigation__arrow'))
await navigateTxButtons[1].click()
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('2'), true, 'changed transaction left')
})
it('adds a transaction while confirm screen is in focus', async () => {
let navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
let navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('2'), true, 'second transaction in focus')
const windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
const dapp = windowHandles[1]
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
await send3eth.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('3'), true, 'correct transaction in focus')
})
it('confirms a transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000)
await confirmButton.click()
await delay(regularDelayMs)
const navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
const navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('4'), true, 'transaction confirmed')
})
it('rejects a transaction', async () => {
const rejectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Reject')]`), 10000)
await rejectButton.click()
await delay(regularDelayMs)
const navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
const navigationText = await navigationElement.getText()
assert.equal(navigationText.includes('3'), true, 'transaction rejected')
})
it('rejects the rest of the transactions', async () => {
const rejectAllButton = await findElement(driver, By.xpath(`//a[contains(text(), 'Reject 3')]`), 10000)
await rejectAllButton.click()
await delay(regularDelayMs)
const rejectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Reject All')]`), 10000)
await rejectButton.click()
await delay(largeDelayMs * 2)
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
assert.equal(confirmedTxes.length, 3, '3 transactions present')
})
})
describe('Deploy contract and call contract methods', () => {
let extension
let dapp
@ -542,7 +667,7 @@ describe('MetaMask', function () {
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
return confirmedTxes.length === 4
}, 10000)
const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
@ -599,7 +724,7 @@ describe('MetaMask', function () {
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 4
return confirmedTxes.length === 5
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
@ -631,7 +756,7 @@ describe('MetaMask', function () {
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 5
return confirmedTxes.length === 6
}, 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
@ -645,9 +770,9 @@ describe('MetaMask', function () {
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^92.*\s*ETH.*$/), 10000)
await driver.wait(until.elementTextMatches(balance, /^89.*\s*ETH.*$/), 10000)
const tokenAmount = await balance.getText()
assert.ok(/^92.*\s*ETH.*$/.test(tokenAmount))
assert.ok(/^89.*\s*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs)
}
})

View File

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

View File

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

View File

@ -0,0 +1,87 @@
const reactTriggerChange = require('react-trigger-change')
const {
timeout,
queryAsync,
} = require('../../lib/util')
QUnit.module('navigate txs')
QUnit.test('successful navigate', (assert) => {
const done = assert.async()
runNavigateTxsFlowTest(assert)
.then(done)
.catch(err => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
done()
})
})
async function runNavigateTxsFlowTest (assert, done) {
const selectState = await queryAsync($, 'select')
selectState.val('navigate txs')
reactTriggerChange(selectState[0])
// Confirm navigation buttons present
let navigateTxButtons = await queryAsync($, '.confirm-page-container-navigation__arrow')
assert.ok(navigateTxButtons[0], 'navigation button present')
assert.ok(navigateTxButtons[1], 'navigation button present')
assert.ok(navigateTxButtons[2], 'navigation button present')
assert.ok(navigateTxButtons[3], 'navigation button present')
// Verify number of transactions present
let trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.includes('1'), true, 'starts on first')
// Verify correct route
let summaryAction = await queryAsync($, '.confirm-page-container-summary__action')
assert.equal(summaryAction[0].innerText, 'CONTRACT DEPLOYMENT', 'correct route')
// Click navigation button
navigateTxButtons[2].click()
await timeout(2000)
// Verify transaction changed to num 2 and routed correctly
trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.includes('2'), true, 'changed transaction right')
summaryAction = await queryAsync($, '.confirm-page-container-summary__action')
// assert.equal(summaryAction[0].innerText, 'CONFIRM', 'correct route')
// Click navigation button
navigateTxButtons = await queryAsync($, '.confirm-page-container-navigation__arrow')
navigateTxButtons[2].click()
// Verify transation changed to num 3 and routed correctly
trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.includes('3'), true, 'changed transaction right')
summaryAction = await queryAsync($, '.confirm-page-container-summary__action')
assert.equal(summaryAction[0].innerText, 'CONFIRM', 'correct route')
// Click navigation button
navigateTxButtons = await queryAsync($, '.confirm-page-container-navigation__arrow')
navigateTxButtons[2].click()
// Verify transation changed to num 4 and routed correctly
trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.split('4').length, 3, '4 transactions present')
summaryAction = await queryAsync($, '.confirm-page-container-summary__action')
assert.equal(summaryAction[0].innerText, 'TRANSFER', 'correct route')
// Verify left arrow is working correctly
navigateTxButtons = await queryAsync($, '.confirm-page-container-navigation__arrow')
navigateTxButtons[1].click()
trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.includes('3'), true, 'changed transaction left')
// Verify navigate to last transaction is working correctly
navigateTxButtons = await queryAsync($, '.confirm-page-container-navigation__arrow')
navigateTxButtons[3].click()
trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.split('4').length, 3, 'navigate to last transaction')
// Verify navigate to first transaction is working correctly
navigateTxButtons = await queryAsync($, '.confirm-page-container-navigation__arrow')
navigateTxButtons[0].click()
trxNum = await queryAsync($, '.confirm-page-container-navigation')
assert.equal(trxNum[0].innerText.includes('1'), true, 'navigate to first transaction')
}

View File

@ -17,13 +17,4 @@ describe('TokenRatesController', () => {
assert.strictEqual(stub.getCall(0).args[1], 1337)
stub.restore()
})
it('should fetch each token rate based on address', async () => {
const controller = new TokenRatesController()
controller.isActive = true
controller.fetchExchangeRate = address => address
controller.tokens = [{ address: 'foo' }, { address: 'bar' }]
await controller.updateExchangeRates()
assert.deepEqual(controller.store.getState().contractExchangeRates, { foo: 'foo', bar: 'bar' })
})
})

View File

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

View File

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

View File

@ -86,13 +86,13 @@ BalanceComponent.prototype.renderBalance = function () {
className: 'token-amount',
value: balanceValue,
type: PRIMARY,
ethNumberOfDecimals: 3,
ethNumberOfDecimals: 4,
}),
showFiat && h(UserPreferencedCurrencyDisplay, {
value: balanceValue,
type: SECONDARY,
ethNumberOfDecimals: 3,
ethNumberOfDecimals: 4,
}),
])
}

View File

@ -7,7 +7,7 @@
display: flex;
justify-content: space-between;
border-bottom: 1px solid $geyser;
padding: 13px 13px 13px 24px;
padding: 4px 13px 4px 13px;
flex: 0 0 auto;
}

View File

@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
const ConfirmPageContainerNavigation = props => {
const { onNextTx, totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = props
return (
<div className="confirm-page-container-navigation"
style={{
display: showNavigation ? 'flex' : 'none',
}}
>
<div className="confirm-page-container-navigation__container"
style={{
visibility: prevTxId ? 'initial' : 'hidden',
}}>
<div
className="confirm-page-container-navigation__arrow"
onClick={() => onNextTx(firstTx)}>
<img src="/images/double-arrow.svg" />
</div>
<div
className="confirm-page-container-navigation__arrow"
onClick={() => onNextTx(prevTxId)}>
<img src="/images/single-arrow.svg" />
</div>
</div>
<div className="confirm-page-container-navigation__textcontainer">
<div className="confirm-page-container-navigation__navtext">
{positionOfCurrentTx} {ofText} {totalTx}
</div>
<div className="confirm-page-container-navigation__longtext">
{requestsWaitingText}
</div>
</div>
<div
className="confirm-page-container-navigation__container"
style={{
visibility: nextTxId ? 'initial' : 'hidden',
}}>
<div
className="confirm-page-container-navigation__arrow"
onClick={() => onNextTx(nextTxId)}>
<img className="confirm-page-container-navigation__imageflip" src="/images/single-arrow.svg" />
</div>
<div
className="confirm-page-container-navigation__arrow"
onClick={() => onNextTx(lastTx)}>
<img className="confirm-page-container-navigation__imageflip" src="/images/double-arrow.svg" />
</div>
</div>
</div>
)
}
ConfirmPageContainerNavigation.propTypes = {
totalTx: PropTypes.number,
positionOfCurrentTx: PropTypes.number,
onNextTx: PropTypes.func,
nextTxId: PropTypes.string,
prevTxId: PropTypes.string,
showNavigation: PropTypes.bool,
firstTx: PropTypes.string,
lastTx: PropTypes.string,
ofText: PropTypes.string,
requestsWaitingText: PropTypes.string,
}
export default ConfirmPageContainerNavigation

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-navigation.component'

View File

@ -0,0 +1,54 @@
.confirm-page-container-navigation {
display: flex;
justify-content: space-between;
font: inherit;
padding: 4px 10px 4px 10px;
border-bottom: 1px solid $geyser;
flex: 0 0 auto;
&__container {
display: flex;
}
&__arrow {
cursor: pointer;
display: flex;
padding-left: 5px;
padding-right: 5px;
}
&__arrow:hover {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1);
}
&__arrow:active {
-webkit-transform: scale(0.95);
-moz-transform: scale(0.95);
-o-transform: scale(0.95);
transform: scale(0.95);
}
&__textcontainer {
text-align: center;
}
&__navtext {
font-size: 9px;
font-weight: bold;
}
&__longtext {
color: $oslo-gray;
font-size: 8px;
}
&__imageflip {
-webkit-transform: scaleX(-1);
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
transform: scaleX(-1);
}
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SenderToRecipient from '../sender-to-recipient'
import { PageContainerFooter } from '../page-container'
import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './'
import { ConfirmPageContainerHeader, ConfirmPageContainerContent, ConfirmPageContainerNavigation } from './'
export default class ConfirmPageContainer extends Component {
static contextTypes = {
@ -43,6 +43,17 @@ export default class ConfirmPageContainer extends Component {
summaryComponent: PropTypes.node,
warning: PropTypes.string,
unapprovedTxCount: PropTypes.number,
// Navigation
totalTx: PropTypes.number,
positionOfCurrentTx: PropTypes.number,
nextTxId: PropTypes.string,
prevTxId: PropTypes.string,
showNavigation: PropTypes.bool,
onNextTx: PropTypes.func,
firstTx: PropTypes.string,
lastTx: PropTypes.string,
ofText: PropTypes.string,
requestsWaitingText: PropTypes.string,
// Footer
onCancelAll: PropTypes.func,
onCancel: PropTypes.func,
@ -79,11 +90,33 @@ export default class ConfirmPageContainer extends Component {
unapprovedTxCount,
assetImage,
warning,
totalTx,
positionOfCurrentTx,
nextTxId,
prevTxId,
showNavigation,
onNextTx,
firstTx,
lastTx,
ofText,
requestsWaitingText,
} = this.props
const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
return (
<div className="page-container">
<ConfirmPageContainerNavigation
totalTx={totalTx}
positionOfCurrentTx={positionOfCurrentTx}
nextTxId={nextTxId}
prevTxId={prevTxId}
showNavigation={showNavigation}
onNextTx={(txId) => onNextTx(txId)}
firstTx={firstTx}
lastTx={lastTx}
ofText={ofText}
requestsWaitingText={requestsWaitingText}
/>
<ConfirmPageContainerHeader
showEdit={showEdit}
onEdit={() => onEdit()}

View File

@ -1,6 +1,8 @@
export { default } from './confirm-page-container.component'
export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
export { default as ConfirmDetailRow } from './confirm-detail-row'
export { default as ConfirmPageContainerNavigation } from './confirm-page-container-navigation'
export {
default as ConfirmPageContainerContent,
ConfirmPageContainerSummary,

View File

@ -3,3 +3,5 @@
@import './confirm-page-container-header/index';
@import './confirm-detail-row/index';
@import './confirm-page-container-navigation/index';

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
import { isBalanceSufficient } from '../../send/send.utils'
import { DEFAULT_ROUTE } from '../../../routes'
import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../../routes'
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
@ -55,6 +55,7 @@ export default class ConfirmTransactionBase extends Component {
transactionStatus: PropTypes.string,
txData: PropTypes.object,
unapprovedTxCount: PropTypes.number,
currentNetworkUnapprovedTxs: PropTypes.object,
// Component props
action: PropTypes.string,
contentComponent: PropTypes.node,
@ -347,6 +348,32 @@ export default class ConfirmTransactionBase extends Component {
/>
)
}
handleNextTx (txId) {
const { history, clearConfirmTransaction } = this.props
if (txId) {
clearConfirmTransaction()
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${txId}`)
}
}
getNavigateTxData () {
const { currentNetworkUnapprovedTxs, txData: { id } = {} } = this.props
const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs).reverse()
const currentPosition = enumUnapprovedTxs.indexOf(id.toString())
return {
totalTx: enumUnapprovedTxs.length,
positionOfCurrentTx: currentPosition + 1,
nextTxId: enumUnapprovedTxs[currentPosition + 1],
prevTxId: enumUnapprovedTxs[currentPosition - 1],
showNavigation: enumUnapprovedTxs.length > 1,
firstTx: enumUnapprovedTxs[0],
lastTx: enumUnapprovedTxs[enumUnapprovedTxs.length - 1],
ofText: this.context.t('ofTextNofM'),
requestsWaitingText: this.context.t('requestsAwaitingAcknowledgement'),
}
}
render () {
const {
@ -376,6 +403,7 @@ export default class ConfirmTransactionBase extends Component {
const { name } = methodData
const { valid, errorKey } = this.getErrorKey()
const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData()
return (
<ConfirmPageContainer
@ -401,6 +429,16 @@ export default class ConfirmTransactionBase extends Component {
errorMessage={errorMessage || submitError}
errorKey={propsErrorKey || errorKey}
warning={warning}
totalTx={totalTx}
positionOfCurrentTx={positionOfCurrentTx}
nextTxId={nextTxId}
prevTxId={prevTxId}
showNavigation={showNavigation}
onNextTx={(txId) => this.handleNextTx(txId)}
firstTx={firstTx}
lastTx={lastTx}
ofText={ofText}
requestsWaitingText={requestsWaitingText}
disabled={!propsValid || !valid || submitting}
onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()}

View File

@ -73,9 +73,9 @@ const mapStateToProps = (state, props) => {
const currentNetworkUnapprovedTxs = R.filter(
({ metamaskNetworkId }) => metamaskNetworkId === network,
valuesFor(unapprovedTxs),
unapprovedTxs,
)
const unapprovedTxCount = currentNetworkUnapprovedTxs.length
const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length
return {
balance,
@ -104,6 +104,7 @@ const mapStateToProps = (state, props) => {
assetImage,
unapprovedTxs,
unapprovedTxCount,
currentNetworkUnapprovedTxs,
}
}

View File

@ -32,21 +32,15 @@ export default class ConfirmTransactionSwitch extends Component {
txData,
methodData: { name },
fetchingData,
isEtherTransaction,
} = this.props
const { id, txParams: { data } = {} } = txData
if (isConfirmDeployContract(txData)) {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
return <Redirect to={{ pathname }} />
}
if (fetchingData) {
return <Loading />
}
if (isEtherTransaction) {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
if (isConfirmDeployContract(txData)) {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
return <Redirect to={{ pathname }} />
}

View File

@ -37,13 +37,13 @@ export default class TransactionViewBalance extends PureComponent {
className="transaction-view-balance__primary-balance"
value={balance}
type={PRIMARY}
ethNumberOfDecimals={3}
ethNumberOfDecimals={4}
/>
<UserPreferencedCurrencyDisplay
className="transaction-view-balance__secondary-balance"
value={balance}
type={SECONDARY}
ethNumberOfDecimals={3}
ethNumberOfDecimals={4}
/>
</div>
)

View File

@ -370,11 +370,16 @@ export function setTransactionToConfirm (transactionId) {
dispatch(setFetchingData(true))
const methodData = await getMethodData(data)
dispatch(updateMethodData(methodData))
} catch (error) {
dispatch(updateMethodData({}))
dispatch(setFetchingData(false))
}
try {
const toSmartContract = await isSmartContractAddress(to)
dispatch(updateToSmartContract(toSmartContract))
dispatch(setFetchingData(false))
} catch (error) {
dispatch(updateMethodData({}))
dispatch(setFetchingData(false))
}

View File

@ -74,6 +74,7 @@ function reduceMetamask (state, action) {
case actions.CLEAR_NOTICES:
return extend(metamaskState, {
noActiveNotices: true,
nextUnreadNotice: undefined,
})
case actions.UPDATE_METAMASK_STATE:
@ -294,8 +295,10 @@ function reduceMetamask (state, action) {
amount: '0x0',
memo: '',
errors: {},
maxModeOn: false,
editingTransactionId: null,
forceGasMin: null,
toNickname: '',
},
})
@ -333,9 +336,9 @@ function reduceMetamask (state, action) {
})
case actions.SET_USE_BLOCKIE:
return extend(metamaskState, {
useBlockie: action.value,
})
return extend(metamaskState, {
useBlockie: action.value,
})
case actions.UPDATE_FEATURE_FLAGS:
return extend(metamaskState, {