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

Merge pull request #6683 from MetaMask/develop

Merge dev to master
This commit is contained in:
Thomas Huang 2019-06-04 14:55:56 -07:00 committed by GitHub
commit be91171194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2761 additions and 1842 deletions

View File

@ -1,24 +1,9 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
[*.json]
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@ -2,6 +2,20 @@
## Current Develop Branch
## 6.6.0 Mon Jun 03 2019
- [#6659](https://github.com/MetaMask/metamask-extension/pull/6659): Enable Ledger hardware wallet support on Firefox
- [#6671](https://github.com/MetaMask/metamask-extension/pull/6671): bugfix: reject enable promise on user rejection
- [#6625](https://github.com/MetaMask/metamask-extension/pull/6625): Ensures that transactions cannot be confirmed if gas limit is below 21000.
- [#6633](https://github.com/MetaMask/metamask-extension/pull/6633): Fix grammatical error in i18n endOfFlowMessage6
## 6.5.3 Thu May 16 2019
- [#6619](https://github.com/MetaMask/metamask-extension/pull/6619): bugfix: show extension window if locked regardless of approval
- [#6388](https://github.com/MetaMask/metamask-extension/pull/6388): Transactions/pending - check nonce against the network and mark as dropped if not included in a block
- [#6606](https://github.com/MetaMask/metamask-extension/pull/6606): Improve ENS Address Input
- [#6615](https://github.com/MetaMask/metamask-extension/pull/6615): Adds e2e test for removing imported accounts.
## 6.5.2 Wed May 15 2019
- [#6613](https://github.com/MetaMask/metamask-extension/pull/6613): Hardware Wallet Fix

View File

@ -1,52 +0,0 @@
<html>
<head>
<title>MetaMask</title>
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
img{
display: block;
}
html, body{
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.app{
position: relative;
width: 100%;
height: auto;
overflow: hidden;
}
img{
display: block;
width: 100%;
height: auto;
}
h2{
display: block;
width: 100%;
overflow: hidden;
position: absolute;
bottom: 20%;
left: 0;
color: #1b243d;
text-align: center;
}
h2 > a{
color: #1b243d;
}
</style>
</head>
<body>
<div class="app">
<img src="./images/404.png" alt="">
<h2>Powered by <a href="https://www.portal.network/">Portal Network</a></h2>
</div>
</body>
</html>

View File

@ -549,7 +549,7 @@
"message": "Be careful of phishing! MetaMask will never spontaneously ask for your seed phrase."
},
"endOfFlowMessage6": {
"message": "If you need to back your up seed phrase again, you can find it in Settings -> Security."
"message": "If you need to back up your seed phrase again, you can find it in Settings -> Security."
},
"endOfFlowMessage7": {
"message": "If you ever have questions or see something fishy, email support@metamask.io."
@ -1249,7 +1249,7 @@
"message": "Revert"
},
"remove": {
"message": "remove"
"message": "Remove"
},
"removeAccount": {
"message": "Remove account"

View File

@ -11,6 +11,9 @@
"exposeDescription": {
"message": "Esporre gli account al sito Web corrente. Utile per dapps legacy."
},
"chartOnlyAvailableEth": {
"message": "Grafico disponibile solo per le reti Ethereum."
},
"confirmExpose": {
"message": "Sei sicuro di voler esporre i tuoi account al sito web corrente?"
},
@ -41,6 +44,15 @@
"providerRequestInfo": {
"message": "Il dominio elencato di seguito sta tentando di richiedere l'accesso all'API Ethereum in modo che possa interagire con la blockchain di Ethereum. Controlla sempre di essere sul sito corretto prima di approvare l'accesso a Ethereum."
},
"about": {
"message": "Informazioni"
},
"aboutSettingsDescription": {
"message": "Version, centro di supporto e contatti."
},
"aboutUs": {
"message": "Chi siamo"
},
"accept": {
"message": "Accetta"
},
@ -71,6 +83,15 @@
"address": {
"message": "Indirizzo"
},
"addNetwork": {
"message": "Aggiungi Rete"
},
"advanced": {
"message": "Avanzate"
},
"advancedSettingsDescription": {
"message": "Accedi alle funzionalità sviluppatore, download dei log di Stato, Reset Account, imposta reti di test e RPC personalizzata."
},
"advancedOptions": {
"message": "Opzioni Avanzate"
},
@ -89,8 +110,14 @@
"addAcquiredTokens": {
"message": "Aggiungi i token che hai acquistato usando MetaMask"
},
"advanced": {
"message": "Avanzato"
"agreeTermsOfService": {
"message": "Accetto i termini di servizio"
},
"allDone": {
"message": "Tutto Fatto"
},
"alreadyHaveSeedPhrase": {
"message": "No, ho già una frase seed"
},
"amount": {
"message": "Importo"
@ -115,6 +142,9 @@
"approved": {
"message": "Approvato"
},
"asset": {
"message": "Asset"
},
"attemptingConnect": {
"message": "Tentativo di connessione alla blockchain."
},
@ -127,6 +157,12 @@
"attributions": {
"message": "Attribuzioni"
},
"autoLogoutTimeLimit": {
"message": "Timer di Logout Automatico (minuti)"
},
"autoLogoutTimeLimitDescription": {
"message": "Imposta il tempo di inattività dopo il quale MetaMask fa il log out automaticamente"
},
"available": {
"message": "Disponibile"
},
@ -158,6 +194,13 @@
"message": "deve essere maggiore o uguale a $1 e minore o uguale a $2.",
"description": "aiuto per inserire un input esadecimale come decimale"
},
"blockExplorerUrl": {
"message": "Block Explorer"
},
"blockExplorerView": {
"message": "Visualizza account su $1",
"description": "$1 replaced by URL for custom block explorer"
},
"blockiesIdenticon": {
"message": "Usa le icone Blockie"
},
@ -179,6 +222,12 @@
"buyCoinbaseExplainer": {
"message": "Coinbase è il servizio più popolare al mondo per comprare e vendere Bitcoin, Ethereum e Litecoin."
},
"buyWithWyre": {
"message": "Compra ETH con Wyre"
},
"buyWithWyreDescription": {
"message": "Wyre ti consente di usare la carta di credito per depositare ETH direttamente nel tuo account MetaMask."
},
"buyCoinSwitch": {
"message": "Compra su CoinSwitch"
},
@ -191,6 +240,9 @@
"ok": {
"message": "Ok"
},
"optionalBlockExplorerUrl": {
"message": "URL del Block Explorer (opzionale)"
},
"cancel": {
"message": "Annulla"
},
@ -206,6 +258,9 @@
"cancelN": {
"message": "Annulla tutte le transazioni relative a $1"
},
"chainId": {
"message": "Chain ID"
},
"classicInterface": {
"message": "Usa l'interfaccia classica"
},
@ -224,6 +279,9 @@
"chromeRequiredForHardwareWallets": {
"message": "Devi usare MetaMask con Google Chrome per connettere il tuo Portafoglio Hardware"
},
"company": {
"message": "Azienda"
},
"confirm": {
"message": "Conferma"
},
@ -245,6 +303,9 @@
"confirmTransaction": {
"message": "Conferma Transazione"
},
"congratulations": {
"message": "Congratulazioni"
},
"connectHardwareWallet": {
"message": "Connetti Portafoglio Hardware"
},
@ -272,6 +333,12 @@
"connectingToRinkeby": {
"message": "Connessione alla Rete di test Rinkeby"
},
"connectingToLocalhost": {
"message": "Connessione a Localhost 8545"
},
"connectingToGoerli": {
"message": "Connessione alla Rete di Test Goerli"
},
"connectingToUnknown": {
"message": "Connessione ad una Rete Sconosciuta"
},
@ -287,6 +354,9 @@
"continueToCoinbase": {
"message": "Continua su Coinbase"
},
"continueToWyre": {
"message": "Continua su Wyre"
},
"continueToCoinSwitch": {
"message": "Continua su CoinSwitch"
},
@ -314,6 +384,12 @@
"copyAddress": {
"message": "Copia l'indirizzo"
},
"copyTransactionId": {
"message": "Copia ID Transazione"
},
"copiedTransactionId": {
"message": "ID Transazione Copiato"
},
"copyToClipboard": {
"message": "Copia negli appunti"
},
@ -329,6 +405,9 @@
"createAccount": {
"message": "Crea Account"
},
"createAWallet": {
"message": "Crea un Wallet"
},
"createDen": {
"message": "Crea"
},
@ -439,6 +518,9 @@
"edit": {
"message": "Modifica"
},
"editNetwork": {
"message": "Modifica Rete"
},
"editAccountName": {
"message": "Modifica Nome Account"
},
@ -451,6 +533,30 @@
"encryptNewDen": {
"message": "Cripta il tuo nuovo DEN"
},
"endOfFlowMessage1": {
"message": "Hai passato il test - tieni la tua frase seed al sicuro, è tua responsabilità!"
},
"endOfFlowMessage2": {
"message": "Suggerimenti su come tenerla al sicuro"
},
"endOfFlowMessage3": {
"message": "Salva un backup in più di un posto."
},
"endOfFlowMessage4": {
"message": "Non condividerla mai con nessuno."
},
"endOfFlowMessage5": {
"message": "Stai attento al phishing! MetaMask non ti chiederà mai spontaneamente la tua frase seed."
},
"endOfFlowMessage6": {
"message": "Se vorrai fare nuovamente un backup della frase, la puoi trovare in Impostazioni -> Sicurezza & Privacy."
},
"endOfFlowMessage7": {
"message": "Se hai delle domande o vedi delle attività sospette, manda una mail a support@metamask.io."
},
"endOfFlowMessage8": {
"message": "MetaMask non può recuperare la tua frase seed. Impara di più."
},
"ensNameNotFound": {
"message": "Nome ENS non trovato"
},
@ -571,6 +677,12 @@
"gasPriceRequired": {
"message": "Prezzo Gas Richiesto"
},
"general": {
"message": "Generale"
},
"generalSettingsDescription": {
"message": "Conversione moneta, moneta primaria, lingua, icone blockie"
},
"generatingTransaction": {
"message": "Generando la transazione"
},
@ -584,10 +696,16 @@
"getHelp": {
"message": "Aiuto."
},
"getStarted": {
"message": "Inizia"
},
"greaterThanMin": {
"message": "deve essere maggiore o uguale a $1.",
"description": "aiuto per inserire un input esadecimale come decimale"
},
"happyToSeeYou": {
"message": "Siamo contenti di vederti."
},
"hardware": {
"message": "hardware"
},
@ -650,6 +768,12 @@
"importDen": {
"message": "Importa un DEN Esistente"
},
"importWallet": {
"message": "Importa Portafoglio"
},
"importYourExisting": {
"message": "Importa il tuo portafoglio esistente usando la tua frase seed a 12 parole"
},
"imported": {
"message": "Importato",
"description": "stato che conferma che un account è stato totalmente caricato nel portachiavi"
@ -687,6 +811,9 @@
"knownAddressRecipient": {
"message": "Indirizzo del contratto conosciuto."
},
"invalidAddressRecipientNotEthNetwork": {
"message": "Non rete ETH, inserisci caratteri minuscoli"
},
"invalidGasParams": {
"message": "Parametri del Gas non validi"
},
@ -727,10 +854,16 @@
"ledgerAccountRestriction": {
"message": "E' necessario utilizzare l'ultimo account prima di poterne aggiungere uno nuovo."
},
"legal": {
"message": "Informazioni Legali"
},
"lessThanMax": {
"message": "deve essere minore o uguale a $1.",
"description": "aiuto per inserire un input esadecimale come decimale"
},
"letsGoSetUp": {
"message": "Si, iniziamo!"
},
"likeToAddTokens": {
"message": "Vorresti aggiungere questi token?"
},
@ -794,6 +927,12 @@
"minutesShorthand": {
"message": "Min"
},
"mobileSyncTitle": {
"message": "Sincronizza account con il dispositivo mobile"
},
"mobileSyncText": {
"message": "Per favore inserisci la password per confermare che sei te!"
},
"myAccounts": {
"message": "Miei Account"
},
@ -814,9 +953,15 @@
"negativeETH": {
"message": "Non puoi inviare una quantità di ETH negativa."
},
"networkName": {
"message": "Nome Rete"
},
"networks": {
"message": "Reti"
},
"networkSettingsDescription": {
"message": "Aggiungi e modifica reti RPC personalizzate"
},
"nevermind": {
"message": "Non importa"
},
@ -842,7 +987,22 @@
"newNetwork": {
"message": "Nuova Rete"
},
"rpcURL": {
"newToMetaMask": {
"message": "Nuovo a MetaMask?"
},
"noAlreadyHaveSeed": {
"message": "No, ho già una frase seed"
},
"protectYourKeys": {
"message": "Proteggi le tue chiavi!"
},
"protectYourKeysMessage1": {
"message": "Stai attento con la tua frase seed - ci sono stati report di siti web che hanno tentato di imitare MetaMask. MetaMask non ti chiederà mai la tua frase seed!"
},
"protectYourKeysMessage2": {
"message": "Tieni la tua frase al sicuro. Se vedi qualcosa di sospetto, o non sei sicuro di un sito web, manda una mail a support@metamask.io"
},
"rpcUrl": {
"message": "Nuovo URL RPC"
},
"showAdvancedOptions": {
@ -884,6 +1044,9 @@
"noTransactions": {
"message": "Nessuna Transazione"
},
"notEnoughGas": {
"message": "Gas Non Sufficiente"
},
"notFound": {
"message": "Non Trovata"
},
@ -934,6 +1097,12 @@
"originalTotal": {
"message": "Totale Precedente"
},
"participateInMetaMetrics": {
"message": "Participa in MetaMetrics"
},
"participateInMetaMetricsDescription": {
"message": "Participa in MetaMetrics per aiutarci a rendere MetaMask migliore"
},
"password": {
"message": "Password"
},
@ -1097,6 +1266,9 @@
"ropsten": {
"message": "Rete di test Ropsten"
},
"goerli": {
"message": "Rete di test Goerli"
},
"rpc": {
"message": "RPC Personalizzata"
},
@ -1147,6 +1319,12 @@
"secretPhrase": {
"message": "Inserisci la tua frase segreta di dodici parole per ripristinare la cassaforte."
},
"securityAndPrivacy": {
"message": "Sicurezza & Privacy"
},
"securitySettingsDescription": {
"message": "Impostazioni sulla Privacy e sulla frase seed del portafoglio"
},
"secondsShorthand": {
"message": "Sec"
},
@ -1207,6 +1385,9 @@
"selectAnAccountHelp": {
"message": "Selezione l'account da visualizzare in MetaMask"
},
"selectAnAsset": {
"message": "Seleziona un Asset"
},
"selectAHigherGasFee": {
"message": "Seleziona un costo in gas maggiore per accelerare l'elaborazione della transazione.*"
},
@ -1229,7 +1410,13 @@
"message": "Controlli gas avanzati"
},
"showAdvancedGasInlineDescription": {
"message": "Seleziona qui per visualizzare i controlli su prezzo e limite del gas nelle schermate di invio e conferma."
"message": "Seleziona per visualizzare i controlli su prezzo e limite del gas nelle schermate di invio e conferma."
},
"showFiatConversionInTestnets": {
"message": "Mostra conversione nelle reti di test"
},
"showFiatConversionInTestnetsDescription": {
"message": "Seleziona se vuoi vedere la conversione in valuta fiat nelle reti di test"
},
"showPrivateKeys": {
"message": "Mostra Chiave Privata"
@ -1330,9 +1517,33 @@
"supportCenter": {
"message": "Visita il nostro Centro di Supporto"
},
"symbol": {
"message": "Simbolo"
},
"symbolBetweenZeroTwelve": {
"message": "Il simbolo deve essere lungo tra 0 e 12 caratteri."
},
"syncWithMobile": {
"message": "Sincronizza con dispositivo mobile"
},
"syncWithMobileTitle": {
"message": "Sincronizza con dispositivo mobile"
},
"syncWithMobileDesc": {
"message": "Puoi sincronizzare i tuoi account e le tue informazioni con il tuo dispositivo mobile. Apri l'app di MetaMask, vai su \"Impostazioni\" e tocca \"Sincronizza da Estensione sul Browser\""
},
"syncWithMobileDescNewUsers": {
"message": "Se hai appena aperto l'app di MetaMask per la prima volta, segui i passaggi sul tuo telefono."
},
"syncWithMobileScanThisCode": {
"message": "Scansiona questo codice con l'app di MetaMask"
},
"syncWithMobileBeCareful": {
"message": "Assicurati che nessun'altro stia guardando al tuo schermo quando scansioni questo codice"
},
"syncWithMobileComplete": {
"message": "I tuoi dati sono stati sincronizzati con successo. Goditi l'app di MetaMask!"
},
"takesTooLong": {
"message": "Ci sta mettendo troppo?"
},
@ -1342,6 +1553,9 @@
"testFaucet": {
"message": "Prova Faucet"
},
"thisWillCreate": {
"message": "Questo creerà un nuovo portafoglio e frase seed"
},
"tips": {
"message": "Suggerimenti"
},
@ -1364,6 +1578,9 @@
"tokenBalance": {
"message": "Bilancio Token:"
},
"tokenContractAddress": {
"message": "Indirizzo Contratto Token"
},
"tokenSelection": {
"message": "Cerca un token o seleziona dalla lista di token più popolari."
},
@ -1525,9 +1742,15 @@
"viewAccount": {
"message": "Vedi Account"
},
"viewOnCustomBlockExplorer": {
"message": "Vedi su $1"
},
"viewOnEtherscan": {
"message": "Vedi su Etherscan"
},
"viewNetworkInfo": {
"message": "Visualizza Informazioni Rete"
},
"visitWebSite": {
"message": "Visita il nostro sito web"
},
@ -1573,6 +1796,9 @@
"yourUniqueAccountImageDescription2": {
"message": "Vedrai questa immagine ogni volta che dovrai confermare una transazione."
},
"yourUniqueAccountImageDescription3": {
"message": "MetaMask non ti chiederà mai la tua frase seed!"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Prezzo del gas maggiore di zero"
}

View File

@ -1,79 +0,0 @@
<html>
<head>
<title>MetaMask Error</title>
<link href="https://fonts.googleapis.com/css?family=Rokkitt" rel="stylesheet">
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
img{
display: block;
}
html, body{
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
@keyframes logoAmin{
from {transform: scale(1);}
50%{transform: scale(1.1);}
to {transform: scale(1);}
}
.errorBox{
width: 70%;
height: auto;
overflow: hidden;
background-image: url("./images/deadface.png");
background-repeat: no-repeat;
background-position: 100% 50%;
background-size: auto 90%;
padding: 5px;
}
.errorBox > img{
width: 100px;
height: auto;
margin-bottom: 25px;
animation: logoAmin 1s infinite linear;
}
.errorBox > h1, .errorBox > h2{
letter-spacing: 2px;
}
.errorBox > h1{
color: #9b9b9b;
font-size: 40px;
}
.errorBox > h2{
color: #1b243d;
font-size: 20px;
padding-top: 5px;
}
.errorBox > h2 >a{
color: #1b243d;
}
.errorBox > h2 >a:hover{
color: #44588e;
}
.errorBox > h1 > span{
color: #33559f;
}
</style>
</head>
<body>
<div class="errorBox">
<img src="./images/logo.png" alt="">
<h1><span id="name"></span> not found</h1>
<h2>Powered by <a href="https://www.portal.network/">Portal Network</a></h2>
</div>
<script>
let index = location.href.lastIndexOf("?name=")
let name = location.href.slice(index + 6)
document.getElementById("name").innerHTML = name
</script>
</body>
</html>

1
app/images/enslogo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 72.52 80.95"><defs><style>.cls-3{fill:#a0a8d4}</style><linearGradient id="linear-gradient" x1="41.95" y1="2.57" x2="12.57" y2="34.42" gradientUnits="userSpaceOnUse"><stop offset=".58" stop-color="#a0a8d4"/><stop offset=".73" stop-color="#8791c7"/><stop offset=".91" stop-color="#6470b4"/></linearGradient><linearGradient id="linear-gradient-2" x1="42.57" y1="81.66" x2="71.96" y2="49.81" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="42.26" y1="1.24" x2="42.26" y2="82.84" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#513eff"/><stop offset=".18" stop-color="#5157ff"/><stop offset=".57" stop-color="#5298ff"/><stop offset="1" stop-color="#52e5ff"/></linearGradient></defs><g style="isolation:isolate"><g id="Layer_1" data-name="Layer 1"><path d="M15.28 34.39c.8 1.71 2.78 5.09 2.78 5.09L40.95 1.64l-22.34 15.6a9.75 9.75 0 0 0-3.18 3.5 16.19 16.19 0 0 0-.15 13.65z" transform="translate(-6 -1.64)" fill="url(#linear-gradient)"/><path class="cls-3" d="M6.21 46.85a25.47 25.47 0 0 0 10 18.51l24.71 17.23s-15.46-22.28-28.5-44.45a22.39 22.39 0 0 1-2.62-7.56 12.1 12.1 0 0 1 0-3.63c-.34.63-1 1.92-1 1.92a29.35 29.35 0 0 0-2.67 8.55 52.28 52.28 0 0 0 .08 9.43z" transform="translate(-6 -1.64)"/><path d="M69.25 49.84c-.8-1.71-2.78-5.09-2.78-5.09L43.58 82.59 65.92 67a9.75 9.75 0 0 0 3.18-3.5 16.19 16.19 0 0 0 .15-13.66z" transform="translate(-6 -1.64)" fill="url(#linear-gradient-2)"/><path class="cls-3" d="M78.32 37.38a25.47 25.47 0 0 0-10-18.51L43.61 1.64s15.45 22.28 28.5 44.45a22.39 22.39 0 0 1 2.61 7.56 12.1 12.1 0 0 1 0 3.63c.34-.63 1-1.92 1-1.92a29.35 29.35 0 0 0 2.67-8.55 52.28 52.28 0 0 0-.07-9.43z" transform="translate(-6 -1.64)"/><path d="M15.43 20.74a9.75 9.75 0 0 1 3.18-3.5l22.34-15.6-22.89 37.85s-2-3.38-2.78-5.09a16.19 16.19 0 0 1 .15-13.66zM6.21 46.85a25.47 25.47 0 0 0 10 18.51l24.71 17.23s-15.46-22.28-28.5-44.45a22.39 22.39 0 0 1-2.62-7.56 12.1 12.1 0 0 1 0-3.63c-.34.63-1 1.92-1 1.92a29.35 29.35 0 0 0-2.67 8.55 52.28 52.28 0 0 0 .08 9.43zm63 3c-.8-1.71-2.78-5.09-2.78-5.09L43.58 82.59 65.92 67a9.75 9.75 0 0 0 3.18-3.5 16.19 16.19 0 0 0 .15-13.66zm9.07-12.46a25.47 25.47 0 0 0-10-18.51L43.61 1.64s15.45 22.28 28.5 44.45a22.39 22.39 0 0 1 2.61 7.56 12.1 12.1 0 0 1 0 3.63c.34-.63 1-1.92 1-1.92a29.35 29.35 0 0 0 2.67-8.55 52.28 52.28 0 0 0-.07-9.43z" transform="translate(-6 -1.64)" style="mix-blend-mode:color" fill="url(#linear-gradient-3)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -11,10 +11,10 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 256px;
text-align: center;
}
#logo {
width: 100%;
width: 256px;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
@ -33,13 +33,8 @@
</head>
<body>
<div id="div-logo">
<img id="logo" src="./images/loginglogo.svg">
<img id="logo" src="./images/enslogo.svg">
<h1 class="center">MetaMask is querying ENS ...</h1>
</div>
<script type="text/javascript">
// redirect to 404 after one minute
setTimeout(() => {
location.href = './404.html'
}, 60000)
</script>
</body>
</html>

View File

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

View File

@ -1,205 +0,0 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
const log = require('loglevel')
// every ten minutes
const POLLING_INTERVAL = 600000
class CurrencyController {
/**
* Controller responsible for managing data associated with the currently selected currency.
*
* @typedef {Object} CurrencyController
* @param {object} opts Overrides the defaults for the initial state of this.store
* @property {array} opts.initState initializes the the state of the CurrencyController. Can contain an
* currentCurrency, conversionRate and conversionDate properties
* @property {string} currentCurrency A 2-4 character shorthand that describes a specific currency, currently
* selected by the user
* @property {number} conversionRate The conversion rate from ETH to the selected currency.
* @property {string} conversionDate The date at which the conversion rate was set. Expressed in in milliseconds
* since midnight of January 1, 1970
* @property {number} conversionInterval The id of the interval created by the scheduleConversionInterval method.
* Used to clear an existing interval on subsequent calls of that method.
* @property {string} nativeCurrency The ticker/symbol of the native chain currency
*
*/
constructor (opts = {}) {
const initState = extend({
currentCurrency: 'usd',
conversionRate: 0,
conversionDate: 'N/A',
nativeCurrency: 'ETH',
}, opts.initState)
this.store = new ObservableStore(initState)
}
//
// PUBLIC METHODS
//
/**
* A getter for the nativeCurrency property
*
* @returns {string} A 2-4 character shorthand that describes the specific currency
*
*/
getNativeCurrency () {
return this.store.getState().nativeCurrency
}
/**
* A setter for the nativeCurrency property
*
* @param {string} nativeCurrency The new currency to set as the nativeCurrency in the store
*
*/
setNativeCurrency (nativeCurrency) {
this.store.updateState({
nativeCurrency,
ticker: nativeCurrency,
})
}
/**
* A getter for the currentCurrency property
*
* @returns {string} A 2-4 character shorthand that describes a specific currency, currently selected by the user
*
*/
getCurrentCurrency () {
return this.store.getState().currentCurrency
}
/**
* A setter for the currentCurrency property
*
* @param {string} currentCurrency The new currency to set as the currentCurrency in the store
*
*/
setCurrentCurrency (currentCurrency) {
this.store.updateState({ currentCurrency })
}
/**
* A getter for the conversionRate property
*
* @returns {string} The conversion rate from ETH to the selected currency.
*
*/
getConversionRate () {
return this.store.getState().conversionRate
}
/**
* A setter for the conversionRate property
*
* @param {number} conversionRate The new rate to set as the conversionRate in the store
*
*/
setConversionRate (conversionRate) {
this.store.updateState({ conversionRate })
}
/**
* A getter for the conversionDate property
*
* @returns {string} The date at which the conversion rate was set. Expressed in milliseconds since midnight of
* January 1, 1970
*
*/
getConversionDate () {
return this.store.getState().conversionDate
}
/**
* A setter for the conversionDate property
*
* @param {number} conversionDate The date, expressed in milliseconds since midnight of January 1, 1970, that the
* conversionRate was set
*
*/
setConversionDate (conversionDate) {
this.store.updateState({ conversionDate })
}
/**
* Updates the conversionRate and conversionDate properties associated with the currentCurrency. Updated info is
* fetched from an external API
*
*/
async updateConversionRate () {
let currentCurrency, nativeCurrency
try {
currentCurrency = this.getCurrentCurrency()
nativeCurrency = this.getNativeCurrency()
// select api
let apiUrl
if (nativeCurrency === 'ETH') {
// ETH
apiUrl = `https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`
} else {
// ETC
apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}`
}
// attempt request
let response
try {
response = await fetch(apiUrl)
} catch (err) {
log.error(new Error(`CurrencyController - Failed to request currency from Infura:\n${err.stack}`))
return
}
// parse response
let rawResponse
let parsedResponse
try {
rawResponse = await response.text()
parsedResponse = JSON.parse(rawResponse)
} catch (err) {
log.error(new Error(`CurrencyController - Failed to parse response "${rawResponse}"`))
return
}
// set conversion rate
if (nativeCurrency === 'ETH') {
// ETH
this.setConversionRate(Number(parsedResponse.bid))
this.setConversionDate(Number(parsedResponse.timestamp))
} else {
// ETC
if (parsedResponse[currentCurrency.toUpperCase()]) {
this.setConversionRate(Number(parsedResponse[currentCurrency.toUpperCase()]))
this.setConversionDate(parseInt((new Date()).getTime() / 1000))
} else {
this.setConversionRate(0)
this.setConversionDate('N/A')
}
}
} catch (err) {
// reset current conversion rate
log.warn(`MetaMask - Failed to query currency conversion:`, nativeCurrency, currentCurrency, err)
this.setConversionRate(0)
this.setConversionDate('N/A')
// throw error
log.error(new Error(`CurrencyController - Failed to query rate for currency "${currentCurrency}":\n${err.stack}`))
return
}
}
/**
* Creates a new poll, using setInterval, to periodically call updateConversionRate. The id of the interval is
* stored at the controller's conversionInterval property. If it is called and such an id already exists, the
* previous interval is clear and a new one is created.
*
*/
scheduleConversionInterval () {
if (this.conversionInterval) {
clearInterval(this.conversionInterval)
}
this.conversionInterval = setInterval(() => {
this.updateConversionRate()
}, POLLING_INTERVAL)
}
}
module.exports = CurrencyController

View File

@ -38,7 +38,8 @@ class ProviderApprovalController extends SafeEventEmitter {
// only handle requestAccounts
if (req.method !== 'eth_requestAccounts') return next()
// if already approved or privacy mode disabled, return early
if (this.shouldExposeAccounts(origin)) {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
if (this.shouldExposeAccounts(origin) && isUnlocked) {
res.result = [this.preferencesController.getSelectedAddress()]
return
}

View File

@ -30,7 +30,7 @@ class TokenRatesController {
async updateExchangeRates () {
if (!this.isActive) { return }
const contractExchangeRates = {}
const nativeCurrency = this.currency ? this.currency.getState().nativeCurrency.toLowerCase() : 'eth'
const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth'
const pairs = this._tokens.map(token => token.address).join(',')
const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}`
if (this._tokens.length > 0) {

View File

@ -17,7 +17,7 @@ const {
const TransactionStateManager = require('./tx-state-manager')
const TxGasUtil = require('./tx-gas-utils')
const PendingTransactionTracker = require('./pending-tx-tracker')
const NonceTracker = require('./nonce-tracker')
const NonceTracker = require('nonce-tracker')
const txUtils = require('./lib/util')
const cleanErrorStack = require('../../lib/cleanErrorStack')
const log = require('loglevel')
@ -555,6 +555,7 @@ class TransactionController extends EventEmitter {
})
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId))
this.pendingTxTracker.on('tx:dropped', this.txStateManager.setTxStatusDropped.bind(this.txStateManager))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber

View File

@ -1,161 +0,0 @@
const EthQuery = require('ethjs-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
/**
@param opts {Object}
@param {Object} opts.provider a ethereum provider
@param {Function} opts.getPendingTransactions a function that returns an array of txMeta
whosee status is `submitted`
@param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta
whose status is `confirmed`
@class
*/
class NonceTracker {
constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
this.blockTracker = blockTracker
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions
this.lockMap = {}
}
/**
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
*/
async getGlobalLock () {
const globalMutex = this._lookupMutex('global')
// await global mutex free
const releaseLock = await globalMutex.acquire()
return { releaseLock }
}
/**
* @typedef NonceDetails
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
* @property {number} highestSuggested - The maximum between the other two, the number returned.
*/
/**
this will return an object with the `nextNonce` `nonceDetails` of type NonceDetails, and the releaseLock
Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding).
@param address {string} the hex string for the address whose nonce we are calculating
@returns {Promise<NonceDetails>}
*/
async getNonceLock (address) {
// await global mutex free
await this._globalMutexFree()
// await lock free, then take lock
const releaseLock = await this._takeMutex(address)
try {
// evaluate multiple nextNonce strategies
const nonceDetails = {}
const networkNonceResult = await this._getNetworkNextNonce(address)
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const nextNetworkNonce = networkNonceResult.nonce
const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
const pendingTxs = this.getPendingTransactions(address)
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
nonceDetails.params = {
highestLocallyConfirmed,
highestSuggested,
nextNetworkNonce,
}
nonceDetails.local = localNonceResult
nonceDetails.network = networkNonceResult
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
} catch (err) {
// release lock if we encounter an error
releaseLock()
throw err
}
}
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const releaseLock = await globalMutex.acquire()
releaseLock()
}
async _takeMutex (lockId) {
const mutex = this._lookupMutex(lockId)
const releaseLock = await mutex.acquire()
return releaseLock
}
_lookupMutex (lockId) {
let mutex = this.lockMap[lockId]
if (!mutex) {
mutex = new Mutex()
this.lockMap[lockId] = mutex
}
return mutex
}
async _getNetworkNextNonce (address) {
// calculate next nonce
// we need to make sure our base count
// and pending count are from the same block
const blockNumber = await this.blockTracker.getLatestBlock()
const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber)
const baseCount = baseCountBN.toNumber()
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
const nonceDetails = { blockNumber, baseCount }
return { name: 'network', nonce: baseCount, details: nonceDetails }
}
_getHighestLocallyConfirmed (address) {
const confirmedTransactions = this.getConfirmedTransactions(address)
const highest = this._getHighestNonce(confirmedTransactions)
return Number.isInteger(highest) ? highest + 1 : 0
}
_getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
const highestNonce = Math.max.apply(null, nonces)
return highestNonce
}
/**
@typedef {object} highestContinuousFrom
@property {string} - name the name for how the nonce was calculated based on the data used
@property {number} - nonce the next suggested nonce
@property {object} - details the provided starting nonce that was used (for debugging)
*/
/**
@param txList {array} - list of txMeta's
@param startPoint {number} - the highest known locally confirmed nonce
@returns {highestContinuousFrom}
*/
_getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
let highest = startPoint
while (nonces.includes(highest)) {
highest++
}
return { name: 'local', nonce: highest, details: { startPoint, highest } }
}
}
module.exports = NonceTracker

View File

@ -22,6 +22,7 @@ const EthQuery = require('ethjs-query')
class PendingTransactionTracker extends EventEmitter {
constructor (config) {
super()
this.droppedBuffer = {}
this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
this.getPendingTransactions = config.getPendingTransactions
@ -139,22 +140,42 @@ class PendingTransactionTracker extends EventEmitter {
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
noTxHashErr.name = 'NoTxHashError'
this.emit('tx:failed', txId, noTxHashErr)
return
}
// If another tx with the same nonce is mined, set as failed.
// If another tx with the same nonce is mined, set as dropped.
const taken = await this._checkIfNonceIsTaken(txMeta)
if (taken) {
const nonceTakenErr = new Error('Another transaction with this nonce has been mined.')
nonceTakenErr.name = 'NonceTakenErr'
return this.emit('tx:failed', txId, nonceTakenErr)
let dropped
try {
// check the network if the nonce is ahead the tx
// and the tx has not been mined into a block
dropped = await this._checkIftxWasDropped(txMeta)
// the dropped buffer is in case we ask a node for the tx
// that is behind the node we asked for tx count
// IS A SECURITY FOR HITTING NODES IN INFURA THAT COULD GO OUT
// OF SYNC.
// on the next block event it will return fire as dropped
if (dropped && !this.droppedBuffer[txHash]) {
this.droppedBuffer[txHash] = true
dropped = false
} else if (dropped && this.droppedBuffer[txHash]) {
// clean up
delete this.droppedBuffer[txHash]
}
} catch (e) {
log.error(e)
}
if (taken || dropped) {
return this.emit('tx:dropped', txId)
}
// get latest transaction status
try {
const txParams = await this.query.getTransactionByHash(txHash)
if (!txParams) return
if (txParams.blockNumber) {
const { blockNumber } = await this.query.getTransactionByHash(txHash) || {}
if (blockNumber) {
this.emit('tx:confirmed', txId)
}
} catch (err) {
@ -165,6 +186,22 @@ class PendingTransactionTracker extends EventEmitter {
this.emit('tx:warning', txMeta, err)
}
}
/**
checks to see if if the tx's nonce has been used by another transaction
@param txMeta {Object} - txMeta object
@emits tx:dropped
@returns {boolean}
*/
async _checkIftxWasDropped (txMeta) {
const { txParams: { nonce, from }, hash } = txMeta
const nextNonce = await this.query.getTransactionCount(from)
const { blockNumber } = await this.query.getTransactionByHash(hash) || {}
if (!blockNumber && parseInt(nextNonce) > parseInt(nonce)) {
return true
}
return false
}
/**
checks to see if a confirmed txMeta has the same nonce

View File

@ -33,8 +33,8 @@ inpageProvider.setMaxListeners(100)
inpageProvider.enable = function ({ force } = {}) {
return new Promise((resolve, reject) => {
inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => {
if (error) {
reject(error)
if (error || response.error) {
reject(error || response.error)
} else {
resolve(response.result)
}

View File

@ -1,2 +1,2 @@
module.exports =
[{'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': 'ret', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': 'ret', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': 'ret', 'type': 'string'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes32'}], 'name': 'ContentChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}]
[{'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': '', 'type': 'bytes32'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'stateMutability': 'pure', 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'key', 'type': 'string'}, {'name': 'value', 'type': 'string'}], 'name': 'setText', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes'}], 'name': 'setContenthash', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'key', 'type': 'string'}], 'name': 'text', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'contenthash', 'outputs': [{'name': '', 'type': 'bytes'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'indexedKey', 'type': 'string'}, {'indexed': false, 'name': 'key', 'type': 'string'}], 'name': 'TextChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes'}], 'name': 'ContenthashChanged', 'type': 'event'}]

View File

@ -1,9 +1,9 @@
const namehash = require('eth-ens-namehash')
const multihash = require('multihashes')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')
const registrarAbi = require('./contracts/registrar')
const registryAbi = require('./contracts/registry')
const resolverAbi = require('./contracts/resolver')
const contentHash = require('content-hash')
module.exports = resolveEnsToIpfsContentId
@ -12,37 +12,47 @@ async function resolveEnsToIpfsContentId ({ provider, name }) {
const eth = new Eth(provider)
const hash = namehash.hash(name)
const contract = new EthContract(eth)
// lookup registrar
// lookup registry
const chainId = Number.parseInt(await eth.net_version(), 10)
const registrarAddress = getRegistrarForChainId(chainId)
if (!registrarAddress) {
throw new Error(`EnsIpfsResolver - no known ens-ipfs registrar for chainId "${chainId}"`)
const registryAddress = getRegistryForChainId(chainId)
if (!registryAddress) {
throw new Error(`EnsIpfsResolver - no known ens-ipfs registry for chainId "${chainId}"`)
}
const Registrar = contract(registrarAbi).at(registrarAddress)
const Registry = contract(registryAbi).at(registryAddress)
// lookup resolver
const resolverLookupResult = await Registrar.resolver(hash)
const resolverLookupResult = await Registry.resolver(hash)
const resolverAddress = resolverLookupResult[0]
if (hexValueIsEmpty(resolverAddress)) {
throw new Error(`EnsIpfsResolver - no resolver found for name "${name}"`)
}
const Resolver = contract(resolverAbi).at(resolverAddress)
const isEIP1577Compliant = await Resolver.supportsInterface('0xbc1c58d1')
const isLegacyResolver = await Resolver.supportsInterface('0xd8389dc5')
if (isEIP1577Compliant[0]) {
const contentLookupResult = await Resolver.contenthash(hash)
const rawContentHash = contentLookupResult[0]
const decodedContentHash = contentHash.decode(rawContentHash)
const type = contentHash.getCodec(rawContentHash)
return {type: type, hash: decodedContentHash}
}
if (isLegacyResolver[0]) {
// lookup content id
const contentLookupResult = await Resolver.content(hash)
const contentHash = contentLookupResult[0]
if (hexValueIsEmpty(contentHash)) {
const content = contentLookupResult[0]
if (hexValueIsEmpty(content)) {
throw new Error(`EnsIpfsResolver - no content ID found for name "${name}"`)
}
const nonPrefixedHex = contentHash.slice(2)
const buffer = multihash.fromHexString(nonPrefixedHex)
const contentId = multihash.toB58String(multihash.encode(buffer, 'sha2-256'))
return contentId
return {type: 'swarm-ns', hash: content.slice(2)}
}
throw new Error(`EnsIpfsResolver - the resolver for name "${name}" is not standard, it should either supports contenthash() or content()`)
}
function hexValueIsEmpty (value) {
return [undefined, null, '0x', '0x0', '0x0000000000000000000000000000000000000000000000000000000000000000'].includes(value)
}
function getRegistrarForChainId (chainId) {
function getRegistryForChainId (chainId) {
switch (chainId) {
// mainnet
case 1:
@ -50,5 +60,11 @@ function getRegistrarForChainId (chainId) {
// ropsten
case 3:
return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
// rinkeby
case 4:
return '0xe7410170f87102df0055eb195163a03b7f2bff4a'
// goerli
case 5:
return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
}
}

View File

@ -37,27 +37,25 @@ function setupEnsIpfsResolver ({ provider }) {
async function attemptResolve ({ tabId, name, path, search }) {
extension.tabs.update(tabId, { url: `loading.html` })
let url = `https://manager.ens.domains/name/${name}`
try {
const ipfsContentId = await resolveEnsToIpfsContentId({ provider, name })
const url = `https://gateway.ipfs.io/ipfs/${ipfsContentId}${path}${search || ''}`
const {type, hash} = await resolveEnsToIpfsContentId({ provider, name })
if (type === 'ipfs-ns') {
const resolvedUrl = `https://gateway.ipfs.io/ipfs/${hash}${path}${search || ''}`
try {
// check if ipfs gateway has result
const response = await fetch(url, { method: 'HEAD' })
// if failure, redirect to 404 page
if (response.status !== 200) {
extension.tabs.update(tabId, { url: '404.html' })
return
}
// otherwise redirect to the correct page
extension.tabs.update(tabId, { url })
const response = await fetch(resolvedUrl, { method: 'HEAD' })
if (response.status === 200) url = resolvedUrl
} catch (err) {
console.warn(err)
// if HEAD fetch failed, redirect so user can see relevant error page
extension.tabs.update(tabId, { url })
}
} else if (type === 'swarm-ns') {
url = `https://swarm-gateways.net/bzz:/${hash}${path}${search || ''}`
}
} catch (err) {
console.warn(err)
extension.tabs.update(tabId, { url: `error.html?name=${name}` })
} finally {
extension.tabs.update(tabId, { url })
}
}
}

View File

@ -26,7 +26,6 @@ const KeyringController = require('eth-keyring-controller')
const NetworkController = require('./controllers/network')
const PreferencesController = require('./controllers/preferences')
const AppStateController = require('./controllers/app-state')
const CurrencyController = require('./controllers/currency')
const InfuraController = require('./controllers/infura')
const CachedBalancesController = require('./controllers/cached-balances')
const RecentBlocksController = require('./controllers/recent-blocks')
@ -56,6 +55,7 @@ const ethUtil = require('ethereumjs-util')
const sigUtil = require('eth-sig-util')
const {
AddressBookController,
CurrencyRateController,
ShapeShiftController,
PhishingController,
} = require('gaba')
@ -109,11 +109,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
// currency controller
this.currencyController = new CurrencyController({
initState: initState.CurrencyController,
})
this.currencyController.updateConversionRate()
this.currencyController.scheduleConversionInterval()
this.currencyRateController = new CurrencyRateController(undefined, initState.CurrencyController)
// infura controller
this.infuraController = new InfuraController({
@ -130,7 +126,7 @@ module.exports = class MetamaskController extends EventEmitter {
// token exchange rate tracker
this.tokenRatesController = new TokenRatesController({
currency: this.currencyController.store,
currency: this.currencyRateController,
preferences: this.preferencesController.store,
})
@ -232,8 +228,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.networkController.on('networkDidChange', () => {
this.balancesController.updateAllBalances()
const currentCurrency = this.currencyController.getCurrentCurrency()
this.setCurrentCurrency(currentCurrency, function () {})
this.setCurrentCurrency(this.currencyRateController.state.currentCurrency, function () {})
})
this.balancesController.updateAllBalances()
@ -262,7 +257,7 @@ module.exports = class MetamaskController extends EventEmitter {
KeyringController: this.keyringController.store,
PreferencesController: this.preferencesController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyController.store,
CurrencyController: this.currencyRateController,
ShapeShiftController: this.shapeshiftController,
NetworkController: this.networkController.store,
InfuraController: this.infuraController.store,
@ -284,7 +279,7 @@ module.exports = class MetamaskController extends EventEmitter {
PreferencesController: this.preferencesController.store,
RecentBlocksController: this.recentBlocksController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyController.store,
CurrencyController: this.currencyRateController,
ShapeshiftController: this.shapeshiftController,
InfuraController: this.infuraController.store,
ProviderApprovalController: this.providerApprovalController.store,
@ -1596,16 +1591,13 @@ module.exports = class MetamaskController extends EventEmitter {
setCurrentCurrency (currencyCode, cb) {
const { ticker } = this.networkController.getNetworkConfig()
try {
this.currencyController.setNativeCurrency(ticker)
this.currencyController.setCurrentCurrency(currencyCode)
this.currencyController.updateConversionRate()
const data = {
nativeCurrency: ticker || 'ETH',
conversionRate: this.currencyController.getConversionRate(),
currentCurrency: this.currencyController.getCurrentCurrency(),
conversionDate: this.currencyController.getConversionDate(),
const currencyState = {
nativeCurrency: ticker,
currentCurrency: currencyCode,
}
cb(null, data)
this.currencyRateController.update(currencyState)
this.currencyRateController.configure(currencyState)
cb(null, this.currencyRateController.state)
} catch (err) {
cb(err)
}

View File

@ -0,0 +1,71 @@
## Creating Metrics Events
The `metricsEvent` method is made available to all components via context. This is done in `metamask-extension/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js`. As such, it can be called in all components by first adding it to the context proptypes:
```
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
```
and then accessing it on `this.context`.
Below is an example of a metrics event call:
```
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Main Menu',
name: 'Switched Account',
},
})
```
### Base Schema
Every `metricsEvent` call is passed an object that must have an `eventOpts` property. This property is an object that itself must have three properties:
- category: categorizes events according to the schema we have set up in our matomo.org instance
- action: usually describes the page on which the event takes place, or sometimes a significant subsections of a page
- name: a very specific descriptor of the event
### Implicit properties
All metrics events send the following data when called:
- network
- environmentType
- activeCurrency
- accountType
- numberOfTokens
- numberOfAccounts
These are added to the metrics event via the metametrics provider.
### Custom Variables
Metrics events can include custom variables. These are included within the `customVariables` property that is a first-level property within first param passed to `metricsEvent`.
For example:
```
this.context.metricsEvent({
eventOpts: {
category: 'Settings',
action: 'Custom RPC',
name: 'Error',
},
customVariables: {
networkId: newRpc,
chainId,
},
})
```
Custom variables can have custom property names and values can be strings or numbers.
**To include a custom variable, there are a set of necessary steps you must take.**
1. First you must declare a constant equal to the desired name of the custom variable property in `metamask-extension/ui/app/helpers/utils/metametrics.util.js` under `//Custom Variable Declarations`
1. Then you must add that name to the `customVariableNameIdMap` declaration
1. The id must be between 1 and 5
1. There can be no more than 5 custom variables assigned ids on a given url

2438
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -72,6 +72,7 @@
"c3": "^0.6.7",
"classnames": "^2.2.5",
"clone": "^2.1.2",
"content-hash": "^2.3.2",
"copy-to-clipboard": "^3.0.8",
"currency-formatter": "^1.4.2",
"d3": "^5.7.0",
@ -133,6 +134,7 @@
"mkdirp": "^0.5.1",
"multihashes": "^0.4.12",
"multiplex": "^6.7.0",
"nonce-tracker": "^1.0.0",
"number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
"obs-store": "^3.0.2",
@ -228,7 +230,7 @@
"file-loader": "^1.1.11",
"fs-extra": "^6.0.1",
"fs-promise": "^2.0.3",
"gaba": "^1.0.1",
"gaba": "^1.3.0",
"ganache-cli": "^6.1.0",
"ganache-core": "^2.5.3",
"geckodriver": "^1.14.1",

View File

@ -27,6 +27,8 @@ describe('Using MetaMask with an existing account', function () {
const testSeedPhrase = 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress'
const testAddress = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3'
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
const testPrivateKey3 = 'F4EC2590A0C10DE95FBF4547845178910E40F5035320C516A18C117DE02B5669'
const tinyDelayMs = 200
const regularDelayMs = 1000
const largeDelayMs = regularDelayMs * 2
@ -323,11 +325,60 @@ describe('Using MetaMask with an existing account', function () {
})
})
describe('Connects to a Hardware wallet', () => {
it('choose Connect Hardware Wallet from the account menu', async () => {
describe('Imports and removes an account', () => {
it('choose Create Account from the account menu', async () => {
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const [importAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Import Account')]`))
await importAccount.click()
await delay(regularDelayMs)
})
it('enter private key', async () => {
const privateKeyInput = await findElement(driver, By.css('#private-key-box'))
await privateKeyInput.sendKeys(testPrivateKey3)
await delay(regularDelayMs)
const importButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
await importButtons[0].click()
await delay(regularDelayMs)
})
it('should open the remove account modal', async () => {
const [accountName] = await findElements(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), 'Account 5')
await delay(regularDelayMs)
await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs)
const accountListItems = await findElements(driver, By.css('.account-menu__account'))
assert.equal(accountListItems.length, 5)
const removeAccountIcons = await findElements(driver, By.css('.remove-account-icon'))
await removeAccountIcons[1].click()
await delay(tinyDelayMs)
await findElement(driver, By.css('.confirm-remove-account__account'))
})
it('should remove the account', async () => {
const removeButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Remove')]`))
await removeButton.click()
await delay(regularDelayMs)
const [accountName] = await findElements(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), 'Account 1')
await delay(regularDelayMs)
const accountListItems = await findElements(driver, By.css('.account-menu__account'))
assert.equal(accountListItems.length, 4)
})
})
describe('Connects to a Hardware wallet', () => {
it('choose Connect Hardware Wallet from the account menu', async () => {
const [connectAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Connect Hardware Wallet')]`))
await connectAccount.click()
await delay(regularDelayMs)

View File

@ -71,11 +71,29 @@ async function runSendFlowTest (assert) {
assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address')
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(3)')
sendAmountField.find('.unit-input')[0].click()
const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input')
const amountMaxButton = await queryAsync($, '.send-v2__amount-max')
amountMaxButton.click()
reactTriggerChange(sendAmountField.find('input')[1])
assert.equal(sendAmountFieldInput.is(':disabled'), true, 'disabled the send amount input when max mode is on')
const gasPriceButtonGroup = await queryAsync($, '.gas-price-button-group--small')
const gasPriceButton = await gasPriceButtonGroup.find('button')[0]
const valueBeforeGasPriceChange = sendAmountFieldInput.prop('value')
gasPriceButton.click()
reactTriggerChange(sendAmountField.find('input')[1])
await timeout(1000)
assert.notEqual(valueBeforeGasPriceChange, sendAmountFieldInput.prop('value'), 'send amount value changes when gas price changes')
amountMaxButton.click()
reactTriggerChange(sendAmountField.find('input')[1])
sendAmountField.find('.unit-input').click()
sendAmountFieldInput.val('5.1')
reactTriggerChange(sendAmountField.find('input')[0])
reactTriggerChange(sendAmountField.find('input')[1])
let errorMessage = await queryAsync($, '.send-v2__error')
assert.equal(errorMessage[0].textContent, 'Insufficient funds.', 'send should render an insufficient fund error message')

View File

@ -1,78 +0,0 @@
const assert = require('assert')
const nock = require('nock')
const CurrencyController = require('../../../../app/scripts/controllers/currency')
describe('currency-controller', function () {
var currencyController
beforeEach(function () {
currencyController = new CurrencyController()
})
describe('currency conversions', function () {
describe('#setCurrentCurrency', function () {
it('should return USD as default', function () {
assert.equal(currencyController.getCurrentCurrency(), 'usd')
})
it('should be able to set to other currency', function () {
assert.equal(currencyController.getCurrentCurrency(), 'usd')
currencyController.setCurrentCurrency('JPY')
var result = currencyController.getCurrentCurrency()
assert.equal(result, 'JPY')
})
})
describe('#getConversionRate', function () {
it('should return undefined if non-existent', function () {
var result = currencyController.getConversionRate()
assert.ok(!result)
})
})
describe('#updateConversionRate', function () {
it('should retrieve an update for ETH to USD and set it in memory', function (done) {
this.timeout(15000)
nock('https://api.infura.io')
.get('/v1/ticker/ethusd')
.reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}')
assert.equal(currencyController.getConversionRate(), 0)
currencyController.setCurrentCurrency('usd')
currencyController.updateConversionRate()
.then(function () {
var result = currencyController.getConversionRate()
assert.equal(typeof result, 'number')
done()
}).catch(function (err) {
done(err)
})
})
it('should work for JPY as well.', function () {
this.timeout(15000)
assert.equal(currencyController.getConversionRate(), 0)
nock('https://api.infura.io')
.get('/v1/ticker/ethjpy')
.reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}')
var promise = new Promise(
function (resolve) {
currencyController.setCurrentCurrency('jpy')
currencyController.updateConversionRate().then(function () {
resolve()
})
})
promise.then(function () {
var result = currencyController.getConversionRate()
assert.equal(typeof result, 'number')
}).catch(function (done, err) {
done(err)
})
})
})
})
})

View File

@ -45,6 +45,11 @@ describe('MetaMaskController', function () {
.get(/.*/)
.reply(200)
nock('https://min-api.cryptocompare.com')
.persist()
.get(/.*/)
.reply(200, '{"JPY":12415.9}')
metamaskController = new MetaMaskController({
showUnapprovedTx: noop,
showUnconfirmedMessage: noop,
@ -441,7 +446,7 @@ describe('MetaMaskController', function () {
let defaultMetaMaskCurrency
beforeEach(function () {
defaultMetaMaskCurrency = metamaskController.currencyController.getCurrentCurrency()
defaultMetaMaskCurrency = metamaskController.currencyRateController.state.currentCurrency
})
it('defaults to usd', function () {
@ -450,7 +455,7 @@ describe('MetaMaskController', function () {
it('sets currency to JPY', function () {
metamaskController.setCurrentCurrency('JPY', noop)
assert.equal(metamaskController.currencyController.getCurrentCurrency(), 'JPY')
assert.equal(metamaskController.currencyRateController.state.currentCurrency, 'JPY')
})
})

View File

@ -1,238 +0,0 @@
const assert = require('assert')
const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../../../../lib/mock-tx-gen')
const providerResultStub = {}
describe('Nonce Tracker', function () {
let nonceTracker, pendingTxs, confirmedTxs
describe('#getNonceLock', function () {
describe('with 3 confirmed and 1 pending', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
})
it('should return 4', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
await nonceLock.releaseLock()
})
})
describe('sentry issue 476304902', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, {
fromNonce: 3,
count: 29,
})
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x3')
})
it('should return 9', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '32', `nonce should be 32 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('issue 3670', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, {
fromNonce: 6,
count: 3,
})
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x6')
})
it('should return 9', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '9', `nonce should be 9 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('with no previous txs', function () {
beforeEach(function () {
nonceTracker = generateNonceTrackerWith([], [])
})
it('should return 0', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('with multiple previous txs with same nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 })
pendingTxs = txGen.generate({
status: 'submitted',
txParams: { nonce: '0x01' },
}, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local confirmed count is higher than network nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local pending count is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [])
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when provider nonce is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are some pending nonces below the remote one and some over.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are pending nonces non sequentially over the network nonce.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
txGen.generate({ status: 'submitted' }, { count: 5 })
// 5 over that number
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('When all three return different values', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 })
pendingTxs = txGen.generate({
status: 'submitted',
nonce: 100,
}, { count: 1 })
// 0x32 is 50 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('Faq issue 67', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 })
pendingTxs = txGen.generate({
status: 'submitted',
}, { count: 10 })
// 0x40 is 64 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x40')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '74', `nonce should be 74 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
})
})
function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
const getPendingTransactions = () => pending
const getConfirmedTransactions = () => confirmed
providerResultStub.result = providerStub
const provider = {
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
}
const blockTracker = {
getCurrentBlock: () => '0x11b568',
getLatestBlock: async () => '0x11b568',
}
return new NonceTracker({
provider,
blockTracker,
getPendingTransactions,
getConfirmedTransactions,
})
}

View File

@ -58,7 +58,7 @@ describe('PendingTransactionTracker', function () {
}
})
it('should become failed if another tx with the same nonce succeeds', async function () {
it('should emit dropped if another tx with the same nonce succeeds', async function () {
// SETUP
const txGen = new MockTxGen()
@ -84,17 +84,16 @@ describe('PendingTransactionTracker', function () {
// THE EXPECTATION
const spy = sinon.spy()
pendingTxTracker.on('tx:failed', (txId, err) => {
pendingTxTracker.on('tx:dropped', (txId) => {
assert.equal(txId, pending.id, 'should fail the pending tx')
assert.equal(err.name, 'NonceTakenErr', 'should emit a nonce taken error.')
spy(txId, err)
spy(txId)
})
// THE METHOD
await pendingTxTracker._checkPendingTx(pending)
// THE ASSERTION
assert.ok(spy.calledWith(pending.id), 'tx failed should be emitted')
assert.ok(spy.calledWith(pending.id), 'tx dropped should be emitted')
})
})
@ -107,6 +106,38 @@ describe('PendingTransactionTracker', function () {
pendingTxTracker._checkPendingTx(txMetaNoHash)
})
it('should emit tx:dropped with the txMetas id only after the second call', function (done) {
txMeta = {
id: 1,
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb',
status: 'submitted',
txParams: {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
nonce: '0x1',
value: '0xfffff',
},
history: [{}],
rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d',
}
providerResultStub['eth_getTransactionCount'] = '0x02'
providerResultStub['eth_getTransactionByHash'] = {}
pendingTxTracker.once('tx:dropped', (id) => {
if (id === txMeta.id) {
delete providerResultStub['eth_getTransactionCount']
delete providerResultStub['eth_getTransactionByHash']
return done()
} else {
done(new Error('wrong tx Id'))
}
})
pendingTxTracker._checkPendingTx(txMeta).then(() => {
pendingTxTracker._checkPendingTx(txMeta).catch(done)
}).catch(done)
})
it('should should return if query does not return txParams', function () {
providerResultStub.eth_getTransactionByHash = null
pendingTxTracker._checkPendingTx(txMeta)
@ -283,6 +314,37 @@ describe('PendingTransactionTracker', function () {
})
})
describe('#_checkIftxWasDropped', () => {
const txMeta = {
id: 1,
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb',
status: 'submitted',
txParams: {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
nonce: '0x1',
value: '0xfffff',
},
rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d',
}
it('should return false when the nonce is the suggested network nonce', (done) => {
providerResultStub['eth_getTransactionCount'] = '0x01'
providerResultStub['eth_getTransactionByHash'] = {}
pendingTxTracker._checkIftxWasDropped(txMeta).then((dropped) => {
assert(!dropped, 'should be false')
done()
}).catch(done)
})
it('should return true when the network nonce is higher then the txMeta nonce', function (done) {
providerResultStub['eth_getTransactionCount'] = '0x02'
providerResultStub['eth_getTransactionByHash'] = {}
pendingTxTracker._checkIftxWasDropped(txMeta).then((dropped) => {
assert(dropped, 'should be true')
done()
}).catch(done)
})
})
describe('#_checkIfNonceIsTaken', function () {
beforeEach(function () {
const confirmedTxList = [{

View File

@ -41,12 +41,15 @@ EnsInput.prototype.onChange = function (recipient) {
ensResolution: null,
ensFailure: null,
toError: null,
recipient,
})
}
this.setState({
loadingEns: true,
recipient,
})
this.checkName(recipient)
}
@ -56,6 +59,7 @@ EnsInput.prototype.render = function () {
list: 'addresses',
onChange: this.onChange.bind(this),
qrScanner: true,
recipient: (this.state || {}).recipient,
})
return h('div', {
style: { width: '100%', position: 'relative' },
@ -79,19 +83,21 @@ EnsInput.prototype.componentDidMount = function () {
EnsInput.prototype.lookupEnsName = function (recipient) {
const { ensResolution } = this.state
recipient = recipient.trim()
log.info(`ENS attempting to resolve name: ${recipient}`)
this.ens.lookup(recipient.trim())
this.ens.lookup(recipient)
.then((address) => {
if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName'))
if (address !== ensResolution) {
this.setState({
loadingEns: false,
ensResolution: address,
nickname: recipient.trim(),
nickname: recipient,
hoverText: address + '\n' + this.context.t('clickCopy'),
ensFailure: false,
toError: null,
recipient,
})
}
})
@ -101,11 +107,11 @@ EnsInput.prototype.lookupEnsName = function (recipient) {
ensResolution: recipient,
ensFailure: true,
toError: null,
recipient: null,
}
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
setStateObj.hoverText = this.context.t('ensNameNotFound')
setStateObj.toError = 'ensNameNotFound'
setStateObj.ensFailure = false
} else {
log.error(reason)
setStateObj.hoverText = reason.message
@ -128,7 +134,7 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
}
if (prevState && ensResolution && this.props.onChange &&
ensResolution !== prevState.ensResolution) {
this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning })
this.props.onChange({ toAddress: ensResolution, recipient: state.recipient, nickname, toError: state.toError, toWarning: state.toWarning })
}
}

View File

@ -7,6 +7,8 @@ import {
setGasPrice,
createSpeedUpTransaction,
hideSidebar,
updateSendAmount,
setGasTotal,
} from '../../../../store/actions'
import {
setCustomGasPrice,
@ -18,6 +20,7 @@ import {
} from '../../../../ducks/gas/gas.duck'
import {
hideGasButtonGroup,
updateSendErrors,
} from '../../../../ducks/send/send.duck'
import {
updateGasAndCalculate,
@ -45,6 +48,9 @@ import {
getBasicGasEstimateBlockTime,
isCustomPriceSafe,
} from '../../../../selectors/custom-gas'
import {
getTokenBalance,
} from '../../../../pages/send/send.selectors'
import {
submittedPendingTransactionsSelector,
} from '../../../../selectors/transactions'
@ -53,6 +59,7 @@ import {
} from '../../../../helpers/utils/confirm-tx.util'
import {
addHexWEIsToDec,
subtractHexWEIsToDec,
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
decGWEIToHexWEI,
hexWEIToDecGWEI,
@ -66,6 +73,8 @@ import {
} from '../../../../pages/send/send.utils'
import { addHexPrefix } from 'ethereumjs-util'
import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils'
import { getMaxModeOn } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors'
import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils'
const mapStateToProps = (state, ownProps) => {
const { transaction = {} } = ownProps
@ -75,8 +84,6 @@ const mapStateToProps = (state, ownProps) => {
const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id)
const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice
const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit
const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex)
@ -90,6 +97,8 @@ const mapStateToProps = (state, ownProps) => {
const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex)
const maxModeOn = getMaxModeOn(state)
const gasPrices = getEstimatedGasPrices(state)
const estimatedTimes = getEstimatedGasTimes(state)
const balance = getCurrentEthBalance(state)
@ -98,9 +107,13 @@ const mapStateToProps = (state, ownProps) => {
const isMainnet = getIsMainnet(state)
const showFiat = Boolean(isMainnet || showFiatInTestnets)
const insufficientBalance = !isBalanceSufficient({
const newTotalEth = maxModeOn ? addHexWEIsToRenderableEth(balance, '0x0') : addHexWEIsToRenderableEth(value, customGasTotal)
const sendAmount = maxModeOn ? subtractHexWEIsFromRenderableEth(balance, customGasTotal) : addHexWEIsToRenderableEth(value, '0x0')
const insufficientBalance = maxModeOn ? false : !isBalanceSufficient({
amount: value,
gasTotal,
gasTotal: customGasTotal,
balance,
conversionRate,
})
@ -112,10 +125,12 @@ const mapStateToProps = (state, ownProps) => {
customModalGasLimitInHex,
customGasPrice,
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
customGasTotal,
newTotalFiat,
currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes),
blockTime: getBasicGasEstimateBlockTime(state),
customPriceIsSafe: isCustomPriceSafe(state),
maxModeOn,
gasPriceButtonGroupProps: {
buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
@ -129,12 +144,12 @@ const mapStateToProps = (state, ownProps) => {
estimatedTimesMax: estimatedTimes[0],
},
infoRowProps: {
originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate),
originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal),
originalTotalFiat: addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate),
originalTotalEth: addHexWEIsToRenderableEth(value, customGasTotal),
newTotalFiat: showFiat ? newTotalFiat : '',
newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal),
newTotalEth,
transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal),
sendAmount: addHexWEIsToRenderableEth(value, '0x0'),
sendAmount,
},
isSpeedUp: transaction.status === 'submitted',
txId: transaction.id,
@ -142,6 +157,9 @@ const mapStateToProps = (state, ownProps) => {
gasEstimatesLoading,
isMainnet,
isEthereumNetwork: isEthereumNetwork(state),
selectedToken: getSelectedToken(state),
balance,
tokenBalance: getTokenBalance(state),
}
}
@ -174,11 +192,29 @@ const mapDispatchToProps = dispatch => {
hideSidebar: () => dispatch(hideSidebar()),
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
setGasTotal: (total) => dispatch(setGasTotal(total)),
setAmountToMax: (maxAmountDataObject) => {
dispatch(updateSendErrors({ amount: null }))
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
},
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps
const {
gasPriceButtonGroupProps,
isConfirm,
txId,
isSpeedUp,
insufficientBalance,
maxModeOn,
customGasPrice,
customGasTotal,
balance,
selectedToken,
tokenBalance,
customGasLimit,
} = stateProps
const {
updateCustomGasPrice: dispatchUpdateCustomGasPrice,
hideGasButtonGroup: dispatchHideGasButtonGroup,
@ -188,6 +224,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
hideSidebar: dispatchHideSidebar,
cancelAndClose: dispatchCancelAndClose,
hideModal: dispatchHideModal,
setAmountToMax: dispatchSetAmountToMax,
...otherDispatchProps
} = dispatchProps
@ -208,6 +245,14 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchHideGasButtonGroup()
dispatchCancelAndClose()
}
if (maxModeOn) {
dispatchSetAmountToMax({
balance,
gasTotal: customGasTotal,
selectedToken,
tokenBalance,
})
}
},
gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps,
@ -219,7 +264,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchHideSidebar()
}
},
disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0),
disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0) || customGasLimit < 21000,
}
}
@ -258,6 +303,13 @@ function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) {
)(aHexWEI, bHexWEI)
}
function subtractHexWEIsFromRenderableEth (aHexWEI, bHexWei) {
return pipe(
subtractHexWEIsToDec,
formatETHFee
)(aHexWEI, bHexWei)
}
function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) {
return pipe(
addHexWEIsToDec,

View File

@ -46,6 +46,10 @@ proxyquire('../gas-modal-page-container.container.js', {
'../../../../ducks/send/send.duck': sendActionSpies,
'../../../../selectors/selectors.js': {
getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
getSelectedToken: () => null,
},
'../../../../pages/send/send.selectors': {
getTokenBalance: (state) => state.metamask.send.tokenBalance || '0x0',
},
})
@ -68,6 +72,7 @@ describe('gas-modal-page-container container', () => {
gasLimit: '16',
gasPrice: '32',
amount: '64',
maxModeOn: false,
},
currentCurrency: 'abc',
conversionRate: 50,
@ -106,6 +111,7 @@ describe('gas-modal-page-container container', () => {
},
}
const baseExpectedResult = {
balance: '0x0',
isConfirm: true,
customGasPrice: 4.294967295,
customGasLimit: 2863311530,
@ -114,6 +120,7 @@ describe('gas-modal-page-container container', () => {
blockTime: 12,
customModalGasLimitInHex: 'aaaaaaaa',
customModalGasPriceInHex: 'ffffffff',
customGasTotal: 'aaaaaaa955555556',
customPriceIsSafe: true,
gasChartProps: {
'currentPrice': 4.294967295,
@ -142,6 +149,9 @@ describe('gas-modal-page-container container', () => {
txId: 34,
isEthereumNetwork: true,
isMainnet: true,
maxModeOn: false,
selectedToken: null,
tokenBalance: '0x0',
}
const baseMockOwnProps = { transaction: { id: 34 } }
const tests = [
@ -150,7 +160,7 @@ describe('gas-modal-page-container container', () => {
mockState: Object.assign({}, baseMockState, {
metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' },
}),
expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }),
expectedResult: Object.assign({}, baseExpectedResult, { balance: '0xfffffffffffffffffffff', insufficientBalance: false }),
mockOwnProps: baseMockOwnProps,
},
{

View File

@ -18,6 +18,7 @@ export default class CurrencyInput extends PureComponent {
static propTypes = {
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
maxModeOn: PropTypes.bool,
nativeCurrency: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func,
@ -136,7 +137,7 @@ export default class CurrencyInput extends PureComponent {
}
render () {
const { fiatSuffix, nativeSuffix, ...restProps } = this.props
const { fiatSuffix, nativeSuffix, maxModeOn, ...restProps } = this.props
const { decimalValue } = this.state
return (
@ -146,6 +147,7 @@ export default class CurrencyInput extends PureComponent {
onChange={this.handleChange}
onBlur={this.handleBlur}
value={decimalValue}
maxModeOn={maxModeOn}
actionComponent={(
<div
className="currency-input__swap-component"

View File

@ -1,18 +1,21 @@
import { connect } from 'react-redux'
import CurrencyInput from './currency-input.component'
import { ETH } from '../../../helpers/constants/common'
import { getMaxModeOn } from '../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors'
import {getIsMainnet, preferencesSelector} from '../../../selectors/selectors'
const mapStateToProps = state => {
const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
const { showFiatInTestnets } = preferencesSelector(state)
const isMainnet = getIsMainnet(state)
const maxModeOn = getMaxModeOn(state)
return {
nativeCurrency,
currentCurrency,
conversionRate,
hideFiat: (!isMainnet && !showFiatInTestnets),
maxModeOn,
}
}

View File

@ -30,6 +30,9 @@ describe('CurrencyInput container', () => {
provider: {
type: 'mainnet',
},
send: {
maxModeOn: false,
},
},
},
expected: {
@ -37,6 +40,7 @@ describe('CurrencyInput container', () => {
currentCurrency: 'usd',
nativeCurrency: 'ETH',
hideFiat: false,
maxModeOn: false,
},
},
// Test # 2
@ -53,6 +57,9 @@ describe('CurrencyInput container', () => {
provider: {
type: 'rinkeby',
},
send: {
maxModeOn: false,
},
},
},
expected: {
@ -60,6 +67,7 @@ describe('CurrencyInput container', () => {
currentCurrency: 'usd',
nativeCurrency: 'ETH',
hideFiat: true,
maxModeOn: false,
},
},
// Test # 3
@ -76,6 +84,9 @@ describe('CurrencyInput container', () => {
provider: {
type: 'rinkeby',
},
send: {
maxModeOn: false,
},
},
},
expected: {
@ -83,6 +94,7 @@ describe('CurrencyInput container', () => {
currentCurrency: 'usd',
nativeCurrency: 'ETH',
hideFiat: false,
maxModeOn: false,
},
},
// Test # 4
@ -99,6 +111,9 @@ describe('CurrencyInput container', () => {
provider: {
type: 'mainnet',
},
send: {
maxModeOn: false,
},
},
},
expected: {
@ -106,6 +121,7 @@ describe('CurrencyInput container', () => {
currentCurrency: 'usd',
nativeCurrency: 'ETH',
hideFiat: false,
maxModeOn: false,
},
},
]

View File

@ -42,6 +42,10 @@
max-width: 22ch;
height: 16px;
line-height: 18px;
&__disabled {
background-color: rgb(222, 222, 222);
}
}
&__input-container {
@ -59,4 +63,9 @@
&--error {
border-color: $red;
}
&__disabled {
background-color: #F2F3F4;
}
}

View File

@ -13,6 +13,7 @@ export default class UnitInput extends PureComponent {
children: PropTypes.node,
actionComponent: PropTypes.node,
error: PropTypes.bool,
maxModeOn: PropTypes.bool,
onBlur: PropTypes.func,
onChange: PropTypes.func,
placeholder: PropTypes.string,
@ -71,25 +72,26 @@ export default class UnitInput extends PureComponent {
}
render () {
const { error, placeholder, suffix, actionComponent, children } = this.props
const { error, placeholder, suffix, actionComponent, children, maxModeOn } = this.props
const { value } = this.state
return (
<div
className={classnames('unit-input', { 'unit-input--error': error })}
onClick={this.handleFocus}
className={classnames('unit-input', { 'unit-input--error': error }, { 'unit-input__disabled': maxModeOn })}
onClick={maxModeOn ? null : this.handleFocus}
>
<div className="unit-input__inputs">
<div className="unit-input__input-container">
<input
type="number"
className="unit-input__input"
className={classnames('unit-input__input', { 'unit-input__disabled': maxModeOn })}
value={value}
placeholder={placeholder}
onChange={this.handleChange}
onBlur={this.handleBlur}
style={{ width: this.getInputWidth(value) }}
ref={ref => { this.unitInput = ref }}
disabled={maxModeOn}
/>
{
suffix && (

View File

@ -520,6 +520,10 @@
color: $red;
}
&__error-amount {
margin-top: 5px;
}
&__warning {
font-size: 12px;
line-height: 12px;
@ -557,6 +561,12 @@
justify-content: space-between;
}
&__form-field-container {
display: flex;
flex-direction: column;
width: 277px;
}
&__form-field {
flex: 1 1 auto;
min-width: 0;
@ -763,7 +773,43 @@
}
}
&__to-autocomplete, &__memo-text-area, &__hex-data {
&__to-autocomplete {
display: flex;
flex-direction: column;
z-index: 1025;
position: relative;
height: 54px;
width: 100%;
border: 1px solid $alto;
border-radius: 4px;
background-color: $white;
color: $tundora;
padding: 0 10px;
font-family: Roboto;
line-height: 21px;
&__input {
font-size: 16px;
height: 100%;
border: none;
}
&__resolved {
font-size: 12px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
height: 30px;
cursor: pointer;
+ .send-v2__to-autocomplete__qr-code {
top: 2px;
right: 0;
}
}
}
&__memo-text-area, &__hex-data {
&__input {
z-index: 1025;
position: relative;
@ -781,12 +827,47 @@
}
&__amount-max {
color: $curious-blue;
font-family: Roboto;
font-size: 12px;
left: 8px;
border: none;
position: relative;
display: inline-block;
width: 56px;
height: 20px;
margin-top: 5px;
&__button {
width: 56px;
height: 20px;
position: absolute;
border: 2px solid #B0D7F2;
border-radius: 6px;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
color: #2f9ae0;
&__disabled {
color: #B0D7F2;
cursor: auto;
}
}
input:checked + &__button {
background-color: #037DD6;
border: 2px solid #037DD6;
color: #fff;
}
}
&__amount-max input {
opacity: 0;
width: 0;
height: 0;
}
&__gas-fee-display {
@ -1041,7 +1122,7 @@
font-size: 14px;
color: #2f9ae0;
cursor: pointer;
margin-top: 16px;
margin-top: 5px;
}
.sliders-icon-container {

View File

@ -1,6 +1,6 @@
import ethUtil from 'ethereumjs-util'
import { ETH, GWEI, WEI } from '../constants/common'
import { conversionUtil, addCurrencies } from './conversion-util'
import { conversionUtil, addCurrencies, subtractCurrencies } from './conversion-util'
export function bnToHex (inputBn) {
return ethUtil.addHexPrefix(inputBn.toString(16))
@ -92,6 +92,15 @@ export function addHexWEIsToDec (aHexWEI, bHexWEI) {
})
}
export function subtractHexWEIsToDec (aHexWEI, bHexWEI) {
return subtractCurrencies(aHexWEI, bHexWEI, {
aBase: 16,
bBase: 16,
fromDenomination: 'WEI',
numberOfDecimals: 6,
})
}
export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) {
return conversionUtil(ethTotal, {
fromNumericBase: 'dec',

View File

@ -12,6 +12,8 @@ const METAMETRICS_TRACKING_URL = inDevelopment
? 'http://www.metamask.io/metametrics'
: 'http://www.metamask.io/metametrics-prod'
/** ***************Custom variables*************** **/
// Custon variable declarations
const METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE = 'gasLimitChange'
const METAMETRICS_CUSTOM_GAS_PRICE_CHANGE = 'gasPriceChange'
const METAMETRICS_CUSTOM_FUNCTION_TYPE = 'functionType'
@ -24,6 +26,28 @@ const METAMETRICS_CUSTOM_ERROR_MESSAGE = 'errorMessage'
const METAMETRICS_CUSTOM_RPC_NETWORK_ID = 'networkId'
const METAMETRICS_CUSTOM_RPC_CHAIN_ID = 'chainId'
const METAMETRICS_CUSTOM_GAS_CHANGED = 'gasChanged'
const METAMETRICS_CUSTOM_ASSET_SELECTED = 'assetSelected'
const customVariableNameIdMap = {
[METAMETRICS_CUSTOM_FUNCTION_TYPE]: 1,
[METAMETRICS_CUSTOM_RECIPIENT_KNOWN]: 2,
[METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN]: 3,
[METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE]: 4,
[METAMETRICS_CUSTOM_GAS_PRICE_CHANGE]: 5,
[METAMETRICS_CUSTOM_FROM_NETWORK]: 1,
[METAMETRICS_CUSTOM_TO_NETWORK]: 2,
[METAMETRICS_CUSTOM_RPC_NETWORK_ID]: 1,
[METAMETRICS_CUSTOM_RPC_CHAIN_ID]: 2,
[METAMETRICS_CUSTOM_ERROR_FIELD]: 3,
[METAMETRICS_CUSTOM_ERROR_MESSAGE]: 4,
[METAMETRICS_CUSTOM_GAS_CHANGED]: 1,
[METAMETRICS_CUSTOM_ASSET_SELECTED]: 2,
}
/** ********************************************************** **/
const METAMETRICS_CUSTOM_NETWORK = 'network'
const METAMETRICS_CUSTOM_ENVIRONMENT_TYPE = 'environmentType'
@ -32,20 +56,6 @@ const METAMETRICS_CUSTOM_ACCOUNT_TYPE = 'accountType'
const METAMETRICS_CUSTOM_NUMBER_OF_TOKENS = 'numberOfTokens'
const METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS = 'numberOfAccounts'
const customVariableNameIdMap = {
[METAMETRICS_CUSTOM_FUNCTION_TYPE]: 1,
[METAMETRICS_CUSTOM_RECIPIENT_KNOWN]: 2,
[METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN]: 3,
[METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE]: 4,
[METAMETRICS_CUSTOM_GAS_PRICE_CHANGE]: 5,
[METAMETRICS_CUSTOM_FROM_NETWORK]: 1,
[METAMETRICS_CUSTOM_TO_NETWORK]: 2,
[METAMETRICS_CUSTOM_RPC_NETWORK_ID]: 1,
[METAMETRICS_CUSTOM_RPC_CHAIN_ID]: 2,
[METAMETRICS_CUSTOM_ERROR_FIELD]: 1,
[METAMETRICS_CUSTOM_ERROR_MESSAGE]: 2,
[METAMETRICS_CUSTOM_GAS_CHANGED]: 1,
}
const customDimensionsNameIdMap = {
[METAMETRICS_CUSTOM_NETWORK]: 5,
@ -61,6 +71,7 @@ function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
}
// composes query params of the form &dimension[0-999]=[value]
function composeCustomDimensionParamAddition (customDimensions) {
const customDimensionParamStrings = Object.keys(customDimensions).reduce((acc, name) => {
return [...acc, `dimension${customDimensionsNameIdMap[name]}=${customDimensions[name]}`]
@ -68,6 +79,8 @@ function composeCustomDimensionParamAddition (customDimensions) {
return `&${customDimensionParamStrings.join('&')}`
}
// composes query params in form: &cvar={[id]:[[name],[value]]}
// Example: &cvar={"1":["OS","iphone 5.0"],"2":["Matomo Mobile Version","1.6.2"],"3":["Locale","en::en"],"4":["Num Accounts","2"]}
function composeCustomVarParamAddition (customVariables) {
const customVariableIdValuePairs = Object.keys(customVariables).reduce((acc, name) => {
return {
@ -84,6 +97,28 @@ function composeParamAddition (paramValue, paramName) {
: `&${paramName}=${paramValue}`
}
/**
* @name composeUrl
* @param {Object} config - configuration object for composing the metametrics url
* @property {object} config.eventOpts Object containing event category, action and name descriptors
* @property {object} config.customVariables Object containing custom properties with values relevant to a specific event
* @property {object} config.pageOpts Objects containing information about a page/route the event is dispatched from
* @property {number} config.network The selected network of the user when the event occurs
* @property {string} config.environmentType The "environment" the user is using the app from: 'popup', 'notification' or 'fullscreen'
* @property {string} config.activeCurrency The current the user has select as their primary currency at the time of the event
* @property {string} config.accountType The account type being used at the time of the event: 'hardware', 'imported' or 'default'
* @property {number} config.numberOfTokens The number of tokens that the user has added at the time of the event
* @property {number} config.numberOfAccounts The number of accounts the user has added at the time of the event
* @property {string} config.previousPath The location path the user was on prior to the path they are on at the time of the event
* @property {string} config.currentPath The location path the user is on at the time of the event
* @property {string} config.metaMetricsId A random id assigned to a user at the time of opting in to metametrics. A hexadecimal number
* @property {string} config.confirmTransactionOrigin The origin on a transaction
* @property {string} config.url The url to track an event at. Overrides `currentPath`
* @property {boolean} config.excludeMetaMetricsId Whether or not the tracked event data should be associated with a metametrics id
* @property {boolean} config.isNewVisit Whether or not the event should be tracked as a new visit/user sessions
* @returns {String} Returns a url to be passed to fetch to make the appropriate request to matomo.
* Example: https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&apiv=1&e_c=Navigation&e_a=Home&e_n=Clicked%20Send:%20Eth&urlref=http%3A%2F%2Fwww.metamask.io%2Fmetametrics%2Fhome.html%23send&dimension5=3&dimension6=fullscreen&dimension7=ETH&dimension8=default&dimension9=0&dimension10=3&url=http%3A%2F%2Fwww.metamask.io%2Fmetametrics%2Fhome.html%23&_id=49c10aff19795e9a&rand=7906028754863992&pv_id=53acad&uid=49c1
*/
function composeUrl (config) {
const {
eventOpts = {},

View File

@ -9,6 +9,7 @@ import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constant
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
GAS_LIMIT_TOO_LOW_ERROR_KEY,
} from '../../helpers/constants/error-keys'
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
@ -134,6 +135,7 @@ export default class ConfirmTransactionBase extends Component {
value: amount,
} = {},
} = {},
customGas,
} = this.props
const insufficientBalance = balance && !isBalanceSufficient({
@ -150,6 +152,13 @@ export default class ConfirmTransactionBase extends Component {
}
}
if (customGas.gasLimit < 21000) {
return {
valid: false,
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
}
}
if (simulationFails) {
return {
valid: true,

View File

@ -8,8 +8,6 @@ const ConnectScreen = require('./connect-screen')
const AccountList = require('./account-list')
const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
const { formatBalance } = require('../../../helpers/utils/util')
const { getPlatform } = require('../../../../../app/scripts/lib/util')
const { PLATFORM_FIREFOX } = require('../../../../../app/scripts/lib/enums')
class ConnectHardwareForm extends Component {
constructor (props) {
@ -51,12 +49,6 @@ class ConnectHardwareForm extends Component {
}
connectToHardwareWallet = (device) => {
// Ledger hardware wallets are not supported on firefox
if (getPlatform() === PLATFORM_FIREFOX && device === 'ledger') {
this.setState({ browserSupported: false, error: null})
return null
}
if (this.state.accounts.length) {
return null
}

View File

@ -1,16 +1,21 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class AmountMaxButton extends Component {
static propTypes = {
balance: PropTypes.string,
buttonDataLoading: PropTypes.bool,
clearMaxAmount: PropTypes.func,
inError: PropTypes.bool,
gasTotal: PropTypes.string,
maxModeOn: PropTypes.bool,
selectedToken: PropTypes.object,
setAmountToMax: PropTypes.func,
setMaxModeTo: PropTypes.func,
tokenBalance: PropTypes.string,
}
static contextTypes = {
@ -35,8 +40,8 @@ export default class AmountMaxButton extends Component {
})
}
onMaxClick = (event) => {
const { setMaxModeTo } = this.props
onMaxClick = () => {
const { setMaxModeTo, clearMaxAmount, maxModeOn } = this.props
const { metricsEvent } = this.context
metricsEvent({
@ -46,25 +51,25 @@ export default class AmountMaxButton extends Component {
name: 'Clicked "Amount Max"',
},
})
event.preventDefault()
if (!maxModeOn) {
setMaxModeTo(true)
this.setMaxAmount()
} else {
setMaxModeTo(false)
clearMaxAmount()
}
}
render () {
return this.props.maxModeOn
? null
: (
<div>
<span
className="send-v2__amount-max"
onClick={this.onMaxClick}
>
const { maxModeOn, buttonDataLoading, inError } = this.props
return (
<div className={'send-v2__amount-max'} onClick={buttonDataLoading || inError ? null : this.onMaxClick}>
<input type="checkbox" checked={maxModeOn} />
<div className={classnames('send-v2__amount-max__button', { 'send-v2__amount-max__button__disabled': buttonDataLoading || inError })}>
{this.context.t('max')}
</span>
</div>
</div>
)
}
}

View File

@ -5,6 +5,7 @@ import {
getSendFromBalance,
getTokenBalance,
} from '../../../send.selectors.js'
import { getBasicGasEstimateLoadingStatus } from '../../../../../selectors/custom-gas'
import { getMaxModeOn } from './amount-max-button.selectors.js'
import { calcMaxAmount } from './amount-max-button.utils.js'
import {
@ -22,6 +23,7 @@ function mapStateToProps (state) {
return {
balance: getSendFromBalance(state),
buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
gasTotal: getGasTotal(state),
maxModeOn: getMaxModeOn(state),
selectedToken: getSelectedToken(state),
@ -35,6 +37,9 @@ function mapDispatchToProps (dispatch) {
dispatch(updateSendErrors({ amount: null }))
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
},
clearMaxAmount: () => {
dispatch(updateSendAmount('0'))
},
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
}
}

View File

@ -65,7 +65,7 @@ describe('AmountMaxButton Component', function () {
assert(wrapper.exists('.send-v2__amount-max'))
})
it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => {
it('should call setMaxModeTo and setMaxAmount when the checkbox is checked', () => {
const {
onClick,
} = wrapper.find('.send-v2__amount-max').props()
@ -81,11 +81,6 @@ describe('AmountMaxButton Component', function () {
)
})
it('should not render anything when maxModeOn is true', () => {
wrapper.setProps({ maxModeOn: true })
assert.ok(!wrapper.exists('.send-v2__amount-max'))
})
it('should render the expected text when maxModeOn is false', () => {
wrapper.setProps({ maxModeOn: false })
assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t')

View File

@ -29,6 +29,7 @@ proxyquire('../amount-max-button.container.js', {
},
'./amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` },
'./amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 },
'../../../../../selectors/custom-gas': { getBasicGasEstimateLoadingStatus: (s) => `mockButtonDataLoading:${s}`},
'../../../../../store/actions': actionSpies,
'../../../../../ducks/send/send.duck': duckActionSpies,
})
@ -40,6 +41,7 @@ describe('amount-max-button container', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
balance: 'mockBalance:mockState',
buttonDataLoading: 'mockButtonDataLoading:mockState',
gasTotal: 'mockGasTotal:mockState',
maxModeOn: 'mockMaxModeOn:mockState',
selectedToken: 'mockSelectedToken:mockState',

View File

@ -110,7 +110,7 @@ export default class SendAmountRow extends Component {
showError={inError}
errorType={'amount'}
>
{!inError && gasTotal && <AmountMaxButton />}
{gasTotal && <AmountMaxButton inError={inError} />}
{ this.renderInput() }
</SendRowWrapper>
)

View File

@ -8,14 +8,19 @@ import AdvancedGasInputs from '../../../../components/app/gas-customization/adva
export default class SendGasRow extends Component {
static propTypes = {
balance: PropTypes.string,
conversionRate: PropTypes.number,
convertedCurrency: PropTypes.string,
gasFeeError: PropTypes.bool,
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
maxModeOn: PropTypes.bool,
showCustomizeGasModal: PropTypes.func,
selectedToken: PropTypes.object,
setAmountToMax: PropTypes.func,
setGasPrice: PropTypes.func,
setGasLimit: PropTypes.func,
tokenBalance: PropTypes.string,
gasPriceButtonGroupProps: PropTypes.object,
gasButtonGroupShown: PropTypes.bool,
advancedInlineGasShown: PropTypes.bool,
@ -47,6 +52,23 @@ export default class SendGasRow extends Component {
</div>
}
setMaxAmount () {
const {
balance,
gasTotal,
selectedToken,
setAmountToMax,
tokenBalance,
} = this.props
setAmountToMax({
balance,
gasTotal,
selectedToken,
tokenBalance,
})
}
renderContent () {
const {
conversionRate,
@ -57,6 +79,7 @@ export default class SendGasRow extends Component {
gasPriceButtonGroupProps,
gasButtonGroupShown,
advancedInlineGasShown,
maxModeOn,
resetGasButtons,
setGasPrice,
setGasLimit,
@ -71,7 +94,7 @@ export default class SendGasRow extends Component {
className="gas-price-button-group--small"
showCheck={false}
{...gasPriceButtonGroupProps}
handleGasPriceSelection={(...args) => {
handleGasPriceSelection={async (...args) => {
metricsEvent({
eventOpts: {
category: 'Transactions',
@ -79,7 +102,10 @@ export default class SendGasRow extends Component {
name: 'Changed Gas Button',
},
})
gasPriceButtonGroupProps.handleGasPriceSelection(...args)
await gasPriceButtonGroupProps.handleGasPriceSelection(...args)
if (maxModeOn) {
this.setMaxAmount()
}
}}
/>
{ this.renderAdvancedOptionsButton() }
@ -89,7 +115,12 @@ export default class SendGasRow extends Component {
convertedCurrency={convertedCurrency}
gasLoadingError={gasLoadingError}
gasTotal={gasTotal}
onReset={resetGasButtons}
onReset={() => {
resetGasButtons()
if (maxModeOn) {
this.setMaxAmount()
}
}}
onClick={() => showCustomizeGasModal()}
/>
const advancedGasInputs = <div>

View File

@ -6,11 +6,17 @@ import {
getGasPrice,
getGasLimit,
getSendAmount,
getSendFromBalance,
getTokenBalance,
} from '../../send.selectors.js'
import {
getMaxModeOn,
} from '../send-amount-row/amount-max-button/amount-max-button.selectors'
import {
isBalanceSufficient,
calcGasTotal,
} from '../../send.utils.js'
import { calcMaxAmount } from '../send-amount-row/amount-max-button/amount-max-button.utils'
import {
getBasicGasEstimateLoadingStatus,
getRenderableEstimateDataForSmallButtonsFromGWEI,
@ -18,6 +24,7 @@ import {
} from '../../../../selectors/custom-gas'
import {
showGasButtonGroup,
updateSendErrors,
} from '../../../../ducks/send/send.duck'
import {
resetCustomData,
@ -25,10 +32,11 @@ import {
setCustomGasLimit,
} from '../../../../ducks/gas/gas.duck'
import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../store/actions'
import { showModal, setGasPrice, setGasLimit, setGasTotal, updateSendAmount } from '../../../../store/actions'
import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../selectors/selectors'
import SendGasRow from './send-gas-row.component'
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
function mapStateToProps (state) {
@ -49,6 +57,7 @@ function mapStateToProps (state) {
})
return {
balance: getSendFromBalance(state),
conversionRate,
convertedCurrency: getCurrentCurrency(state),
gasTotal,
@ -65,6 +74,9 @@ function mapStateToProps (state) {
gasPrice,
gasLimit,
insufficientBalance,
maxModeOn: getMaxModeOn(state),
selectedToken: getSelectedToken(state),
tokenBalance: getTokenBalance(state),
}
}
@ -85,6 +97,10 @@ function mapDispatchToProps (dispatch) {
dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice)))
}
},
setAmountToMax: maxAmountDataObject => {
dispatch(updateSendErrors({ amount: null }))
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
},
showGasButtonGroup: () => dispatch(showGasButtonGroup()),
resetCustomData: () => dispatch(resetCustomData()),
}

View File

@ -44,6 +44,11 @@ proxyquire('../send-gas-row.container.js', {
getGasPrice: (s) => `mockGasPrice:${s}`,
getGasLimit: (s) => `mockGasLimit:${s}`,
getSendAmount: (s) => `mockSendAmount:${s}`,
getSendFromBalance: (s) => `mockBalance:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`,
},
'../send-amount-row/amount-max-button/amount-max-button.selectors': {
getMaxModeOn: (s) => `mockMaxModeOn:${s}`,
},
'../../send.utils.js': {
isBalanceSufficient: ({
@ -75,6 +80,7 @@ describe('send-gas-row container', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
balance: 'mockBalance:mockState',
conversionRate: 'mockConversionRate:mockState',
convertedCurrency: 'mockConvertedCurrency:mockState',
gasTotal: 'mockGasTotal:mockState',
@ -91,6 +97,9 @@ describe('send-gas-row container', () => {
gasLimit: 'mockGasLimit:mockState',
gasPrice: 'mockGasPrice:mockState',
insufficientBalance: false,
maxModeOn: 'mockMaxModeOn:mockState',
selectedToken: false,
tokenBalance: 'mockTokenBalance:mockState',
})
})

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class SendRowErrorMessage extends Component {
@ -19,7 +20,7 @@ export default class SendRowErrorMessage extends Component {
return (
errorMessage
? <div className="send-v2__error">{this.context.t(errorMessage)}</div>
? <div className={classnames('send-v2__error', {'send-v2__error-amount': errorType === 'amount'})}>{this.context.t(errorMessage)}</div>
: null
)
}

View File

@ -18,7 +18,7 @@ export default class SendRowWrapper extends Component {
t: PropTypes.func,
};
render () {
renderAmountFormRow () {
const {
children,
errorType = '',
@ -30,6 +30,38 @@ export default class SendRowWrapper extends Component {
const formField = Array.isArray(children) ? children[1] || children[0] : children
const customLabelContent = children.length > 1 ? children[0] : null
return (
<div className="send-v2__form-row">
<div className="send-v2__form-label">
{label}
{customLabelContent}
</div>
<div className="send-v2__form-field-container">
<div className="send-v2__form-field">
{formField}
</div>
<div>
{showError && <SendRowErrorMessage errorType={errorType} />}
{!showError && showWarning && <SendRowWarningMessage warningType={warningType} />}
</div>
</div>
</div>
)
}
renderFormRow () {
const {
children,
errorType = '',
label,
showError = false,
showWarning = false,
warningType = '',
} = this.props
const formField = Array.isArray(children) ? children[1] || children[0] : children
const customLabelContent = (Array.isArray(children) && children.length) > 1 ? children[0] : null
return (
<div className="send-v2__form-row">
<div className="send-v2__form-label">
@ -45,4 +77,14 @@ export default class SendRowWrapper extends Component {
)
}
render () {
const {
errorType = '',
} = this.props
return (
errorType === 'amount' ? this.renderAmountFormRow() : this.renderFormRow()
)
}
}

View File

@ -1,6 +1,7 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const copyToClipboard = require('copy-to-clipboard')
const inherits = require('util').inherits
const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect
@ -93,24 +94,34 @@ ToAutoComplete.prototype.componentDidUpdate = function (nextProps) {
ToAutoComplete.prototype.render = function () {
const {
to,
recipient,
dropdownOpen,
onChange,
inError,
qrScanner,
} = this.props
return h('div.send-v2__to-autocomplete', {}, [
const isRecipientToDiff = recipient && recipient !== to
return h('div.send-v2__to-autocomplete', {style: {
borderColor: inError ? 'red' : null,
}}, [
h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
placeholder: this.context.t('recipientAddress'),
className: inError ? `send-v2__error-border` : '',
value: to,
value: recipient,
onChange: event => onChange(event.target.value),
onFocus: event => this.handleInputEvent(event),
style: {
borderColor: inError ? 'red' : null,
},
}),
isRecipientToDiff && h(Tooltip, {title: this.context.t('copyToClipboard')},
h('div.send-v2__to-autocomplete__resolved', {
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
copyToClipboard(to)
},
}, to)),
qrScanner && h(Tooltip, {
title: this.context.t('scanQrCode'),
position: 'bottom',