mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
commit
be91171194
@ -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
|
||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -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
|
||||
|
52
app/404.html
52
app/404.html
@ -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>
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
1
app/images/enslogo.svg
Normal 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 |
@ -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>
|
||||
|
@ -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__",
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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'}]
|
||||
|
@ -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)
|
||||
// lookup content id
|
||||
const contentLookupResult = await Resolver.content(hash)
|
||||
const contentHash = contentLookupResult[0]
|
||||
if (hexValueIsEmpty(contentHash)) {
|
||||
throw new Error(`EnsIpfsResolver - no content ID found for name "${name}"`)
|
||||
|
||||
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}
|
||||
}
|
||||
const nonPrefixedHex = contentHash.slice(2)
|
||||
const buffer = multihash.fromHexString(nonPrefixedHex)
|
||||
const contentId = multihash.toB58String(multihash.encode(buffer, 'sha2-256'))
|
||||
return contentId
|
||||
if (isLegacyResolver[0]) {
|
||||
// lookup content id
|
||||
const contentLookupResult = await Resolver.content(hash)
|
||||
const content = contentLookupResult[0]
|
||||
if (hexValueIsEmpty(content)) {
|
||||
throw new Error(`EnsIpfsResolver - no content ID found for name "${name}"`)
|
||||
}
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
@ -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 || ''}`
|
||||
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
|
||||
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(resolvedUrl, { method: 'HEAD' })
|
||||
if (response.status === 200) url = resolvedUrl
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
// otherwise redirect to the correct page
|
||||
extension.tabs.update(tabId, { url })
|
||||
} 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
71
docs/creating-metrics-events.md
Normal file
71
docs/creating-metrics-events.md
Normal 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
|
2576
package-lock.json
generated
2576
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
@ -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 = [{
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 && (
|
||||
|
@ -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;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
|
@ -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',
|
||||
|
@ -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 = {},
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
setMaxModeTo(true)
|
||||
this.setMaxAmount()
|
||||
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}
|
||||
>
|
||||
{this.context.t('max')}
|
||||
</span>
|
||||
</div>
|
||||
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')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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',
|
||||
|
@ -110,7 +110,7 @@ export default class SendAmountRow extends Component {
|
||||
showError={inError}
|
||||
errorType={'amount'}
|
||||
>
|
||||
{!inError && gasTotal && <AmountMaxButton />}
|
||||
{gasTotal && <AmountMaxButton inError={inError} />}
|
||||
{ this.renderInput() }
|
||||
</SendRowWrapper>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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()),
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export default class SendRowWrapper extends Component {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
renderAmountFormRow () {
|
||||
const {
|
||||
children,
|
||||
errorType = '',
|
||||
@ -34,7 +34,39 @@ export default class SendRowWrapper extends Component {
|
||||
<div className="send-v2__form-row">
|
||||
<div className="send-v2__form-label">
|
||||
{label}
|
||||
{showError && <SendRowErrorMessage errorType={errorType}/>}
|
||||
{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">
|
||||
{label}
|
||||
{showError && <SendRowErrorMessage errorType={errorType} />}
|
||||
{!showError && showWarning && <SendRowWarningMessage warningType={warningType} />}
|
||||
{customLabelContent}
|
||||
</div>
|
||||
@ -45,4 +77,14 @@ export default class SendRowWrapper extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
errorType = '',
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
errorType === 'amount' ? this.renderAmountFormRow() : this.renderFormRow()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user