mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Refactor send page state management (#10965)
This commit is contained in:
parent
85f17831a2
commit
e17325c38a
@ -751,9 +751,6 @@
|
||||
"recents": {
|
||||
"message": "የቅርብ ጊዜያት"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "የተቀባይ አድራሻ"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "ፍለጋ፣ ለሕዝብ ክፍት የሆነ አድራሻ (0x), ወይም ENS"
|
||||
},
|
||||
|
@ -747,9 +747,6 @@
|
||||
"recents": {
|
||||
"message": "الحديث"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "عنوان المستلم"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "البحث، العنوان العام (0x)، أو ENS"
|
||||
},
|
||||
|
@ -750,9 +750,6 @@
|
||||
"recents": {
|
||||
"message": "Скорошни"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Адрес на получателя"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Търсене, публичен адрес (0x) или ENS"
|
||||
},
|
||||
|
@ -754,9 +754,6 @@
|
||||
"recents": {
|
||||
"message": "সাম্প্রতিকগুলি"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "প্রাপকের ঠিকানা"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "অনুসন্ধান, সার্বজনীন ঠিকানা (0x), বা ENS"
|
||||
},
|
||||
|
@ -732,9 +732,6 @@
|
||||
"readdToken": {
|
||||
"message": "Pots tornar a afegir aquesta fitxa en el futur anant a \"Afegir fitxa\" al menu d'opcions dels teus comptes."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adreça del destinatari"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Cerca, adreça pública (0x), o ENS"
|
||||
},
|
||||
|
@ -308,9 +308,6 @@
|
||||
"readdToken": {
|
||||
"message": "Tento token můžete v budoucnu přidat zpět s „Přidat token“ v nastavení účtu."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adresa příjemce"
|
||||
},
|
||||
"reject": {
|
||||
"message": "Odmítnout"
|
||||
},
|
||||
|
@ -735,9 +735,6 @@
|
||||
"recents": {
|
||||
"message": "Seneste"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Modtagerens adresse"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Søg, offentlig adresse (0x) eller ENS"
|
||||
},
|
||||
|
@ -723,9 +723,6 @@
|
||||
"recents": {
|
||||
"message": "Letzte"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Empfängeradresse"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Suchen, öffentliche Adresse (0x) oder ENS"
|
||||
},
|
||||
|
@ -751,9 +751,6 @@
|
||||
"recents": {
|
||||
"message": "Πρόσφατα"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Διεύθυνση Παραλήπτη"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Αναζήτηση, δημόσια διεύθυνση (0x) ή ENS"
|
||||
},
|
||||
|
@ -650,12 +650,21 @@
|
||||
"message": "The endpoint returned a different chain ID: $1",
|
||||
"description": "$1 is the return value of eth_chainId from an RPC endpoint"
|
||||
},
|
||||
"ensIllegalCharacter": {
|
||||
"message": "Illegal Character for ENS."
|
||||
},
|
||||
"ensNotFoundOnCurrentNetwork": {
|
||||
"message": "ENS name not found on the current network. Try switching to Ethereum Mainnet."
|
||||
},
|
||||
"ensNotSupportedOnNetwork": {
|
||||
"message": "Network does not support ENS"
|
||||
},
|
||||
"ensRegistrationError": {
|
||||
"message": "Error in ENS name registration"
|
||||
},
|
||||
"ensUnknownError": {
|
||||
"message": "ENS Lookup failed."
|
||||
},
|
||||
"enterAnAlias": {
|
||||
"message": "Enter an alias"
|
||||
},
|
||||
@ -1451,9 +1460,6 @@
|
||||
"recents": {
|
||||
"message": "Recents"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Recipient Address"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Search, public address (0x), or ENS"
|
||||
},
|
||||
|
@ -1411,9 +1411,6 @@
|
||||
"recents": {
|
||||
"message": "Recientes"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Dirección del destinatario"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Búsqueda, dirección pública (0x) o ENS"
|
||||
},
|
||||
|
@ -1419,9 +1419,6 @@
|
||||
"recents": {
|
||||
"message": "Recientes"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Dirección del destinatario"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Búsqueda, dirección pública (0x) o ENS"
|
||||
},
|
||||
|
@ -744,9 +744,6 @@
|
||||
"recents": {
|
||||
"message": "Hiljutised"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Saaja aadress"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Otsing, avalik aadress (0x) või ENS"
|
||||
},
|
||||
|
@ -754,9 +754,6 @@
|
||||
"recents": {
|
||||
"message": "واپسین"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "آدرس دریافت کننده"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "جستجو، آدرس عمومی (0x)، یا ENS"
|
||||
},
|
||||
|
@ -751,9 +751,6 @@
|
||||
"recents": {
|
||||
"message": "Viimeaikaiset"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Vastaanottajan osoite"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Haku, julkinen osoite (0x) tai ENS"
|
||||
},
|
||||
|
@ -678,9 +678,6 @@
|
||||
"recents": {
|
||||
"message": "Kamakailan"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Address ng Recipient"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Maghanap, pampublikong address (0x), o ENS"
|
||||
},
|
||||
|
@ -736,9 +736,6 @@
|
||||
"recents": {
|
||||
"message": "Récents"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adresse du destinataire"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Recherche, adresse publique (0x) ou ENS"
|
||||
},
|
||||
|
@ -751,9 +751,6 @@
|
||||
"recents": {
|
||||
"message": "אחרונים"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "כתובת הנמען"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "חיפוש, כתובת ציבורית (0x), או ENS"
|
||||
},
|
||||
|
@ -1411,9 +1411,6 @@
|
||||
"recents": {
|
||||
"message": "हाल ही के"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "प्राप्तकर्ता का पता"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "खोज, सार्वजनिक पता (0x) या ENS"
|
||||
},
|
||||
|
@ -285,9 +285,6 @@
|
||||
"readdToken": {
|
||||
"message": "आप अपने खाता विकल्प मेनू में .टोकन जोड़ें. पर जाकर भविष्य में इस टोकन को वापस जोड़ सकते हैं।"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "प्राप्तकर्ता पता"
|
||||
},
|
||||
"reject": {
|
||||
"message": "अस्वीकार"
|
||||
},
|
||||
|
@ -747,9 +747,6 @@
|
||||
"recents": {
|
||||
"message": "Nedavno"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adresa primatelja"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Pretraži, javne adrese (0x) ili ENS"
|
||||
},
|
||||
|
@ -450,9 +450,6 @@
|
||||
"readdToken": {
|
||||
"message": "Ou ka ajoute token sa aprè sa ankò ou prale nan \"Ajoute token\" nan opsyon meni kont ou an."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adrès pou resevwa"
|
||||
},
|
||||
"reject": {
|
||||
"message": "Rejte"
|
||||
},
|
||||
|
@ -747,9 +747,6 @@
|
||||
"recents": {
|
||||
"message": "Legutóbbiak"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Címzett címe"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Keresés, nyilvános cím (0x) vagy ENS"
|
||||
},
|
||||
|
@ -1411,9 +1411,6 @@
|
||||
"recents": {
|
||||
"message": "Terkini"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Alamat Penerima"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Cari, alamat publik (0x), atau ENS"
|
||||
},
|
||||
|
@ -1201,9 +1201,6 @@
|
||||
"recents": {
|
||||
"message": "Recenti"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Indirizzo Destinatario"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Ricerca, indirizzo pubblico (0x) o ENS"
|
||||
},
|
||||
|
@ -1411,9 +1411,6 @@
|
||||
"recents": {
|
||||
"message": "最近"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "受信者のアドレス"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "検索、パブリック アドレス (0x)、または ENS"
|
||||
},
|
||||
|
@ -754,9 +754,6 @@
|
||||
"recents": {
|
||||
"message": "ಇತ್ತೀಚಿನವುಗಳು"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "ಸ್ವೀಕರಿಸುವವರ ವಿಳಾಸ"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "ಸಾರ್ವಜನಿಕ ವಿಳಾಸ (0x) ಅಥವಾ ENS ಹುಡುಕಿ"
|
||||
},
|
||||
|
@ -1415,9 +1415,6 @@
|
||||
"recents": {
|
||||
"message": "최근"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "수신인 주소"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "검색, 공개 주소(0x) 또는 ENS"
|
||||
},
|
||||
|
@ -754,9 +754,6 @@
|
||||
"recents": {
|
||||
"message": "Naujausi"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Gavėjo adresas"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Ieška, viešieji adresai (0x) arba ENS"
|
||||
},
|
||||
|
@ -750,9 +750,6 @@
|
||||
"recents": {
|
||||
"message": "Nesenie"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Saņēmēja adrese"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Meklēšana, publiskā adrese (0x) vai ENS"
|
||||
},
|
||||
|
@ -731,9 +731,6 @@
|
||||
"recents": {
|
||||
"message": "Baru-baru ini"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Alamat Penerima"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Cari, alamat awam (0x), atau ENS"
|
||||
},
|
||||
|
@ -272,9 +272,6 @@
|
||||
"readdToken": {
|
||||
"message": "U kunt dit token in de toekomst weer toevoegen door naar \"Token toevoegen\" te gaan in het menu met accountopties."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Geadresseerde adres"
|
||||
},
|
||||
"reject": {
|
||||
"message": "Afwijzen"
|
||||
},
|
||||
|
@ -741,9 +741,6 @@
|
||||
"recents": {
|
||||
"message": "Nylige"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Mottakeradresse"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Søk, offentlig adresse (0x) eller ENS"
|
||||
},
|
||||
|
@ -1419,9 +1419,6 @@
|
||||
"recents": {
|
||||
"message": "Mga Kamakailan"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Address ng Tatanggap"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Maghanap, pampublikong address (0x), o ENS"
|
||||
},
|
||||
|
@ -748,9 +748,6 @@
|
||||
"recents": {
|
||||
"message": "Ostatnie"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adres odbiorcy"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Szukaj, adres publiczny (0x) lub ENS"
|
||||
},
|
||||
|
@ -282,9 +282,6 @@
|
||||
"readdToken": {
|
||||
"message": "Pode adicionar este token de novo clicando na opção “Adicionar token” no menu de opções da sua conta."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Endereço do Destinatário"
|
||||
},
|
||||
"reject": {
|
||||
"message": "Rejeitar"
|
||||
},
|
||||
|
@ -1405,9 +1405,6 @@
|
||||
"recents": {
|
||||
"message": "Recentes"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Endereço do destinatário"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Busca, endereço público (0x) ou ENS"
|
||||
},
|
||||
|
@ -741,9 +741,6 @@
|
||||
"recents": {
|
||||
"message": "Recente"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adresă destinatar"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Căutare, adresa publică (0x) sau ENS"
|
||||
},
|
||||
|
@ -1411,9 +1411,6 @@
|
||||
"recents": {
|
||||
"message": "Недавние"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Адрес получателя"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Поиск, публичный адрес (0x) или ENS"
|
||||
},
|
||||
|
@ -723,9 +723,6 @@
|
||||
"recents": {
|
||||
"message": "Posledné"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adresa příjemce"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Vyhľadávať verejnú adresu (0x) alebo ENS"
|
||||
},
|
||||
|
@ -742,9 +742,6 @@
|
||||
"recents": {
|
||||
"message": "Nedavno"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Prejemnikov naslov"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Iskanje, javni naslov (0x) ali ENS"
|
||||
},
|
||||
|
@ -745,9 +745,6 @@
|
||||
"recents": {
|
||||
"message": "Skorašnje"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Adresa primaoca"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Pretraga, javna adresa (0x) ili ENS"
|
||||
},
|
||||
|
@ -738,9 +738,6 @@
|
||||
"recents": {
|
||||
"message": "Senaste"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Mottagaradress"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Sök, allmän adress (0x) eller ENS"
|
||||
},
|
||||
|
@ -732,9 +732,6 @@
|
||||
"recents": {
|
||||
"message": "Za hivi karibuni"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Anwani ya Mpokeaji"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Tafuta, anwani za umma (0x), au ENS"
|
||||
},
|
||||
|
@ -372,9 +372,6 @@
|
||||
"readdToken": {
|
||||
"message": "உங்கள் கணக்கு விருப்பங்கள் மெனுவில் \"டோக்கனைச் சேர்\" என்பதன் மூலம் நீங்கள் எதிர்காலத்தில் இந்த டோக்கனை மீண்டும் சேர்க்கலாம்."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "பெறுநர் முகவரி"
|
||||
},
|
||||
"reject": {
|
||||
"message": "நிராகரி"
|
||||
},
|
||||
|
@ -375,9 +375,6 @@
|
||||
"readdToken": {
|
||||
"message": "คุณสามารถเพิ่มโทเค็นนี้ในอนาคตได้โดยไปที่ “เพิ่มโทเค็น” ในเมนูตัวเลือกบัญชีของคุณ"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "แอดแดรสผู้รับ"
|
||||
},
|
||||
"reject": {
|
||||
"message": "ปฏิเสธ"
|
||||
},
|
||||
|
@ -1192,9 +1192,6 @@
|
||||
"recents": {
|
||||
"message": "Mga Kamakailan"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Address ng Tatanggap"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Maghanap, pampublikong address (0x), o ENS"
|
||||
},
|
||||
|
@ -324,9 +324,6 @@
|
||||
"readdToken": {
|
||||
"message": "Gelecekte Bu jetonu hesap seçenekleri menüsünde “Jeton ekle”'ye giderek geri ekleyebilirsiniz."
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Alıcı adresi"
|
||||
},
|
||||
"reject": {
|
||||
"message": "Reddetmek"
|
||||
},
|
||||
|
@ -754,9 +754,6 @@
|
||||
"recents": {
|
||||
"message": "Останні"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Адреса отримувача"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Пошук, публічна адреса (0x), або ENS"
|
||||
},
|
||||
|
@ -1411,9 +1411,6 @@
|
||||
"recents": {
|
||||
"message": "Gần đây"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "Địa chỉ người nhận"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Tìm kiếm, địa chỉ công khai (0x) hoặc ENS"
|
||||
},
|
||||
|
@ -1195,9 +1195,6 @@
|
||||
"recents": {
|
||||
"message": "最近记录"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "接收地址"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "查找、公用地址 (0x) 或 ENS"
|
||||
},
|
||||
|
@ -751,9 +751,6 @@
|
||||
"recents": {
|
||||
"message": "最近"
|
||||
},
|
||||
"recipientAddress": {
|
||||
"message": "接收位址"
|
||||
},
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "搜尋,公開地址 (0x),或 ENS"
|
||||
},
|
||||
|
219
test/e2e/tests/send-eth.spec.js
Normal file
219
test/e2e/tests/send-eth.spec.js
Normal file
@ -0,0 +1,219 @@
|
||||
const { strict: assert } = require('assert');
|
||||
const { withFixtures, regularDelayMs } = require('../helpers');
|
||||
|
||||
describe('Send ETH from inside MetaMask using default gas', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: 25000000000000000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'imported-account',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await driver.navigate();
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
await driver.clickElement('[data-testid="eth-overview-send"]');
|
||||
|
||||
await driver.fill(
|
||||
'input[placeholder="Search, public address (0x), or ENS"]',
|
||||
'0x2f318C334780961FB129D2a6c30D0763d9a5C970',
|
||||
);
|
||||
|
||||
const inputAmount = await driver.findElement('.unit-input__input');
|
||||
await inputAmount.fill('1000');
|
||||
|
||||
const errorAmount = await driver.findElement('.send-v2__error-amount');
|
||||
assert.equal(
|
||||
await errorAmount.getText(),
|
||||
'Insufficient funds.',
|
||||
'send screen should render an insufficient fund error message',
|
||||
);
|
||||
|
||||
await inputAmount.press(driver.Key.BACK_SPACE);
|
||||
await inputAmount.press(driver.Key.BACK_SPACE);
|
||||
await inputAmount.press(driver.Key.BACK_SPACE);
|
||||
await driver.delay(regularDelayMs);
|
||||
|
||||
await driver.assertElementNotPresent('.send-v2__error-amount');
|
||||
|
||||
const amountMax = await driver.findClickableElement(
|
||||
'.send-v2__amount-max',
|
||||
);
|
||||
await amountMax.click();
|
||||
|
||||
let inputValue = await inputAmount.getAttribute('value');
|
||||
|
||||
assert(Number(inputValue) > 24);
|
||||
|
||||
await amountMax.click();
|
||||
|
||||
assert.equal(await inputAmount.isEnabled(), true);
|
||||
|
||||
await inputAmount.fill('1');
|
||||
|
||||
inputValue = await inputAmount.getAttribute('value');
|
||||
assert.equal(inputValue, '1');
|
||||
|
||||
// Continue to next screen
|
||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||
|
||||
await driver.clickElement('[data-testid="home__activity-tab"]');
|
||||
await driver.wait(async () => {
|
||||
const confirmedTxes = await driver.findElements(
|
||||
'.transaction-list__completed-transactions .transaction-list-item',
|
||||
);
|
||||
return confirmedTxes.length === 1;
|
||||
}, 10000);
|
||||
|
||||
await driver.waitForSelector({
|
||||
css: '.transaction-list-item__primary-currency',
|
||||
text: '-1 ETH',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Send ETH from inside MetaMask using fast gas option', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: 25000000000000000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'imported-account',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await driver.navigate();
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
await driver.clickElement('[data-testid="eth-overview-send"]');
|
||||
|
||||
await driver.fill(
|
||||
'input[placeholder="Search, public address (0x), or ENS"]',
|
||||
'0x2f318C334780961FB129D2a6c30D0763d9a5C970',
|
||||
);
|
||||
|
||||
const inputAmount = await driver.findElement('.unit-input__input');
|
||||
await inputAmount.fill('1');
|
||||
|
||||
const inputValue = await inputAmount.getAttribute('value');
|
||||
assert.equal(inputValue, '1');
|
||||
|
||||
// Set the gas price
|
||||
await driver.clickElement({ text: 'Fast', tag: 'button/div/div' });
|
||||
|
||||
// Continue to next screen
|
||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||
|
||||
await driver.waitForSelector(
|
||||
'.transaction-list__completed-transactions .transaction-list-item',
|
||||
);
|
||||
await driver.waitForSelector({
|
||||
css: '.transaction-list-item__primary-currency',
|
||||
text: '-1 ETH',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Send ETH from inside MetaMask using advanced gas modal', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: 25000000000000000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'imported-account',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await driver.navigate();
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
await driver.clickElement('[data-testid="eth-overview-send"]');
|
||||
|
||||
await driver.fill(
|
||||
'input[placeholder="Search, public address (0x), or ENS"]',
|
||||
'0x2f318C334780961FB129D2a6c30D0763d9a5C970',
|
||||
);
|
||||
|
||||
const inputAmount = await driver.findElement('.unit-input__input');
|
||||
await inputAmount.fill('1');
|
||||
|
||||
const inputValue = await inputAmount.getAttribute('value');
|
||||
assert.equal(inputValue, '1');
|
||||
|
||||
// Set the gas limit
|
||||
await driver.clickElement('.advanced-gas-options-btn');
|
||||
|
||||
// wait for gas modal to be visible
|
||||
const gasModal = await driver.findVisibleElement('span .modal');
|
||||
|
||||
await driver.clickElement({ text: 'Save', tag: 'button' });
|
||||
|
||||
// Wait for gas modal to be removed from DOM
|
||||
await gasModal.waitForElementState('hidden');
|
||||
|
||||
// Continue to next screen
|
||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||
|
||||
const transactionAmounts = await driver.findElements(
|
||||
'.currency-display-component__text',
|
||||
);
|
||||
const transactionAmount = transactionAmounts[0];
|
||||
assert.equal(await transactionAmount.getText(), '1');
|
||||
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||
|
||||
await driver.wait(async () => {
|
||||
const confirmedTxes = await driver.findElements(
|
||||
'.transaction-list__completed-transactions .transaction-list-item',
|
||||
);
|
||||
return confirmedTxes.length === 1;
|
||||
}, 10000);
|
||||
|
||||
await driver.waitForSelector(
|
||||
{
|
||||
css: '.transaction-list-item__primary-currency',
|
||||
text: '-1 ETH',
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -10,7 +10,7 @@ import InfoIcon from '../../ui/icon/info-icon.component';
|
||||
import Button from '../../ui/button';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { useMetricEvent } from '../../../hooks/useMetricEvent';
|
||||
import { updateSendToken } from '../../../ducks/send/send.duck';
|
||||
import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
|
||||
import { SEND_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { SEVERITIES } from '../../../helpers/constants/design-system';
|
||||
|
||||
@ -69,13 +69,17 @@ const AssetListItem = ({
|
||||
e.stopPropagation();
|
||||
sendTokenEvent();
|
||||
dispatch(
|
||||
updateSendToken({
|
||||
address: tokenAddress,
|
||||
decimals: tokenDecimals,
|
||||
symbol: tokenSymbol,
|
||||
updateSendAsset({
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
details: {
|
||||
address: tokenAddress,
|
||||
decimals: tokenDecimals,
|
||||
symbol: tokenSymbol,
|
||||
},
|
||||
}),
|
||||
);
|
||||
history.push(SEND_ROUTE);
|
||||
).then(() => {
|
||||
history.push(SEND_ROUTE);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('sendSpecifiedTokens', [tokenSymbol])}
|
||||
|
@ -9,10 +9,10 @@ import {
|
||||
} from '../../../../ducks/gas/gas.duck';
|
||||
|
||||
import {
|
||||
hideGasButtonGroup,
|
||||
setGasLimit,
|
||||
setGasPrice,
|
||||
} from '../../../../ducks/send/send.duck';
|
||||
useCustomGas,
|
||||
updateGasLimit,
|
||||
updateGasPrice,
|
||||
} from '../../../../ducks/send';
|
||||
|
||||
let mapDispatchToProps;
|
||||
let mergeProps;
|
||||
@ -32,8 +32,6 @@ jest.mock('../../../../selectors', () => ({
|
||||
`mockRenderableBasicEstimateData:${Object.keys(s).length}`,
|
||||
getDefaultActiveButtonIndex: (a, b) => a + b,
|
||||
getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
|
||||
getSendToken: () => null,
|
||||
getTokenBalance: (state) => state.send.tokenBalance || '0x0',
|
||||
getCustomGasPrice: (state) => state.gas.customData.price || '0x0',
|
||||
getCustomGasLimit: (state) => state.gas.customData.limit || '0x0',
|
||||
getCurrentCurrency: jest.fn().mockReturnValue('usd'),
|
||||
@ -57,11 +55,15 @@ jest.mock('../../../../ducks/gas/gas.duck', () => ({
|
||||
resetCustomData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../ducks/send/send.duck', () => ({
|
||||
hideGasButtonGroup: jest.fn(),
|
||||
setGasLimit: jest.fn(),
|
||||
setGasPrice: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../../ducks/send', () => {
|
||||
const { ASSET_TYPES } = jest.requireActual('../../../../ducks/send');
|
||||
return {
|
||||
useCustomGas: jest.fn(),
|
||||
updateGasLimit: jest.fn(),
|
||||
updateGasPrice: jest.fn(),
|
||||
getSendAsset: jest.fn(() => ({ type: ASSET_TYPES.NATIVE })),
|
||||
};
|
||||
});
|
||||
|
||||
require('./gas-modal-page-container.container');
|
||||
|
||||
@ -79,11 +81,11 @@ describe('gas-modal-page-container container', () => {
|
||||
dispatchSpy.resetHistory();
|
||||
});
|
||||
|
||||
describe('hideGasButtonGroup()', () => {
|
||||
it('should dispatch a hideGasButtonGroup action', () => {
|
||||
mapDispatchToPropsObject.hideGasButtonGroup();
|
||||
describe('useCustomGas()', () => {
|
||||
it('should dispatch a useCustomGas action', () => {
|
||||
mapDispatchToPropsObject.useCustomGas();
|
||||
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||
expect(hideGasButtonGroup).toHaveBeenCalled();
|
||||
expect(useCustomGas).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -126,13 +128,13 @@ describe('gas-modal-page-container container', () => {
|
||||
});
|
||||
|
||||
describe('setGasData()', () => {
|
||||
it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => {
|
||||
it('should dispatch a updateGasPrice and updateGasLimit action with the correct props', () => {
|
||||
mapDispatchToPropsObject.setGasData('ffff', 'aaaa');
|
||||
expect(dispatchSpy.calledTwice).toStrictEqual(true);
|
||||
expect(setGasPrice).toHaveBeenCalled();
|
||||
expect(setGasLimit).toHaveBeenCalled();
|
||||
expect(setGasLimit).toHaveBeenCalledWith('ffff');
|
||||
expect(setGasPrice).toHaveBeenCalledWith('aaaa');
|
||||
expect(updateGasPrice).toHaveBeenCalled();
|
||||
expect(updateGasLimit).toHaveBeenCalled();
|
||||
expect(updateGasLimit).toHaveBeenCalledWith('ffff');
|
||||
expect(updateGasPrice).toHaveBeenCalledWith('aaaa');
|
||||
});
|
||||
});
|
||||
|
||||
@ -165,7 +167,7 @@ describe('gas-modal-page-container container', () => {
|
||||
};
|
||||
dispatchProps = {
|
||||
updateCustomGasPrice: sinon.spy(),
|
||||
hideGasButtonGroup: sinon.spy(),
|
||||
useCustomGas: sinon.spy(),
|
||||
setGasData: sinon.spy(),
|
||||
updateConfirmTxGasAndCalculate: sinon.spy(),
|
||||
someOtherDispatchProp: sinon.spy(),
|
||||
@ -194,7 +196,7 @@ describe('gas-modal-page-container container', () => {
|
||||
dispatchProps.updateConfirmTxGasAndCalculate.callCount,
|
||||
).toStrictEqual(0);
|
||||
expect(dispatchProps.setGasData.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.hideModal.callCount).toStrictEqual(0);
|
||||
|
||||
result.onSubmit();
|
||||
@ -203,7 +205,7 @@ describe('gas-modal-page-container container', () => {
|
||||
dispatchProps.updateConfirmTxGasAndCalculate.callCount,
|
||||
).toStrictEqual(1);
|
||||
expect(dispatchProps.setGasData.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.hideModal.callCount).toStrictEqual(1);
|
||||
|
||||
expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0);
|
||||
@ -238,7 +240,7 @@ describe('gas-modal-page-container container', () => {
|
||||
dispatchProps.updateConfirmTxGasAndCalculate.callCount,
|
||||
).toStrictEqual(0);
|
||||
expect(dispatchProps.setGasData.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(0);
|
||||
|
||||
result.onSubmit('mockNewLimit', 'mockNewPrice');
|
||||
@ -251,7 +253,7 @@ describe('gas-modal-page-container container', () => {
|
||||
'mockNewLimit',
|
||||
'mockNewPrice',
|
||||
]);
|
||||
expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(1);
|
||||
expect(dispatchProps.useCustomGas.callCount).toStrictEqual(1);
|
||||
expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1);
|
||||
|
||||
expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0);
|
||||
@ -278,7 +280,7 @@ describe('gas-modal-page-container container', () => {
|
||||
dispatchProps.updateConfirmTxGasAndCalculate.callCount,
|
||||
).toStrictEqual(0);
|
||||
expect(dispatchProps.setGasData.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0);
|
||||
expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1);
|
||||
|
||||
expect(dispatchProps.createSpeedUpTransaction.callCount).toStrictEqual(1);
|
||||
|
@ -14,19 +14,21 @@ import {
|
||||
fetchBasicGasEstimates,
|
||||
} from '../../../../ducks/gas/gas.duck';
|
||||
import {
|
||||
hideGasButtonGroup,
|
||||
setGasLimit,
|
||||
setGasPrice,
|
||||
setGasTotal,
|
||||
updateSendAmount,
|
||||
updateSendErrors,
|
||||
} from '../../../../ducks/send/send.duck';
|
||||
getSendMaxModeState,
|
||||
getGasLimit,
|
||||
getGasPrice,
|
||||
getSendAmount,
|
||||
updateGasLimit,
|
||||
updateGasPrice,
|
||||
useCustomGas,
|
||||
getSendAsset,
|
||||
ASSET_TYPES,
|
||||
} from '../../../../ducks/send';
|
||||
import {
|
||||
conversionRateSelector as getConversionRate,
|
||||
getCurrentCurrency,
|
||||
getCurrentEthBalance,
|
||||
getIsMainnet,
|
||||
getSendToken,
|
||||
getPreferences,
|
||||
getIsTestnet,
|
||||
getBasicGasEstimateLoadingStatus,
|
||||
@ -35,8 +37,6 @@ import {
|
||||
getDefaultActiveButtonIndex,
|
||||
getRenderableBasicEstimateData,
|
||||
isCustomPriceSafe,
|
||||
getTokenBalance,
|
||||
getSendMaxModeState,
|
||||
isCustomPriceSafeForCustomNetwork,
|
||||
getAveragePriceEstimateInHexWEI,
|
||||
isCustomPriceExcessive,
|
||||
@ -57,16 +57,15 @@ import {
|
||||
isBalanceSufficient,
|
||||
} from '../../../../pages/send/send.utils';
|
||||
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants';
|
||||
import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils';
|
||||
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction';
|
||||
import { GAS_LIMITS } from '../../../../../shared/constants/gas';
|
||||
import GasModalPageContainer from './gas-modal-page-container.component';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
metamask: { currentNetworkTxList },
|
||||
send,
|
||||
} = state;
|
||||
const gasLimit = getGasLimit(state);
|
||||
const gasPrice = getGasPrice(state);
|
||||
const amount = getSendAmount(state);
|
||||
const { currentNetworkTxList } = state.metamask;
|
||||
const { modalState: { props: modalProps } = {} } = state.appState.modal || {};
|
||||
const { txData = {} } = modalProps || {};
|
||||
const { transaction = {}, onSubmit } = ownProps;
|
||||
@ -74,15 +73,15 @@ const mapStateToProps = (state, ownProps) => {
|
||||
({ id }) => id === (transaction.id || txData.id),
|
||||
);
|
||||
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state);
|
||||
const sendToken = getSendToken(state);
|
||||
const asset = getSendAsset(state);
|
||||
|
||||
// a "default" txParams is used during the send flow, since the transaction doesn't exist yet in that case
|
||||
const txParams = selectedTransaction?.txParams
|
||||
? selectedTransaction.txParams
|
||||
: {
|
||||
gas: send.gasLimit || GAS_LIMITS.SIMPLE,
|
||||
gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state, true),
|
||||
value: sendToken ? '0x0' : send.amount,
|
||||
gas: gasLimit || GAS_LIMITS.SIMPLE,
|
||||
gasPrice: gasPrice || getAveragePriceEstimateInHexWEI(state, true),
|
||||
value: asset.type === ASSET_TYPES.TOKEN ? '0x0' : amount,
|
||||
};
|
||||
|
||||
const { gasPrice: currentGasPrice, gas: currentGasLimit } = txParams;
|
||||
@ -120,16 +119,15 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const isMainnet = getIsMainnet(state);
|
||||
const showFiat = Boolean(isMainnet || showFiatInTestnets);
|
||||
|
||||
const isSendTokenSet = Boolean(sendToken);
|
||||
const isTestnet = getIsTestnet(state);
|
||||
|
||||
const newTotalEth =
|
||||
maxModeOn && !isSendTokenSet
|
||||
maxModeOn && asset.type === ASSET_TYPES.NATIVE
|
||||
? sumHexWEIsToRenderableEth([balance, '0x0'])
|
||||
: sumHexWEIsToRenderableEth([value, customGasTotal]);
|
||||
|
||||
const sendAmount =
|
||||
maxModeOn && !isSendTokenSet
|
||||
maxModeOn && asset.type === ASSET_TYPES.NATIVE
|
||||
? subtractHexWEIsFromRenderableEth(balance, customGasTotal)
|
||||
: sumHexWEIsToRenderableEth([value, '0x0']);
|
||||
|
||||
@ -194,9 +192,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
txId: transaction.id,
|
||||
insufficientBalance,
|
||||
isMainnet,
|
||||
sendToken,
|
||||
balance,
|
||||
tokenBalance: getTokenBalance(state),
|
||||
conversionRate,
|
||||
value,
|
||||
onSubmit,
|
||||
@ -213,12 +209,13 @@ const mapDispatchToProps = (dispatch) => {
|
||||
dispatch(hideModal());
|
||||
},
|
||||
hideModal: () => dispatch(hideModal()),
|
||||
useCustomGas: () => dispatch(useCustomGas()),
|
||||
updateCustomGasPrice,
|
||||
updateCustomGasLimit: (newLimit) =>
|
||||
dispatch(setCustomGasLimit(addHexPrefix(newLimit))),
|
||||
setGasData: (newLimit, newPrice) => {
|
||||
dispatch(setGasLimit(newLimit));
|
||||
dispatch(setGasPrice(newPrice));
|
||||
dispatch(updateGasLimit(newLimit));
|
||||
dispatch(updateGasPrice(newPrice));
|
||||
},
|
||||
updateConfirmTxGasAndCalculate: (gasLimit, gasPrice, updatedTx) => {
|
||||
updateCustomGasPrice(gasPrice);
|
||||
@ -231,14 +228,8 @@ const mapDispatchToProps = (dispatch) => {
|
||||
createSpeedUpTransaction: (txId, gasPrice, gasLimit) => {
|
||||
return dispatch(createSpeedUpTransaction(txId, gasPrice, gasLimit));
|
||||
},
|
||||
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
|
||||
hideSidebar: () => dispatch(hideSidebar()),
|
||||
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
|
||||
setGasTotal: (total) => dispatch(setGasTotal(total)),
|
||||
setAmountToMax: (maxAmountDataObject) => {
|
||||
dispatch(updateSendErrors({ amount: null }));
|
||||
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -251,17 +242,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
isSpeedUp,
|
||||
isRetry,
|
||||
insufficientBalance,
|
||||
maxModeOn,
|
||||
customGasPrice,
|
||||
customGasTotal,
|
||||
balance,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
customGasLimit,
|
||||
transaction,
|
||||
} = stateProps;
|
||||
const {
|
||||
hideGasButtonGroup: dispatchHideGasButtonGroup,
|
||||
useCustomGas: dispatchUseCustomGas,
|
||||
setGasData: dispatchSetGasData,
|
||||
updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
|
||||
createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
|
||||
@ -269,7 +255,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
hideSidebar: dispatchHideSidebar,
|
||||
cancelAndClose: dispatchCancelAndClose,
|
||||
hideModal: dispatchHideModal,
|
||||
setAmountToMax: dispatchSetAmountToMax,
|
||||
...otherDispatchProps
|
||||
} = dispatchProps;
|
||||
|
||||
@ -305,17 +290,9 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
dispatchCancelAndClose();
|
||||
} else {
|
||||
dispatchSetGasData(gasLimit, gasPrice);
|
||||
dispatchHideGasButtonGroup();
|
||||
dispatchUseCustomGas();
|
||||
dispatchCancelAndClose();
|
||||
}
|
||||
if (maxModeOn) {
|
||||
dispatchSetAmountToMax({
|
||||
balance,
|
||||
gasTotal: customGasTotal,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
});
|
||||
}
|
||||
},
|
||||
gasPriceButtonGroupProps: {
|
||||
...gasPriceButtonGroupProps,
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
} from '../../../hooks/useMetricEvent';
|
||||
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
||||
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
||||
import { updateSendToken } from '../../../ducks/send/send.duck';
|
||||
import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
|
||||
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
|
||||
import {
|
||||
getAssetImages,
|
||||
@ -85,8 +85,14 @@ const TokenOverview = ({ className, token }) => {
|
||||
className="token-overview__button"
|
||||
onClick={() => {
|
||||
sendTokenEvent();
|
||||
dispatch(updateSendToken(token));
|
||||
history.push(SEND_ROUTE);
|
||||
dispatch(
|
||||
updateSendAsset({
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
details: token,
|
||||
}),
|
||||
).then(() => {
|
||||
history.push(SEND_ROUTE);
|
||||
});
|
||||
}}
|
||||
Icon={SendIcon}
|
||||
label={t('send')}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { removeLeadingZeroes } from '../../../pages/send/send.utils';
|
||||
|
||||
function removeLeadingZeroes(str) {
|
||||
return str.replace(/^0*(?=\d)/u, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also
|
||||
|
@ -15,10 +15,11 @@ import {
|
||||
getNumberOfAccounts,
|
||||
getNumberOfTokens,
|
||||
} from '../selectors/selectors';
|
||||
import { getSendToken } from '../selectors/send';
|
||||
import { getSendAsset, ASSET_TYPES } from '../ducks/send';
|
||||
import { txDataSelector } from '../selectors/confirm-transaction';
|
||||
import { getEnvironmentType } from '../../app/scripts/lib/util';
|
||||
import { trackMetaMetricsEvent } from '../store/actions';
|
||||
import { getNativeCurrency } from '../ducks/metamask/metamask';
|
||||
|
||||
export const MetaMetricsContext = createContext(() => {
|
||||
captureException(
|
||||
@ -31,7 +32,8 @@ export const MetaMetricsContext = createContext(() => {
|
||||
export function MetaMetricsProvider({ children }) {
|
||||
const txData = useSelector(txDataSelector) || {};
|
||||
const environmentType = getEnvironmentType();
|
||||
const activeCurrency = useSelector(getSendToken)?.symbol;
|
||||
const activeAsset = useSelector(getSendAsset);
|
||||
const nativeAssetSymbol = useSelector(getNativeCurrency);
|
||||
const accountType = useSelector(getAccountType);
|
||||
const confirmTransactionOrigin = txData.origin;
|
||||
const numberOfTokens = useSelector(getNumberOfTokens);
|
||||
@ -72,7 +74,10 @@ export function MetaMetricsProvider({ children }) {
|
||||
action: eventOpts.action,
|
||||
number_of_tokens: numberOfTokens,
|
||||
number_of_accounts: numberOfAccounts,
|
||||
active_currency: activeCurrency,
|
||||
active_currency:
|
||||
activeAsset.type === ASSET_TYPES.NATIVE
|
||||
? nativeAssetSymbol
|
||||
: activeAsset?.details?.symbol,
|
||||
account_type: accountType,
|
||||
is_new_visit: config.is_new_visit,
|
||||
// the properties coming from this key will not match our standards for
|
||||
@ -102,7 +107,8 @@ export function MetaMetricsProvider({ children }) {
|
||||
accountType,
|
||||
currentPath,
|
||||
confirmTransactionOrigin,
|
||||
activeCurrency,
|
||||
activeAsset,
|
||||
nativeAssetSymbol,
|
||||
numberOfTokens,
|
||||
numberOfAccounts,
|
||||
environmentType,
|
||||
|
197
ui/ducks/ens.js
Normal file
197
ui/ducks/ens.js
Normal file
@ -0,0 +1,197 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import ENS from 'ethjs-ens';
|
||||
import log from 'loglevel';
|
||||
import networkMap from 'ethereum-ens-network-map';
|
||||
import { isConfusing } from 'unicode-confusables';
|
||||
import { isHexString } from 'ethereumjs-util';
|
||||
|
||||
import { getCurrentChainId } from '../selectors';
|
||||
import {
|
||||
CHAIN_ID_TO_NETWORK_ID_MAP,
|
||||
MAINNET_NETWORK_ID,
|
||||
} from '../../shared/constants/network';
|
||||
import {
|
||||
CONFUSING_ENS_ERROR,
|
||||
ENS_ILLEGAL_CHARACTER,
|
||||
ENS_NOT_FOUND_ON_NETWORK,
|
||||
ENS_NOT_SUPPORTED_ON_NETWORK,
|
||||
ENS_NO_ADDRESS_FOR_NAME,
|
||||
ENS_REGISTRATION_ERROR,
|
||||
ENS_UNKNOWN_ERROR,
|
||||
} from '../pages/send/send.constants';
|
||||
import { isValidDomainName } from '../helpers/utils/util';
|
||||
import { CHAIN_CHANGED } from '../store/actionConstants';
|
||||
import {
|
||||
BURN_ADDRESS,
|
||||
isBurnAddress,
|
||||
isValidHexAddress,
|
||||
} from '../../shared/modules/hexstring-utils';
|
||||
|
||||
// Local Constants
|
||||
const ZERO_X_ERROR_ADDRESS = '0x';
|
||||
|
||||
const initialState = {
|
||||
stage: 'UNINITIALIZED',
|
||||
resolution: null,
|
||||
error: null,
|
||||
warning: null,
|
||||
network: null,
|
||||
};
|
||||
|
||||
export const ensInitialState = initialState;
|
||||
|
||||
const name = 'ENS';
|
||||
|
||||
let ens = null;
|
||||
|
||||
const slice = createSlice({
|
||||
name,
|
||||
initialState,
|
||||
reducers: {
|
||||
ensLookup: (state, action) => {
|
||||
// first clear out the previous state
|
||||
state.resolution = null;
|
||||
state.error = null;
|
||||
state.warning = null;
|
||||
const { address, ensName, error, network } = action.payload;
|
||||
|
||||
if (error) {
|
||||
if (
|
||||
isValidDomainName(ensName) &&
|
||||
error.message === 'ENS name not defined.'
|
||||
) {
|
||||
state.error =
|
||||
network === MAINNET_NETWORK_ID
|
||||
? ENS_NO_ADDRESS_FOR_NAME
|
||||
: ENS_NOT_FOUND_ON_NETWORK;
|
||||
} else if (error.message === 'Illegal Character for ENS.') {
|
||||
state.error = ENS_ILLEGAL_CHARACTER;
|
||||
} else {
|
||||
log.error(error);
|
||||
state.error = ENS_UNKNOWN_ERROR;
|
||||
}
|
||||
} else if (address) {
|
||||
if (address === BURN_ADDRESS) {
|
||||
state.error = ENS_NO_ADDRESS_FOR_NAME;
|
||||
} else if (address === ZERO_X_ERROR_ADDRESS) {
|
||||
state.error = ENS_REGISTRATION_ERROR;
|
||||
} else {
|
||||
state.resolution = address;
|
||||
}
|
||||
if (isValidDomainName(address) && isConfusing(address)) {
|
||||
state.warning = CONFUSING_ENS_ERROR;
|
||||
}
|
||||
}
|
||||
},
|
||||
enableEnsLookup: (state, action) => {
|
||||
state.stage = 'INITIALIZED';
|
||||
state.error = null;
|
||||
state.resolution = null;
|
||||
state.warning = null;
|
||||
state.network = action.payload;
|
||||
},
|
||||
disableEnsLookup: (state) => {
|
||||
state.stage = 'NO_NETWORK_SUPPORT';
|
||||
state.error = ENS_NOT_SUPPORTED_ON_NETWORK;
|
||||
state.warning = null;
|
||||
state.resolution = null;
|
||||
state.network = null;
|
||||
},
|
||||
resetResolution: (state) => {
|
||||
state.resolution = null;
|
||||
state.warning = null;
|
||||
state.error =
|
||||
state.stage === 'NO_NETWORK_SUPPORT'
|
||||
? ENS_NOT_SUPPORTED_ON_NETWORK
|
||||
: null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(CHAIN_CHANGED, (state, action) => {
|
||||
if (action.payload !== state.currentChainId) {
|
||||
state.stage = 'UNINITIALIZED';
|
||||
ens = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { reducer, actions } = slice;
|
||||
export default reducer;
|
||||
|
||||
const {
|
||||
disableEnsLookup,
|
||||
ensLookup,
|
||||
enableEnsLookup,
|
||||
resetResolution,
|
||||
} = actions;
|
||||
export { resetResolution };
|
||||
|
||||
export function initializeEnsSlice() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const chainId = getCurrentChainId(state);
|
||||
const network = CHAIN_ID_TO_NETWORK_ID_MAP[chainId];
|
||||
const networkIsSupported = Boolean(networkMap[network]);
|
||||
if (networkIsSupported) {
|
||||
ens = new ENS({ provider: global.ethereumProvider, network });
|
||||
dispatch(enableEnsLookup(network));
|
||||
} else {
|
||||
ens = null;
|
||||
dispatch(disableEnsLookup());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function lookupEnsName(ensName) {
|
||||
return async (dispatch, getState) => {
|
||||
const trimmedEnsName = ensName.trim();
|
||||
let state = getState();
|
||||
if (state[name].stage === 'UNINITIALIZED') {
|
||||
await dispatch(initializeEnsSlice());
|
||||
}
|
||||
state = getState();
|
||||
if (
|
||||
state[name].stage === 'NO_NETWORK_SUPPORT' &&
|
||||
!(
|
||||
isBurnAddress(trimmedEnsName) === false &&
|
||||
isValidHexAddress(trimmedEnsName, { mixedCaseUseChecksum: true })
|
||||
) &&
|
||||
!isHexString(trimmedEnsName)
|
||||
) {
|
||||
await dispatch(resetResolution());
|
||||
} else {
|
||||
log.info(`ENS attempting to resolve name: ${trimmedEnsName}`);
|
||||
let address;
|
||||
let error;
|
||||
try {
|
||||
address = await ens.lookup(trimmedEnsName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
const chainId = getCurrentChainId(state);
|
||||
const network = CHAIN_ID_TO_NETWORK_ID_MAP[chainId];
|
||||
await dispatch(
|
||||
ensLookup({
|
||||
ensName: trimmedEnsName,
|
||||
address,
|
||||
error,
|
||||
chainId,
|
||||
network,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getEnsResolution(state) {
|
||||
return state[name].resolution;
|
||||
}
|
||||
|
||||
export function getEnsError(state) {
|
||||
return state[name].error;
|
||||
}
|
||||
|
||||
export function getEnsWarning(state) {
|
||||
return state[name].warning;
|
||||
}
|
14
ui/ducks/gas/gas-action-constants.js
Normal file
14
ui/ducks/gas/gas-action-constants.js
Normal file
@ -0,0 +1,14 @@
|
||||
// This file has been separated because it is required in both the gas and send
|
||||
// slices. This created a circular dependency problem as both slices also
|
||||
// import from the actions and selectors files. This easiest path for
|
||||
// untangling is having the constants separate.
|
||||
|
||||
// Actions
|
||||
export const BASIC_GAS_ESTIMATE_STATUS =
|
||||
'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
|
||||
export const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA';
|
||||
export const SET_BASIC_GAS_ESTIMATE_DATA =
|
||||
'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
|
||||
export const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
|
||||
export const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
|
||||
export const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';
|
@ -10,6 +10,14 @@ import GasReducer, {
|
||||
fetchBasicGasEstimates,
|
||||
} from './gas.duck';
|
||||
|
||||
import {
|
||||
BASIC_GAS_ESTIMATE_STATUS,
|
||||
SET_BASIC_GAS_ESTIMATE_DATA,
|
||||
SET_CUSTOM_GAS_PRICE,
|
||||
SET_CUSTOM_GAS_LIMIT,
|
||||
SET_ESTIMATE_SOURCE,
|
||||
} from './gas-action-constants';
|
||||
|
||||
jest.mock('../../helpers/utils/storage-helpers.js', () => ({
|
||||
getStorageItem: jest.fn(),
|
||||
setStorageItem: jest.fn(),
|
||||
@ -61,13 +69,6 @@ describe('Gas Duck', () => {
|
||||
type: 'mainnet',
|
||||
};
|
||||
|
||||
const BASIC_GAS_ESTIMATE_STATUS = 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
|
||||
const SET_BASIC_GAS_ESTIMATE_DATA =
|
||||
'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
|
||||
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
|
||||
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
|
||||
const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';
|
||||
|
||||
describe('GasReducer()', () => {
|
||||
it('should initialize state', () => {
|
||||
expect(GasReducer(undefined, {})).toStrictEqual(initState);
|
||||
|
@ -10,6 +10,14 @@ import {
|
||||
} from '../../helpers/utils/conversions.util';
|
||||
import { getIsMainnet, getCurrentChainId } from '../../selectors';
|
||||
import fetchWithCache from '../../helpers/utils/fetch-with-cache';
|
||||
import {
|
||||
BASIC_GAS_ESTIMATE_STATUS,
|
||||
RESET_CUSTOM_DATA,
|
||||
SET_BASIC_GAS_ESTIMATE_DATA,
|
||||
SET_CUSTOM_GAS_LIMIT,
|
||||
SET_CUSTOM_GAS_PRICE,
|
||||
SET_ESTIMATE_SOURCE,
|
||||
} from './gas-action-constants';
|
||||
|
||||
export const BASIC_ESTIMATE_STATES = {
|
||||
LOADING: 'LOADING',
|
||||
@ -22,14 +30,6 @@ export const GAS_SOURCE = {
|
||||
ETHGASPRICE: 'eth_gasprice',
|
||||
};
|
||||
|
||||
// Actions
|
||||
const BASIC_GAS_ESTIMATE_STATUS = 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
|
||||
const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA';
|
||||
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
|
||||
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
|
||||
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
|
||||
const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';
|
||||
|
||||
const initState = {
|
||||
customData: {
|
||||
price: null,
|
||||
|
@ -2,7 +2,8 @@ import { combineReducers } from 'redux';
|
||||
import { ALERT_TYPES } from '../../shared/constants/alerts';
|
||||
import metamaskReducer from './metamask/metamask';
|
||||
import localeMessagesReducer from './locale/locale';
|
||||
import sendReducer from './send/send.duck';
|
||||
import sendReducer from './send/send';
|
||||
import ensReducer from './ens';
|
||||
import appStateReducer from './app/app';
|
||||
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck';
|
||||
import gasReducer from './gas/gas.duck';
|
||||
@ -16,6 +17,7 @@ export default combineReducers({
|
||||
activeTab: (s) => (s === undefined ? null : s),
|
||||
metamask: metamaskReducer,
|
||||
appState: appStateReducer,
|
||||
ENS: ensReducer,
|
||||
history: historyReducer,
|
||||
send: sendReducer,
|
||||
confirmTransaction: confirmTransactionReducer,
|
||||
|
1
ui/ducks/send/index.js
Normal file
1
ui/ducks/send/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from './send';
|
@ -1,142 +0,0 @@
|
||||
import SendReducer, {
|
||||
openToDropdown,
|
||||
closeToDropdown,
|
||||
updateSendErrors,
|
||||
showGasButtonGroup,
|
||||
hideGasButtonGroup,
|
||||
} from './send.duck';
|
||||
|
||||
describe('Send Duck', () => {
|
||||
const mockState = {
|
||||
mockProp: 123,
|
||||
};
|
||||
const initState = {
|
||||
toDropdownOpen: false,
|
||||
gasButtonGroupShown: true,
|
||||
errors: {},
|
||||
gasLimit: null,
|
||||
gasPrice: null,
|
||||
gasTotal: null,
|
||||
tokenBalance: '0x0',
|
||||
from: '',
|
||||
to: '',
|
||||
amount: '0',
|
||||
memo: '',
|
||||
maxModeOn: false,
|
||||
editingTransactionId: null,
|
||||
toNickname: '',
|
||||
ensResolution: null,
|
||||
ensResolutionError: '',
|
||||
gasIsLoading: false,
|
||||
};
|
||||
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN';
|
||||
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN';
|
||||
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS';
|
||||
const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE';
|
||||
const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP';
|
||||
const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP';
|
||||
|
||||
describe('SendReducer()', () => {
|
||||
it('should initialize state', () => {
|
||||
expect(SendReducer(undefined, {})).toStrictEqual(initState);
|
||||
});
|
||||
|
||||
it('should return state unchanged if it does not match a dispatched actions type', () => {
|
||||
expect(
|
||||
SendReducer(mockState, {
|
||||
type: 'someOtherAction',
|
||||
value: 'someValue',
|
||||
}),
|
||||
).toStrictEqual(mockState);
|
||||
});
|
||||
|
||||
it('should set toDropdownOpen to true when receiving a OPEN_TO_DROPDOWN action', () => {
|
||||
expect(
|
||||
SendReducer(mockState, {
|
||||
type: OPEN_TO_DROPDOWN,
|
||||
}),
|
||||
).toStrictEqual({ toDropdownOpen: true, ...mockState });
|
||||
});
|
||||
|
||||
it('should set toDropdownOpen to false when receiving a CLOSE_TO_DROPDOWN action', () => {
|
||||
expect(
|
||||
SendReducer(mockState, {
|
||||
type: CLOSE_TO_DROPDOWN,
|
||||
}),
|
||||
).toStrictEqual({ toDropdownOpen: false, ...mockState });
|
||||
});
|
||||
|
||||
it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', () => {
|
||||
expect(
|
||||
SendReducer(
|
||||
{ ...mockState, gasButtonGroupShown: false },
|
||||
{ type: SHOW_GAS_BUTTON_GROUP },
|
||||
),
|
||||
).toStrictEqual({ gasButtonGroupShown: true, ...mockState });
|
||||
});
|
||||
|
||||
it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', () => {
|
||||
expect(
|
||||
SendReducer(mockState, { type: HIDE_GAS_BUTTON_GROUP }),
|
||||
).toStrictEqual({ gasButtonGroupShown: false, ...mockState });
|
||||
});
|
||||
|
||||
it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', () => {
|
||||
const modifiedMockState = {
|
||||
...mockState,
|
||||
errors: {
|
||||
someError: false,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
SendReducer(modifiedMockState, {
|
||||
type: UPDATE_SEND_ERRORS,
|
||||
value: { someOtherError: true },
|
||||
}),
|
||||
).toStrictEqual({
|
||||
...modifiedMockState,
|
||||
errors: {
|
||||
someError: false,
|
||||
someOtherError: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the initial state in response to a RESET_SEND_STATE action', () => {
|
||||
expect(
|
||||
SendReducer(mockState, {
|
||||
type: RESET_SEND_STATE,
|
||||
}),
|
||||
).toStrictEqual(initState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Send Duck Actions', () => {
|
||||
it('calls openToDropdown action', () => {
|
||||
expect(openToDropdown()).toStrictEqual({ type: OPEN_TO_DROPDOWN });
|
||||
});
|
||||
|
||||
it('calls closeToDropdown action', () => {
|
||||
expect(closeToDropdown()).toStrictEqual({ type: CLOSE_TO_DROPDOWN });
|
||||
});
|
||||
|
||||
it('calls showGasButtonGroup action', () => {
|
||||
expect(showGasButtonGroup()).toStrictEqual({
|
||||
type: SHOW_GAS_BUTTON_GROUP,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls hideGasButtonGroup action', () => {
|
||||
expect(hideGasButtonGroup()).toStrictEqual({
|
||||
type: HIDE_GAS_BUTTON_GROUP,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls updateSendErrors action', () => {
|
||||
expect(updateSendErrors('mockErrorObject')).toStrictEqual({
|
||||
type: UPDATE_SEND_ERRORS,
|
||||
value: 'mockErrorObject',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,382 +0,0 @@
|
||||
import log from 'loglevel';
|
||||
import { estimateGas } from '../../store/actions';
|
||||
import { setCustomGasLimit } from '../gas/gas.duck';
|
||||
import {
|
||||
estimateGasForSend,
|
||||
calcTokenBalance,
|
||||
} from '../../pages/send/send.utils';
|
||||
|
||||
// Actions
|
||||
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN';
|
||||
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN';
|
||||
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS';
|
||||
const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE';
|
||||
const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP';
|
||||
const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP';
|
||||
const UPDATE_GAS_LIMIT = 'UPDATE_GAS_LIMIT';
|
||||
const UPDATE_GAS_PRICE = 'UPDATE_GAS_PRICE';
|
||||
const UPDATE_GAS_TOTAL = 'UPDATE_GAS_TOTAL';
|
||||
const UPDATE_SEND_HEX_DATA = 'UPDATE_SEND_HEX_DATA';
|
||||
const UPDATE_SEND_TOKEN_BALANCE = 'UPDATE_SEND_TOKEN_BALANCE';
|
||||
const UPDATE_SEND_TO = 'UPDATE_SEND_TO';
|
||||
const UPDATE_SEND_AMOUNT = 'UPDATE_SEND_AMOUNT';
|
||||
const UPDATE_MAX_MODE = 'UPDATE_MAX_MODE';
|
||||
const UPDATE_SEND = 'UPDATE_SEND';
|
||||
const UPDATE_SEND_TOKEN = 'UPDATE_SEND_TOKEN';
|
||||
const CLEAR_SEND = 'CLEAR_SEND';
|
||||
const GAS_LOADING_STARTED = 'GAS_LOADING_STARTED';
|
||||
const GAS_LOADING_FINISHED = 'GAS_LOADING_FINISHED';
|
||||
const UPDATE_SEND_ENS_RESOLUTION = 'UPDATE_SEND_ENS_RESOLUTION';
|
||||
const UPDATE_SEND_ENS_RESOLUTION_ERROR = 'UPDATE_SEND_ENS_RESOLUTION_ERROR';
|
||||
|
||||
const initState = {
|
||||
toDropdownOpen: false,
|
||||
gasButtonGroupShown: true,
|
||||
errors: {},
|
||||
gasLimit: null,
|
||||
gasPrice: null,
|
||||
gasTotal: null,
|
||||
tokenBalance: '0x0',
|
||||
from: '',
|
||||
to: '',
|
||||
amount: '0',
|
||||
memo: '',
|
||||
maxModeOn: false,
|
||||
editingTransactionId: null,
|
||||
toNickname: '',
|
||||
ensResolution: null,
|
||||
ensResolutionError: '',
|
||||
gasIsLoading: false,
|
||||
};
|
||||
|
||||
// Reducer
|
||||
export default function reducer(state = initState, action) {
|
||||
switch (action.type) {
|
||||
case OPEN_TO_DROPDOWN:
|
||||
return {
|
||||
...state,
|
||||
toDropdownOpen: true,
|
||||
};
|
||||
case CLOSE_TO_DROPDOWN:
|
||||
return {
|
||||
...state,
|
||||
toDropdownOpen: false,
|
||||
};
|
||||
case UPDATE_SEND_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
errors: {
|
||||
...state.errors,
|
||||
...action.value,
|
||||
},
|
||||
};
|
||||
case SHOW_GAS_BUTTON_GROUP:
|
||||
return {
|
||||
...state,
|
||||
gasButtonGroupShown: true,
|
||||
};
|
||||
case HIDE_GAS_BUTTON_GROUP:
|
||||
return {
|
||||
...state,
|
||||
gasButtonGroupShown: false,
|
||||
};
|
||||
case UPDATE_GAS_LIMIT:
|
||||
return {
|
||||
...state,
|
||||
gasLimit: action.value,
|
||||
};
|
||||
case UPDATE_GAS_PRICE:
|
||||
return {
|
||||
...state,
|
||||
gasPrice: action.value,
|
||||
};
|
||||
case RESET_SEND_STATE:
|
||||
return { ...initState };
|
||||
case UPDATE_GAS_TOTAL:
|
||||
return {
|
||||
...state,
|
||||
gasTotal: action.value,
|
||||
};
|
||||
case UPDATE_SEND_TOKEN_BALANCE:
|
||||
return {
|
||||
...state,
|
||||
tokenBalance: action.value,
|
||||
};
|
||||
case UPDATE_SEND_HEX_DATA:
|
||||
return {
|
||||
...state,
|
||||
data: action.value,
|
||||
};
|
||||
case UPDATE_SEND_TO:
|
||||
return {
|
||||
...state,
|
||||
to: action.value.to,
|
||||
toNickname: action.value.nickname,
|
||||
};
|
||||
case UPDATE_SEND_AMOUNT:
|
||||
return {
|
||||
...state,
|
||||
amount: action.value,
|
||||
};
|
||||
case UPDATE_MAX_MODE:
|
||||
return {
|
||||
...state,
|
||||
maxModeOn: action.value,
|
||||
};
|
||||
case UPDATE_SEND:
|
||||
return Object.assign(state, action.value);
|
||||
case UPDATE_SEND_TOKEN: {
|
||||
const newSend = {
|
||||
...state,
|
||||
token: action.value,
|
||||
};
|
||||
// erase token-related state when switching back to native currency
|
||||
if (newSend.editingTransactionId && !newSend.token) {
|
||||
const unapprovedTx =
|
||||
newSend?.unapprovedTxs?.[newSend.editingTransactionId] || {};
|
||||
const txParams = unapprovedTx.txParams || {};
|
||||
Object.assign(newSend, {
|
||||
tokenBalance: null,
|
||||
balance: '0',
|
||||
from: unapprovedTx.from || '',
|
||||
unapprovedTxs: {
|
||||
...newSend.unapprovedTxs,
|
||||
[newSend.editingTransactionId]: {
|
||||
...unapprovedTx,
|
||||
txParams: {
|
||||
...txParams,
|
||||
data: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return Object.assign(state, newSend);
|
||||
}
|
||||
case UPDATE_SEND_ENS_RESOLUTION:
|
||||
return {
|
||||
...state,
|
||||
ensResolution: action.payload,
|
||||
ensResolutionError: '',
|
||||
};
|
||||
case UPDATE_SEND_ENS_RESOLUTION_ERROR:
|
||||
return {
|
||||
...state,
|
||||
ensResolution: null,
|
||||
ensResolutionError: action.payload,
|
||||
};
|
||||
case CLEAR_SEND:
|
||||
return {
|
||||
...state,
|
||||
gasLimit: null,
|
||||
gasPrice: null,
|
||||
gasTotal: null,
|
||||
tokenBalance: null,
|
||||
from: '',
|
||||
to: '',
|
||||
amount: '0x0',
|
||||
memo: '',
|
||||
errors: {},
|
||||
maxModeOn: false,
|
||||
editingTransactionId: null,
|
||||
toNickname: '',
|
||||
};
|
||||
case GAS_LOADING_STARTED:
|
||||
return {
|
||||
...state,
|
||||
gasIsLoading: true,
|
||||
};
|
||||
|
||||
case GAS_LOADING_FINISHED:
|
||||
return {
|
||||
...state,
|
||||
gasIsLoading: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Action Creators
|
||||
export function openToDropdown() {
|
||||
return { type: OPEN_TO_DROPDOWN };
|
||||
}
|
||||
|
||||
export function closeToDropdown() {
|
||||
return { type: CLOSE_TO_DROPDOWN };
|
||||
}
|
||||
|
||||
export function showGasButtonGroup() {
|
||||
return { type: SHOW_GAS_BUTTON_GROUP };
|
||||
}
|
||||
|
||||
export function hideGasButtonGroup() {
|
||||
return { type: HIDE_GAS_BUTTON_GROUP };
|
||||
}
|
||||
|
||||
export function updateSendErrors(errorObject) {
|
||||
return {
|
||||
type: UPDATE_SEND_ERRORS,
|
||||
value: errorObject,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetSendState() {
|
||||
return { type: RESET_SEND_STATE };
|
||||
}
|
||||
|
||||
export function setGasLimit(gasLimit) {
|
||||
return {
|
||||
type: UPDATE_GAS_LIMIT,
|
||||
value: gasLimit,
|
||||
};
|
||||
}
|
||||
|
||||
export function setGasPrice(gasPrice) {
|
||||
return {
|
||||
type: UPDATE_GAS_PRICE,
|
||||
value: gasPrice,
|
||||
};
|
||||
}
|
||||
|
||||
export function setGasTotal(gasTotal) {
|
||||
return {
|
||||
type: UPDATE_GAS_TOTAL,
|
||||
value: gasTotal,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateGasData({
|
||||
gasPrice,
|
||||
blockGasLimit,
|
||||
selectedAddress,
|
||||
sendToken,
|
||||
to,
|
||||
value,
|
||||
data,
|
||||
}) {
|
||||
return (dispatch) => {
|
||||
dispatch(gasLoadingStarted());
|
||||
return estimateGasForSend({
|
||||
estimateGasMethod: estimateGas,
|
||||
blockGasLimit,
|
||||
selectedAddress,
|
||||
sendToken,
|
||||
to,
|
||||
value,
|
||||
estimateGasPrice: gasPrice,
|
||||
data,
|
||||
})
|
||||
.then((gas) => {
|
||||
dispatch(setGasLimit(gas));
|
||||
dispatch(setCustomGasLimit(gas));
|
||||
dispatch(updateSendErrors({ gasLoadingError: null }));
|
||||
dispatch(gasLoadingFinished());
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }));
|
||||
dispatch(gasLoadingFinished());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function gasLoadingStarted() {
|
||||
return {
|
||||
type: GAS_LOADING_STARTED,
|
||||
};
|
||||
}
|
||||
|
||||
export function gasLoadingFinished() {
|
||||
return {
|
||||
type: GAS_LOADING_FINISHED,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendTokenBalance({ sendToken, tokenContract, address }) {
|
||||
return (dispatch) => {
|
||||
const tokenBalancePromise = tokenContract
|
||||
? tokenContract.balanceOf(address)
|
||||
: Promise.resolve();
|
||||
return tokenBalancePromise
|
||||
.then((usersToken) => {
|
||||
if (usersToken) {
|
||||
const newTokenBalance = calcTokenBalance({ sendToken, usersToken });
|
||||
dispatch(setSendTokenBalance(newTokenBalance));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
updateSendErrors({ tokenBalance: 'tokenBalanceError' });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function setSendTokenBalance(tokenBalance) {
|
||||
return {
|
||||
type: UPDATE_SEND_TOKEN_BALANCE,
|
||||
value: tokenBalance,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendHexData(value) {
|
||||
return {
|
||||
type: UPDATE_SEND_HEX_DATA,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendTo(to, nickname = '') {
|
||||
return {
|
||||
type: UPDATE_SEND_TO,
|
||||
value: { to, nickname },
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendAmount(amount) {
|
||||
return {
|
||||
type: UPDATE_SEND_AMOUNT,
|
||||
value: amount,
|
||||
};
|
||||
}
|
||||
|
||||
export function setMaxModeTo(bool) {
|
||||
return {
|
||||
type: UPDATE_MAX_MODE,
|
||||
value: bool,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSend(newSend) {
|
||||
return {
|
||||
type: UPDATE_SEND,
|
||||
value: newSend,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendToken(token) {
|
||||
return {
|
||||
type: UPDATE_SEND_TOKEN,
|
||||
value: token,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearSend() {
|
||||
return {
|
||||
type: CLEAR_SEND,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendEnsResolution(ensResolution) {
|
||||
return {
|
||||
type: UPDATE_SEND_ENS_RESOLUTION,
|
||||
payload: ensResolution,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSendEnsResolutionError(errorMessage) {
|
||||
return {
|
||||
type: UPDATE_SEND_ENS_RESOLUTION_ERROR,
|
||||
payload: errorMessage,
|
||||
};
|
||||
}
|
1472
ui/ducks/send/send.js
Normal file
1472
ui/ducks/send/send.js
Normal file
File diff suppressed because it is too large
Load Diff
1808
ui/ducks/send/send.test.js
Normal file
1808
ui/ducks/send/send.test.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { updateSend } from '../../ducks/send/send.duck';
|
||||
import { ASSET_TYPES, editTransaction } from '../../ducks/send';
|
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import ConfirmSendEther from './confirm-send-ether.component';
|
||||
|
||||
@ -18,22 +18,8 @@ const mapStateToProps = (state) => {
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
editTransaction: (txData) => {
|
||||
const { id, txParams } = txData;
|
||||
const { from, gas: gasLimit, gasPrice, to, value: amount } = txParams;
|
||||
|
||||
dispatch(
|
||||
updateSend({
|
||||
from,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
gasTotal: null,
|
||||
to,
|
||||
amount,
|
||||
errors: { to: null, amount: null },
|
||||
editingTransactionId: id?.toString(),
|
||||
}),
|
||||
);
|
||||
|
||||
const { id } = txData;
|
||||
dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString()));
|
||||
dispatch(clearConfirmTransaction());
|
||||
},
|
||||
};
|
||||
|
@ -3,13 +3,8 @@ import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import { showSendTokenPage } from '../../store/actions';
|
||||
import { conversionUtil } from '../../helpers/utils/conversion-util';
|
||||
import {
|
||||
getTokenValueParam,
|
||||
getTokenAddressParam,
|
||||
} from '../../helpers/utils/token-util';
|
||||
import { ASSET_TYPES, editTransaction } from '../../ducks/send';
|
||||
import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors';
|
||||
import { updateSend } from '../../ducks/send/send.duck';
|
||||
import ConfirmSendToken from './confirm-send-token.component';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@ -22,35 +17,15 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
editTransaction: ({ txData, tokenData, tokenProps }) => {
|
||||
const {
|
||||
id,
|
||||
txParams: { from, to: tokenAddress, gas: gasLimit, gasPrice } = {},
|
||||
} = txData;
|
||||
|
||||
const to = getTokenValueParam(tokenData);
|
||||
const tokenAmountInDec = getTokenAddressParam(tokenData);
|
||||
|
||||
const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'hex',
|
||||
});
|
||||
|
||||
editTransaction: ({ txData, tokenData, tokenProps: assetDetails }) => {
|
||||
const { id } = txData;
|
||||
dispatch(
|
||||
updateSend({
|
||||
from,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
gasTotal: null,
|
||||
to,
|
||||
amount: tokenAmountInHex,
|
||||
errors: { to: null, amount: null },
|
||||
editingTransactionId: id?.toString(),
|
||||
token: {
|
||||
...tokenProps,
|
||||
address: tokenAddress,
|
||||
},
|
||||
}),
|
||||
editTransaction(
|
||||
ASSET_TYPES.TOKEN,
|
||||
id.toString(),
|
||||
tokenData,
|
||||
assetDetails,
|
||||
),
|
||||
);
|
||||
dispatch(clearConfirmTransaction());
|
||||
dispatch(showSendTokenPage());
|
||||
|
@ -12,6 +12,7 @@ import Loading from '../../components/ui/loading-screen';
|
||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
||||
import { getSendTo } from '../../ducks/send';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { metamask, appState } = state;
|
||||
@ -38,7 +39,7 @@ function mapStateToProps(state) {
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
unapprovedTypedMessagesCount,
|
||||
send: state.send,
|
||||
sendTo: getSendTo(state),
|
||||
currentNetworkTxList: state.metamask.currentNetworkTxList,
|
||||
};
|
||||
}
|
||||
@ -68,9 +69,7 @@ class ConfirmTxScreen extends Component {
|
||||
history: PropTypes.object,
|
||||
identities: PropTypes.object,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
send: PropTypes.shape({
|
||||
to: PropTypes.string,
|
||||
}).isRequired,
|
||||
sendTo: PropTypes.string,
|
||||
};
|
||||
|
||||
getUnapprovedMessagesTotal() {
|
||||
@ -182,13 +181,13 @@ class ConfirmTxScreen extends Component {
|
||||
mostRecentOverviewPage,
|
||||
network,
|
||||
chainId,
|
||||
send,
|
||||
sendTo,
|
||||
} = this.props;
|
||||
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network, chainId);
|
||||
|
||||
if (
|
||||
unconfTxList.length === 0 &&
|
||||
!send.to &&
|
||||
!sendTo &&
|
||||
this.getUnapprovedMessagesTotal() === 0
|
||||
) {
|
||||
history.push(mostRecentOverviewPage);
|
||||
@ -201,7 +200,7 @@ class ConfirmTxScreen extends Component {
|
||||
network,
|
||||
chainId,
|
||||
currentNetworkTxList,
|
||||
send,
|
||||
sendTo,
|
||||
history,
|
||||
match: { params: { id: transactionId } = {} },
|
||||
mostRecentOverviewPage,
|
||||
@ -241,7 +240,7 @@ class ConfirmTxScreen extends Component {
|
||||
|
||||
if (
|
||||
unconfTxList.length === 0 &&
|
||||
!send.to &&
|
||||
!sendTo &&
|
||||
this.getUnapprovedMessagesTotal() === 0
|
||||
) {
|
||||
this.props.history.push(mostRecentOverviewPage);
|
||||
|
@ -35,7 +35,7 @@ export default class ConfirmTransaction extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
totalUnapprovedCount: PropTypes.number.isRequired,
|
||||
send: PropTypes.object,
|
||||
sendTo: PropTypes.string,
|
||||
setTransactionToConfirm: PropTypes.func,
|
||||
clearConfirmTransaction: PropTypes.func,
|
||||
fetchBasicGasEstimates: PropTypes.func,
|
||||
@ -52,7 +52,7 @@ export default class ConfirmTransaction extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
totalUnapprovedCount = 0,
|
||||
send = {},
|
||||
sendTo,
|
||||
history,
|
||||
mostRecentOverviewPage,
|
||||
transaction: { txParams: { data, to } = {} } = {},
|
||||
@ -64,7 +64,7 @@ export default class ConfirmTransaction extends Component {
|
||||
isTokenMethodAction,
|
||||
} = this.props;
|
||||
|
||||
if (!totalUnapprovedCount && !send.to) {
|
||||
if (!totalUnapprovedCount && !sendTo) {
|
||||
history.replace(mostRecentOverviewPage);
|
||||
return;
|
||||
}
|
||||
|
@ -15,17 +15,18 @@ import {
|
||||
} from '../../store/actions';
|
||||
import { unconfirmedTransactionsListSelector } from '../../selectors';
|
||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||
import { getSendTo } from '../../ducks/send';
|
||||
import ConfirmTransaction from './confirm-transaction.component';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
metamask: { unapprovedTxs },
|
||||
send,
|
||||
} = state;
|
||||
const {
|
||||
match: { params = {} },
|
||||
} = ownProps;
|
||||
const { id } = params;
|
||||
const sendTo = getSendTo(state);
|
||||
|
||||
const unconfirmedTransactions = unconfirmedTransactionsListSelector(state);
|
||||
const totalUnconfirmed = unconfirmedTransactions.length;
|
||||
@ -36,7 +37,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
return {
|
||||
totalUnapprovedCount: totalUnconfirmed,
|
||||
send,
|
||||
sendTo,
|
||||
unapprovedTxs,
|
||||
id,
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './send.container';
|
||||
export { default } from './send';
|
||||
|
@ -8,26 +8,28 @@ import RecipientGroup from '../../../../components/app/contact-list/recipient-gr
|
||||
import { ellipsify } from '../../send.utils';
|
||||
import Button from '../../../../components/ui/button';
|
||||
import Confusable from '../../../../components/ui/confusable';
|
||||
import {
|
||||
isBurnAddress,
|
||||
isValidHexAddress,
|
||||
} from '../../../../../shared/modules/hexstring-utils';
|
||||
|
||||
export default class AddRecipient extends Component {
|
||||
static propTypes = {
|
||||
query: PropTypes.string,
|
||||
userInput: PropTypes.string,
|
||||
ownedAccounts: PropTypes.array,
|
||||
addressBook: PropTypes.array,
|
||||
updateGas: PropTypes.func,
|
||||
updateSendTo: PropTypes.func,
|
||||
updateRecipient: PropTypes.func,
|
||||
ensResolution: PropTypes.string,
|
||||
toError: PropTypes.string,
|
||||
toWarning: PropTypes.string,
|
||||
ensResolutionError: PropTypes.string,
|
||||
ensError: PropTypes.string,
|
||||
ensWarning: PropTypes.string,
|
||||
addressBookEntryName: PropTypes.string,
|
||||
contacts: PropTypes.array,
|
||||
nonContacts: PropTypes.array,
|
||||
setInternalSearch: PropTypes.func,
|
||||
useMyAccountsForRecipientSearch: PropTypes.func,
|
||||
useContactListForRecipientSearch: PropTypes.func,
|
||||
isUsingMyAccountsForRecipientSearch: PropTypes.bool,
|
||||
recipient: PropTypes.shape({
|
||||
address: PropTypes.string,
|
||||
nickname: PropTypes.nickname,
|
||||
error: PropTypes.string,
|
||||
warning: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@ -61,60 +63,58 @@ export default class AddRecipient extends Component {
|
||||
metricsEvent: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
isShowingTransfer: false,
|
||||
};
|
||||
|
||||
selectRecipient = (to, nickname = '') => {
|
||||
const { updateSendTo, updateGas } = this.props;
|
||||
|
||||
updateSendTo(to, nickname);
|
||||
updateGas({ to });
|
||||
selectRecipient = (address, nickname = '') => {
|
||||
this.props.updateRecipient({ address, nickname });
|
||||
};
|
||||
|
||||
searchForContacts = () => {
|
||||
const { query, contacts } = this.props;
|
||||
const { userInput, contacts } = this.props;
|
||||
|
||||
let _contacts = contacts;
|
||||
|
||||
if (query) {
|
||||
if (userInput) {
|
||||
this.contactFuse.setCollection(contacts);
|
||||
_contacts = this.contactFuse.search(query);
|
||||
_contacts = this.contactFuse.search(userInput);
|
||||
}
|
||||
|
||||
return _contacts;
|
||||
};
|
||||
|
||||
searchForRecents = () => {
|
||||
const { query, nonContacts } = this.props;
|
||||
const { userInput, nonContacts } = this.props;
|
||||
|
||||
let _nonContacts = nonContacts;
|
||||
|
||||
if (query) {
|
||||
if (userInput) {
|
||||
this.recentFuse.setCollection(nonContacts);
|
||||
_nonContacts = this.recentFuse.search(query);
|
||||
_nonContacts = this.recentFuse.search(userInput);
|
||||
}
|
||||
|
||||
return _nonContacts;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { ensResolution, query, addressBookEntryName } = this.props;
|
||||
const { isShowingTransfer } = this.state;
|
||||
const {
|
||||
ensResolution,
|
||||
recipient,
|
||||
userInput,
|
||||
addressBookEntryName,
|
||||
isUsingMyAccountsForRecipientSearch,
|
||||
} = this.props;
|
||||
|
||||
let content;
|
||||
|
||||
if (
|
||||
!isBurnAddress(query) &&
|
||||
isValidHexAddress(query, { mixedCaseUseChecksum: true })
|
||||
) {
|
||||
content = this.renderExplicitAddress(query);
|
||||
if (recipient.address) {
|
||||
content = this.renderExplicitAddress(
|
||||
recipient.address,
|
||||
recipient.nickname,
|
||||
);
|
||||
} else if (ensResolution) {
|
||||
content = this.renderExplicitAddress(
|
||||
ensResolution,
|
||||
addressBookEntryName || query,
|
||||
addressBookEntryName || userInput,
|
||||
);
|
||||
} else if (isShowingTransfer) {
|
||||
} else if (isUsingMyAccountsForRecipientSearch) {
|
||||
content = this.renderTransfer();
|
||||
}
|
||||
|
||||
@ -150,15 +150,18 @@ export default class AddRecipient extends Component {
|
||||
|
||||
renderTransfer() {
|
||||
let { ownedAccounts } = this.props;
|
||||
const { query, setInternalSearch } = this.props;
|
||||
const {
|
||||
userInput,
|
||||
useContactListForRecipientSearch,
|
||||
isUsingMyAccountsForRecipientSearch,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
const { isShowingTransfer } = this.state;
|
||||
|
||||
if (isShowingTransfer && query) {
|
||||
if (isUsingMyAccountsForRecipientSearch && userInput) {
|
||||
ownedAccounts = ownedAccounts.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 ||
|
||||
item.address.toLowerCase().indexOf(query.toLowerCase()) > -1,
|
||||
item.name.toLowerCase().indexOf(userInput.toLowerCase()) > -1 ||
|
||||
item.address.toLowerCase().indexOf(userInput.toLowerCase()) > -1,
|
||||
);
|
||||
}
|
||||
|
||||
@ -167,10 +170,7 @@ export default class AddRecipient extends Component {
|
||||
<Button
|
||||
type="link"
|
||||
className="send__select-recipient-wrapper__list__link"
|
||||
onClick={() => {
|
||||
setInternalSearch(false);
|
||||
this.setState({ isShowingTransfer: false });
|
||||
}}
|
||||
onClick={useContactListForRecipientSearch}
|
||||
>
|
||||
<div className="send__select-recipient-wrapper__list__back-caret" />
|
||||
{t('backToAll')}
|
||||
@ -187,10 +187,10 @@ export default class AddRecipient extends Component {
|
||||
renderMain() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
query,
|
||||
userInput,
|
||||
ownedAccounts = [],
|
||||
addressBook,
|
||||
setInternalSearch,
|
||||
useMyAccountsForRecipientSearch,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -201,14 +201,11 @@ export default class AddRecipient extends Component {
|
||||
searchForRecents={this.searchForRecents.bind(this)}
|
||||
selectRecipient={this.selectRecipient.bind(this)}
|
||||
>
|
||||
{ownedAccounts && ownedAccounts.length > 1 && !query && (
|
||||
{ownedAccounts && ownedAccounts.length > 1 && !userInput && (
|
||||
<Button
|
||||
type="link"
|
||||
className="send__select-recipient-wrapper__list__link"
|
||||
onClick={() => {
|
||||
setInternalSearch(true);
|
||||
this.setState({ isShowingTransfer: true });
|
||||
}}
|
||||
onClick={useMyAccountsForRecipientSearch}
|
||||
>
|
||||
{t('transferBetweenAccounts')}
|
||||
</Button>
|
||||
@ -219,30 +216,19 @@ export default class AddRecipient extends Component {
|
||||
}
|
||||
|
||||
renderDialogs() {
|
||||
const {
|
||||
toError,
|
||||
toWarning,
|
||||
ensResolutionError,
|
||||
ensResolution,
|
||||
} = this.props;
|
||||
const { ensError, recipient, ensWarning } = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
if (ensResolutionError) {
|
||||
if (ensError || (recipient.error && recipient.error !== 'required')) {
|
||||
return (
|
||||
<Dialog type="error" className="send__error-dialog">
|
||||
{ensResolutionError}
|
||||
{t(ensError ?? recipient.error)}
|
||||
</Dialog>
|
||||
);
|
||||
} else if (toError && toError !== 'required' && !ensResolution) {
|
||||
return (
|
||||
<Dialog type="error" className="send__error-dialog">
|
||||
{t(toError)}
|
||||
</Dialog>
|
||||
);
|
||||
} else if (toWarning) {
|
||||
} else if (ensWarning || recipient.warning) {
|
||||
return (
|
||||
<Dialog type="warning" className="send__error-dialog">
|
||||
{t(toWarning)}
|
||||
{t(ensWarning ?? recipient.warning)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -5,30 +5,24 @@ import Dialog from '../../../../components/ui/dialog';
|
||||
import AddRecipient from './add-recipient.component';
|
||||
|
||||
const propsMethodSpies = {
|
||||
closeToDropdown: sinon.spy(),
|
||||
openToDropdown: sinon.spy(),
|
||||
updateGas: sinon.spy(),
|
||||
updateSendTo: sinon.spy(),
|
||||
updateSendToError: sinon.spy(),
|
||||
updateSendToWarning: sinon.spy(),
|
||||
updateRecipient: sinon.spy(),
|
||||
useMyAccountsForRecipientSearch: sinon.spy(),
|
||||
useContactListForRecipientSearch: sinon.spy(),
|
||||
};
|
||||
|
||||
describe('AddRecipient Component', () => {
|
||||
let wrapper;
|
||||
let instance;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<AddRecipient
|
||||
closeToDropdown={propsMethodSpies.closeToDropdown}
|
||||
inError={false}
|
||||
inWarning={false}
|
||||
network="mockNetwork"
|
||||
openToDropdown={propsMethodSpies.openToDropdown}
|
||||
to="mockTo"
|
||||
toAccounts={['mockAccount']}
|
||||
toDropdownOpen={false}
|
||||
updateGas={propsMethodSpies.updateGas}
|
||||
userInput=""
|
||||
recipient={{
|
||||
address: '',
|
||||
nickname: '',
|
||||
error: '',
|
||||
warning: '',
|
||||
}}
|
||||
updateSendTo={propsMethodSpies.updateSendTo}
|
||||
updateSendToError={propsMethodSpies.updateSendToError}
|
||||
updateSendToWarning={propsMethodSpies.updateSendToWarning}
|
||||
@ -53,34 +47,12 @@ describe('AddRecipient Component', () => {
|
||||
/>,
|
||||
{ context: { t: (str) => `${str}_t` } },
|
||||
);
|
||||
instance = wrapper.instance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.closeToDropdown.resetHistory();
|
||||
propsMethodSpies.openToDropdown.resetHistory();
|
||||
propsMethodSpies.updateSendTo.resetHistory();
|
||||
propsMethodSpies.updateSendToError.resetHistory();
|
||||
propsMethodSpies.updateSendToWarning.resetHistory();
|
||||
propsMethodSpies.updateGas.resetHistory();
|
||||
});
|
||||
|
||||
describe('selectRecipient', () => {
|
||||
it('should call updateSendTo', () => {
|
||||
expect(propsMethodSpies.updateSendTo.callCount).toStrictEqual(0);
|
||||
instance.selectRecipient('mockTo2', 'mockNickname');
|
||||
expect(propsMethodSpies.updateSendTo.callCount).toStrictEqual(1);
|
||||
expect(propsMethodSpies.updateSendTo.getCall(0).args).toStrictEqual([
|
||||
'mockTo2',
|
||||
'mockNickname',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call updateGas if there is no to error', () => {
|
||||
expect(propsMethodSpies.updateGas.callCount).toStrictEqual(0);
|
||||
instance.selectRecipient(false);
|
||||
expect(propsMethodSpies.updateGas.callCount).toStrictEqual(1);
|
||||
});
|
||||
propsMethodSpies.updateRecipient.resetHistory();
|
||||
propsMethodSpies.useMyAccountsForRecipientSearch.resetHistory();
|
||||
propsMethodSpies.useContactListForRecipientSearch.resetHistory();
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
@ -104,6 +76,7 @@ describe('AddRecipient Component', () => {
|
||||
|
||||
it('should render transfer', () => {
|
||||
wrapper.setProps({
|
||||
isUsingMyAccountsForRecipientSearch: true,
|
||||
ownedAccounts: [
|
||||
{ address: '0x123', name: '123' },
|
||||
{ address: '0x124', name: '124' },
|
||||
@ -163,7 +136,7 @@ describe('AddRecipient Component', () => {
|
||||
it('should render error when query has no results', () => {
|
||||
wrapper.setProps({
|
||||
addressBook: [],
|
||||
toError: 'bad',
|
||||
ensError: 'bad',
|
||||
contacts: [],
|
||||
nonContacts: [],
|
||||
});
|
||||
@ -178,8 +151,7 @@ describe('AddRecipient Component', () => {
|
||||
it('should render error when query has ens does not resolve', () => {
|
||||
wrapper.setProps({
|
||||
addressBook: [],
|
||||
toError: 'bad',
|
||||
ensResolutionError: 'very bad',
|
||||
ensError: 'very bad',
|
||||
contacts: [],
|
||||
nonContacts: [],
|
||||
});
|
||||
@ -187,20 +159,20 @@ describe('AddRecipient Component', () => {
|
||||
const dialog = wrapper.find(Dialog);
|
||||
|
||||
expect(dialog.props().type).toStrictEqual('error');
|
||||
expect(dialog.props().children).toStrictEqual('very bad');
|
||||
expect(dialog.props().children).toStrictEqual('very bad_t');
|
||||
expect(dialog).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not render error when ens resolved', () => {
|
||||
it('should render error when ens resolved but ens error exists', () => {
|
||||
wrapper.setProps({
|
||||
addressBook: [],
|
||||
toError: 'bad',
|
||||
ensError: 'bad',
|
||||
ensResolution: '0x128',
|
||||
});
|
||||
|
||||
const dialog = wrapper.find(Dialog);
|
||||
|
||||
expect(dialog).toHaveLength(0);
|
||||
expect(dialog).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,19 +1,30 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getSendEnsResolution,
|
||||
getSendEnsResolutionError,
|
||||
accountsWithSendEtherInfoSelector,
|
||||
getAddressBook,
|
||||
getAddressBookEntry,
|
||||
} from '../../../../selectors';
|
||||
|
||||
import { updateSendTo } from '../../../../ducks/send/send.duck';
|
||||
import {
|
||||
updateRecipient,
|
||||
updateRecipientUserInput,
|
||||
useMyAccountsForRecipientSearch,
|
||||
useContactListForRecipientSearch,
|
||||
getIsUsingMyAccountForRecipientSearch,
|
||||
getRecipientUserInput,
|
||||
getRecipient,
|
||||
} from '../../../../ducks/send';
|
||||
import {
|
||||
getEnsResolution,
|
||||
getEnsError,
|
||||
getEnsWarning,
|
||||
} from '../../../../ducks/ens';
|
||||
import AddRecipient from './add-recipient.component';
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddRecipient);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const ensResolution = getSendEnsResolution(state);
|
||||
const ensResolution = getEnsResolution(state);
|
||||
|
||||
let addressBookEntryName = '';
|
||||
if (ensResolution) {
|
||||
@ -32,14 +43,27 @@ function mapStateToProps(state) {
|
||||
addressBookEntryName,
|
||||
contacts: addressBook.filter(({ name }) => Boolean(name)),
|
||||
ensResolution,
|
||||
ensResolutionError: getSendEnsResolutionError(state),
|
||||
ensError: getEnsError(state),
|
||||
ensWarning: getEnsWarning(state),
|
||||
nonContacts: addressBook.filter(({ name }) => !name),
|
||||
ownedAccounts,
|
||||
isUsingMyAccountsForRecipientSearch: getIsUsingMyAccountForRecipientSearch(
|
||||
state,
|
||||
),
|
||||
userInput: getRecipientUserInput(state),
|
||||
recipient: getRecipient(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
|
||||
updateRecipient: ({ address, nickname }) =>
|
||||
dispatch(updateRecipient({ address, nickname })),
|
||||
updateRecipientUserInput: (newInput) =>
|
||||
dispatch(updateRecipientUserInput(newInput)),
|
||||
useMyAccountsForRecipientSearch: () =>
|
||||
dispatch(useMyAccountsForRecipientSearch()),
|
||||
useContactListForRecipientSearch: () =>
|
||||
dispatch(useContactListForRecipientSearch()),
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
import sinon from 'sinon';
|
||||
import { updateSendTo } from '../../../../ducks/send/send.duck';
|
||||
|
||||
let mapStateToProps;
|
||||
let mapDispatchToProps;
|
||||
|
||||
@ -13,8 +10,6 @@ jest.mock('react-redux', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('../../../../selectors', () => ({
|
||||
getSendEnsResolution: (s) => `mockSendEnsResolution:${s}`,
|
||||
getSendEnsResolutionError: (s) => `mockSendEnsResolutionError:${s}`,
|
||||
getAddressBook: (s) => [{ name: `mockAddressBook:${s}` }],
|
||||
getAddressBookEntry: (s) => `mockAddressBookEntry:${s}`,
|
||||
accountsWithSendEtherInfoSelector: () => [
|
||||
@ -23,8 +18,26 @@ jest.mock('../../../../selectors', () => ({
|
||||
],
|
||||
}));
|
||||
|
||||
jest.mock('../../../../ducks/send/send.duck.js', () => ({
|
||||
updateSendTo: jest.fn(),
|
||||
jest.mock('../../../../ducks/ens', () => ({
|
||||
getEnsResolution: (s) => `mockSendEnsResolution:${s}`,
|
||||
getEnsError: (s) => `mockSendEnsResolutionError:${s}`,
|
||||
getEnsWarning: (s) => `mockSendEnsResolutionWarning:${s}`,
|
||||
useMyAccountsForRecipientSearch: (s) =>
|
||||
`useMyAccountsForRecipientSearch:${s}`,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../ducks/send', () => ({
|
||||
updateRecipient: ({ address, nickname }) =>
|
||||
`{mockUpdateRecipient: {address: ${address}, nickname: ${nickname}}}`,
|
||||
updateRecipientUserInput: (s) => `mockUpdateRecipientUserInput:${s}`,
|
||||
useMyAccountsForRecipientSearch: (s) =>
|
||||
`mockUseMyAccountsForRecipientSearch:${s}`,
|
||||
useContactListForRecipientSearch: (s) =>
|
||||
`mockUseContactListForRecipientSearch:${s}`,
|
||||
getIsUsingMyAccountForRecipientSearch: (s) =>
|
||||
`mockGetIsUsingMyAccountForRecipientSearch:${s}`,
|
||||
getRecipientUserInput: (s) => `mockRecipientUserInput:${s}`,
|
||||
getRecipient: (s) => `mockRecipient:${s}`,
|
||||
}));
|
||||
|
||||
require('./add-recipient.container.js');
|
||||
@ -34,29 +47,40 @@ describe('add-recipient container', () => {
|
||||
it('should map the correct properties to props', () => {
|
||||
expect(mapStateToProps('mockState')).toStrictEqual({
|
||||
addressBook: [{ name: 'mockAddressBook:mockState' }],
|
||||
addressBookEntryName: undefined,
|
||||
contacts: [{ name: 'mockAddressBook:mockState' }],
|
||||
ensResolution: 'mockSendEnsResolution:mockState',
|
||||
ensResolutionError: 'mockSendEnsResolutionError:mockState',
|
||||
ownedAccounts: [
|
||||
{ name: `account1:mockState` },
|
||||
{ name: `account2:mockState` },
|
||||
],
|
||||
addressBookEntryName: undefined,
|
||||
ensError: 'mockSendEnsResolutionError:mockState',
|
||||
ensWarning: 'mockSendEnsResolutionWarning:mockState',
|
||||
nonContacts: [],
|
||||
ownedAccounts: [
|
||||
{ name: 'account1:mockState' },
|
||||
{ name: 'account2:mockState' },
|
||||
],
|
||||
isUsingMyAccountsForRecipientSearch:
|
||||
'mockGetIsUsingMyAccountForRecipientSearch:mockState',
|
||||
userInput: 'mockRecipientUserInput:mockState',
|
||||
recipient: 'mockRecipient:mockState',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
describe('updateSendTo()', () => {
|
||||
const dispatchSpy = sinon.spy();
|
||||
describe('updateRecipient()', () => {
|
||||
const dispatchSpy = jest.fn();
|
||||
|
||||
const mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy);
|
||||
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname');
|
||||
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||
expect(updateSendTo).toHaveBeenCalled();
|
||||
expect(updateSendTo).toHaveBeenCalledWith('mockTo', 'mockNickname');
|
||||
mapDispatchToPropsObject.updateRecipient({
|
||||
address: 'mockAddress',
|
||||
nickname: 'mockNickname',
|
||||
});
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchSpy.mock.calls[0][0]).toStrictEqual(
|
||||
'{mockUpdateRecipient: {address: mockAddress, nickname: mockNickname}}',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,56 +0,0 @@
|
||||
import contractMap from '@metamask/contract-metadata';
|
||||
import { isConfusing } from 'unicode-confusables';
|
||||
import {
|
||||
REQUIRED_ERROR,
|
||||
INVALID_RECIPIENT_ADDRESS_ERROR,
|
||||
KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
|
||||
CONFUSING_ENS_ERROR,
|
||||
CONTRACT_ADDRESS_ERROR,
|
||||
} from '../../send.constants';
|
||||
|
||||
import {
|
||||
checkExistingAddresses,
|
||||
isValidDomainName,
|
||||
isOriginContractAddress,
|
||||
isDefaultMetaMaskChain,
|
||||
} from '../../../../helpers/utils/util';
|
||||
import {
|
||||
isBurnAddress,
|
||||
isValidHexAddress,
|
||||
toChecksumHexAddress,
|
||||
} from '../../../../../shared/modules/hexstring-utils';
|
||||
|
||||
export function getToErrorObject(to, sendTokenAddress, chainId) {
|
||||
let toError = null;
|
||||
if (!to) {
|
||||
toError = REQUIRED_ERROR;
|
||||
} else if (
|
||||
isBurnAddress(to) ||
|
||||
(!isValidHexAddress(to, { mixedCaseUseChecksum: true }) &&
|
||||
!isValidDomainName(to))
|
||||
) {
|
||||
toError = isDefaultMetaMaskChain(chainId)
|
||||
? INVALID_RECIPIENT_ADDRESS_ERROR
|
||||
: INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR;
|
||||
} else if (isOriginContractAddress(to, sendTokenAddress)) {
|
||||
toError = CONTRACT_ADDRESS_ERROR;
|
||||
}
|
||||
|
||||
return { to: toError };
|
||||
}
|
||||
|
||||
export function getToWarningObject(to, tokens = [], sendToken = null) {
|
||||
let toWarning = null;
|
||||
if (
|
||||
sendToken &&
|
||||
(toChecksumHexAddress(to) in contractMap ||
|
||||
checkExistingAddresses(to, tokens))
|
||||
) {
|
||||
toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR;
|
||||
} else if (isValidDomainName(to) && isConfusing(to)) {
|
||||
toWarning = CONFUSING_ENS_ERROR;
|
||||
}
|
||||
|
||||
return { to: toWarning };
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
import {
|
||||
REQUIRED_ERROR,
|
||||
INVALID_RECIPIENT_ADDRESS_ERROR,
|
||||
KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
CONFUSING_ENS_ERROR,
|
||||
CONTRACT_ADDRESS_ERROR,
|
||||
} from '../../send.constants';
|
||||
import { getToErrorObject, getToWarningObject } from './add-recipient';
|
||||
|
||||
jest.mock('../../../../helpers/utils/util', () => ({
|
||||
isDefaultMetaMaskChain: jest.fn().mockReturnValue(true),
|
||||
isEthNetwork: jest.fn().mockReturnValue(true),
|
||||
checkExistingAddresses: jest.fn().mockReturnValue(true),
|
||||
isValidDomainName: jest.requireActual('../../../../helpers/utils/util')
|
||||
.isValidDomainName,
|
||||
isOriginContractAddress: jest.requireActual('../../../../helpers/utils/util')
|
||||
.isOriginContractAddress,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../shared/modules/hexstring-utils', () => ({
|
||||
isValidHexAddress: jest.fn((to) =>
|
||||
Boolean(to.match(/^[0xabcdef123456798]+$/u)),
|
||||
),
|
||||
isBurnAddress: jest.fn(() => false),
|
||||
toChecksumHexAddress: jest.fn((input) => input),
|
||||
}));
|
||||
|
||||
describe('add-recipient utils', () => {
|
||||
describe('getToErrorObject()', () => {
|
||||
it('should return a required error if "to" is falsy', () => {
|
||||
expect(getToErrorObject(null)).toStrictEqual({
|
||||
to: REQUIRED_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an invalid recipient error if "to" is truthy but invalid', () => {
|
||||
expect(getToErrorObject('mockInvalidTo')).toStrictEqual({
|
||||
to: INVALID_RECIPIENT_ADDRESS_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if "to" is truthy and valid', () => {
|
||||
expect(getToErrorObject('0xabc123')).toStrictEqual({
|
||||
to: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a contract address error if the recipient is the same as the tokens contract address', () => {
|
||||
expect(getToErrorObject('0xabc123', '0xabc123')).toStrictEqual({
|
||||
to: CONTRACT_ADDRESS_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if the recipient address is not the token contract address', () => {
|
||||
expect(getToErrorObject('0xabc123', '0xabc456')).toStrictEqual({
|
||||
to: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToWarningObject()', () => {
|
||||
it('should return a known address recipient error if "to" is a token address', () => {
|
||||
expect(
|
||||
getToWarningObject('0xabc123', [{ address: '0xabc123' }], {
|
||||
address: '0xabc123',
|
||||
}),
|
||||
).toStrictEqual({
|
||||
to: KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should null if "to" is a token address but sendToken is falsy', () => {
|
||||
expect(
|
||||
getToWarningObject('0xabc123', [{ address: '0xabc123' }]),
|
||||
).toStrictEqual({
|
||||
to: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a known address recipient error if "to" is part of contract metadata', () => {
|
||||
expect(
|
||||
getToWarningObject(
|
||||
'0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
|
||||
[{ address: '0xabc123' }],
|
||||
{ address: '0xabc123' },
|
||||
),
|
||||
).toStrictEqual({
|
||||
to: KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
});
|
||||
});
|
||||
it('should null if "to" is part of contract metadata but sendToken is falsy', () => {
|
||||
expect(
|
||||
getToWarningObject(
|
||||
'0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
|
||||
[{ address: '0xabc123' }],
|
||||
{ address: '0xabc123' },
|
||||
),
|
||||
).toStrictEqual({
|
||||
to: KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn if name is a valid domain and confusable', () => {
|
||||
expect(getToWarningObject('demo.eth')).toStrictEqual({
|
||||
to: CONFUSING_ENS_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not warn if name is a valid domain and not confusable', () => {
|
||||
expect(getToWarningObject('vitalik.eth')).toStrictEqual({
|
||||
to: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -2,146 +2,39 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import copyToClipboard from 'copy-to-clipboard/index';
|
||||
import ENS from 'ethjs-ens';
|
||||
import networkMap from 'ethereum-ens-network-map';
|
||||
import log from 'loglevel';
|
||||
import { isHexString } from 'ethereumjs-util';
|
||||
import { ellipsify } from '../../send.utils';
|
||||
import { isValidDomainName } from '../../../../helpers/utils/util';
|
||||
import { MAINNET_NETWORK_ID } from '../../../../../shared/constants/network';
|
||||
import {
|
||||
isBurnAddress,
|
||||
isValidHexAddress,
|
||||
} from '../../../../../shared/modules/hexstring-utils';
|
||||
|
||||
// Local Constants
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
const ZERO_X_ERROR_ADDRESS = '0x';
|
||||
|
||||
export default class EnsInput extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
metricsEvent: PropTypes.func,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
network: PropTypes.string,
|
||||
selectedAddress: PropTypes.string,
|
||||
selectedName: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
updateEnsResolution: PropTypes.func,
|
||||
scanQrCode: PropTypes.func,
|
||||
updateEnsResolutionError: PropTypes.func,
|
||||
onPaste: PropTypes.func,
|
||||
onReset: PropTypes.func,
|
||||
onValidAddressTyped: PropTypes.func,
|
||||
contact: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
internalSearch: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
input: '',
|
||||
toError: null,
|
||||
ensResolution: undefined,
|
||||
userInput: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
lookupEnsName: PropTypes.func.isRequired,
|
||||
initializeEnsSlice: PropTypes.func.isRequired,
|
||||
resetEnsResolution: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { network, internalSearch } = this.props;
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network);
|
||||
this.setState({ ensResolution: ZERO_ADDRESS });
|
||||
|
||||
if (networkHasEnsSupport && !internalSearch) {
|
||||
const provider = global.ethereumProvider;
|
||||
this.ens = new ENS({ provider, network });
|
||||
this.checkName = debounce(this.lookupEnsName, 200);
|
||||
}
|
||||
this.props.initializeEnsSlice();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { input } = this.state;
|
||||
const { network, value, internalSearch } = this.props;
|
||||
|
||||
let newValue;
|
||||
|
||||
// Set the value of our input based on QR code provided by parent
|
||||
const newProvidedValue = input !== value && prevProps.value !== value;
|
||||
if (newProvidedValue) {
|
||||
newValue = value;
|
||||
}
|
||||
|
||||
if (prevProps.network !== network) {
|
||||
if (getNetworkEnsSupport(network)) {
|
||||
const provider = global.ethereumProvider;
|
||||
this.ens = new ENS({ provider, network });
|
||||
this.checkName = debounce(this.lookupEnsName, 200);
|
||||
if (!newProvidedValue) {
|
||||
newValue = input;
|
||||
}
|
||||
} else {
|
||||
// ens is null on mount on a network that does not have ens support
|
||||
// this is intended to prevent accidental lookup of domains across
|
||||
// networks
|
||||
this.ens = null;
|
||||
this.checkName = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue !== undefined) {
|
||||
this.onChange({ target: { value: newValue } });
|
||||
}
|
||||
if (!internalSearch && prevProps.internalSearch) {
|
||||
this.resetInput();
|
||||
}
|
||||
}
|
||||
|
||||
resetInput = () => {
|
||||
const {
|
||||
updateEnsResolution,
|
||||
updateEnsResolutionError,
|
||||
onReset,
|
||||
} = this.props;
|
||||
this.onChange({ target: { value: '' } });
|
||||
onReset();
|
||||
updateEnsResolution('');
|
||||
updateEnsResolutionError('');
|
||||
};
|
||||
|
||||
lookupEnsName = (ensName) => {
|
||||
const { network } = this.props;
|
||||
const recipient = ensName.trim();
|
||||
|
||||
log.info(`ENS attempting to resolve name: ${recipient}`);
|
||||
this.ens
|
||||
.lookup(recipient)
|
||||
.then((address) => {
|
||||
if (address === ZERO_ADDRESS) {
|
||||
throw new Error(this.context.t('noAddressForName'));
|
||||
}
|
||||
if (address === ZERO_X_ERROR_ADDRESS) {
|
||||
throw new Error(this.context.t('ensRegistrationError'));
|
||||
}
|
||||
this.props.updateEnsResolution(address);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (
|
||||
isValidDomainName(recipient) &&
|
||||
reason.message === 'ENS name not defined.'
|
||||
) {
|
||||
this.props.updateEnsResolutionError(
|
||||
network === MAINNET_NETWORK_ID
|
||||
? this.context.t('noAddressForName')
|
||||
: this.context.t('ensNotFoundOnCurrentNetwork'),
|
||||
);
|
||||
} else {
|
||||
log.error(reason);
|
||||
this.props.updateEnsResolutionError(reason.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onPaste = (event) => {
|
||||
event.clipboardData.items[0].getAsString((text) => {
|
||||
if (
|
||||
@ -155,40 +48,23 @@ export default class EnsInput extends Component {
|
||||
|
||||
onChange = (e) => {
|
||||
const {
|
||||
network,
|
||||
onChange,
|
||||
updateEnsResolution,
|
||||
updateEnsResolutionError,
|
||||
onValidAddressTyped,
|
||||
internalSearch,
|
||||
onChange,
|
||||
lookupEnsName,
|
||||
resetEnsResolution,
|
||||
} = this.props;
|
||||
const input = e.target.value;
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network);
|
||||
|
||||
this.setState({ input }, () => onChange(input));
|
||||
onChange(input);
|
||||
if (internalSearch) {
|
||||
return null;
|
||||
}
|
||||
// Empty ENS state if input is empty
|
||||
// maybe scan ENS
|
||||
|
||||
if (
|
||||
!networkHasEnsSupport &&
|
||||
!(
|
||||
isBurnAddress(input) === false &&
|
||||
isValidHexAddress(input, { mixedCaseUseChecksum: true })
|
||||
) &&
|
||||
!isHexString(input)
|
||||
) {
|
||||
updateEnsResolution('');
|
||||
updateEnsResolutionError(
|
||||
networkHasEnsSupport ? '' : 'Network does not support ENS',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isValidDomainName(input)) {
|
||||
this.lookupEnsName(input);
|
||||
lookupEnsName(input);
|
||||
} else if (
|
||||
onValidAddressTyped &&
|
||||
!isBurnAddress(input) &&
|
||||
@ -196,20 +72,16 @@ export default class EnsInput extends Component {
|
||||
) {
|
||||
onValidAddressTyped(input);
|
||||
} else {
|
||||
updateEnsResolution('');
|
||||
updateEnsResolutionError('');
|
||||
resetEnsResolution();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.context;
|
||||
const { className, selectedAddress } = this.props;
|
||||
const { input } = this.state;
|
||||
const { className, selectedAddress, selectedName, userInput } = this.props;
|
||||
|
||||
if (selectedAddress) {
|
||||
return this.renderSelected();
|
||||
}
|
||||
const hasSelectedAddress = Boolean(selectedAddress);
|
||||
|
||||
return (
|
||||
<div className={classnames('ens-input', className)}>
|
||||
@ -217,135 +89,61 @@ export default class EnsInput extends Component {
|
||||
className={classnames('ens-input__wrapper', {
|
||||
'ens-input__wrapper__status-icon--error': false,
|
||||
'ens-input__wrapper__status-icon--valid': false,
|
||||
'ens-input__wrapper--valid': hasSelectedAddress,
|
||||
})}
|
||||
>
|
||||
<div className="ens-input__wrapper__status-icon" />
|
||||
<input
|
||||
className="ens-input__wrapper__input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={t('recipientAddressPlaceholder')}
|
||||
onChange={this.onChange}
|
||||
onPaste={this.onPaste}
|
||||
value={selectedAddress || input}
|
||||
autoFocus
|
||||
data-testid="ens-input"
|
||||
/>
|
||||
<button
|
||||
className={classnames('ens-input__wrapper__action-icon', {
|
||||
'ens-input__wrapper__action-icon--erase': input,
|
||||
'ens-input__wrapper__action-icon--qrcode': !input,
|
||||
<div
|
||||
className={classnames('ens-input__wrapper__status-icon', {
|
||||
'ens-input__wrapper__status-icon--valid': hasSelectedAddress,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (input) {
|
||||
this.resetInput();
|
||||
} else {
|
||||
this.props.scanQrCode();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSelected() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
className,
|
||||
selectedAddress,
|
||||
selectedName,
|
||||
contact = {},
|
||||
} = this.props;
|
||||
const name = contact.name || selectedName;
|
||||
|
||||
return (
|
||||
<div className={classnames('ens-input', className)}>
|
||||
<div className="ens-input__wrapper ens-input__wrapper--valid">
|
||||
<div className="ens-input__wrapper__status-icon ens-input__wrapper__status-icon--valid" />
|
||||
<div
|
||||
className="ens-input__wrapper__input ens-input__wrapper__input--selected"
|
||||
placeholder={t('recipientAddress')}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<div className="ens-input__selected-input__title">
|
||||
{name || ellipsify(selectedAddress)}
|
||||
</div>
|
||||
{name && (
|
||||
<div className="ens-input__selected-input__subtitle">
|
||||
{selectedAddress}
|
||||
{hasSelectedAddress ? (
|
||||
<>
|
||||
<div className="ens-input__wrapper__input ens-input__wrapper__input--selected">
|
||||
<div className="ens-input__selected-input__title">
|
||||
{selectedName || ellipsify(selectedAddress)}
|
||||
</div>
|
||||
{selectedName && (
|
||||
<div className="ens-input__selected-input__subtitle">
|
||||
{selectedAddress}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="ens-input__wrapper__action-icon ens-input__wrapper__action-icon--erase"
|
||||
onClick={this.resetInput}
|
||||
/>
|
||||
<div
|
||||
className="ens-input__wrapper__action-icon ens-input__wrapper__action-icon--erase"
|
||||
onClick={this.props.onReset}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
className="ens-input__wrapper__input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={t('recipientAddressPlaceholder')}
|
||||
onChange={this.onChange}
|
||||
onPaste={this.onPaste}
|
||||
value={selectedAddress || userInput}
|
||||
autoFocus
|
||||
data-testid="ens-input"
|
||||
/>
|
||||
<button
|
||||
className={classnames('ens-input__wrapper__action-icon', {
|
||||
'ens-input__wrapper__action-icon--erase': userInput,
|
||||
'ens-input__wrapper__action-icon--qrcode': !userInput,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (userInput) {
|
||||
this.props.onReset();
|
||||
} else {
|
||||
this.props.scanQrCode();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ensIcon(recipient) {
|
||||
const { hoverText } = this.state;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="#ensIcon"
|
||||
title={hoverText}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
left: '-25px',
|
||||
}}
|
||||
>
|
||||
{this.ensIconContents(recipient)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
ensIconContents() {
|
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state;
|
||||
|
||||
if (toError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loadingEns) {
|
||||
return (
|
||||
<img
|
||||
src="images/loading.svg"
|
||||
style={{
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
transform: 'translateY(-6px)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (ensFailure) {
|
||||
return <i className="fa fa-warning fa-lg warning" />;
|
||||
}
|
||||
|
||||
if (ensResolution && ensResolution !== ZERO_ADDRESS) {
|
||||
return (
|
||||
<i
|
||||
className="fa fa-check-circle fa-lg cursor-pointer"
|
||||
style={{ color: 'green' }}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
copyToClipboard(ensResolution);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getNetworkEnsSupport(network) {
|
||||
return Boolean(networkMap[network]);
|
||||
}
|
||||
|
@ -1,20 +1,18 @@
|
||||
import { debounce } from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { CHAIN_ID_TO_NETWORK_ID_MAP } from '../../../../../shared/constants/network';
|
||||
import {
|
||||
getSendTo,
|
||||
getSendToNickname,
|
||||
getAddressBookEntry,
|
||||
getCurrentChainId,
|
||||
} from '../../../../selectors';
|
||||
lookupEnsName,
|
||||
initializeEnsSlice,
|
||||
resetResolution,
|
||||
} from '../../../../ducks/ens';
|
||||
import EnsInput from './ens-input.component';
|
||||
|
||||
export default connect((state) => {
|
||||
const selectedAddress = getSendTo(state);
|
||||
const chainId = getCurrentChainId(state);
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
network: CHAIN_ID_TO_NETWORK_ID_MAP[chainId],
|
||||
selectedAddress,
|
||||
selectedName: getSendToNickname(state),
|
||||
contact: getAddressBookEntry(state, selectedAddress),
|
||||
lookupEnsName: debounce((ensName) => dispatch(lookupEnsName(ensName)), 150),
|
||||
initializeEnsSlice: () => dispatch(initializeEnsSlice()),
|
||||
resetEnsResolution: debounce(() => dispatch(resetResolution()), 300),
|
||||
};
|
||||
})(EnsInput);
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(EnsInput);
|
||||
|
@ -1,93 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import AmountMaxButton from './amount-max-button.component';
|
||||
|
||||
describe('AmountMaxButton Component', () => {
|
||||
let wrapper;
|
||||
let instance;
|
||||
|
||||
const propsMethodSpies = {
|
||||
setAmountToMax: sinon.spy(),
|
||||
setMaxModeTo: sinon.spy(),
|
||||
};
|
||||
|
||||
const MOCK_EVENT = { preventDefault: () => undefined };
|
||||
|
||||
beforeAll(() => {
|
||||
sinon.spy(AmountMaxButton.prototype, 'setMaxAmount');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<AmountMaxButton
|
||||
balance="mockBalance"
|
||||
gasTotal="mockGasTotal"
|
||||
maxModeOn={false}
|
||||
sendToken={{ address: 'mockTokenAddress' }}
|
||||
setAmountToMax={propsMethodSpies.setAmountToMax}
|
||||
setMaxModeTo={propsMethodSpies.setMaxModeTo}
|
||||
tokenBalance="mockTokenBalance"
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
t: (str) => `${str}_t`,
|
||||
metricsEvent: () => undefined,
|
||||
},
|
||||
},
|
||||
);
|
||||
instance = wrapper.instance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.setAmountToMax.resetHistory();
|
||||
propsMethodSpies.setMaxModeTo.resetHistory();
|
||||
AmountMaxButton.prototype.setMaxAmount.resetHistory();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('setMaxAmount', () => {
|
||||
it('should call setAmountToMax with the correct params', () => {
|
||||
expect(propsMethodSpies.setAmountToMax.callCount).toStrictEqual(0);
|
||||
instance.setMaxAmount();
|
||||
expect(propsMethodSpies.setAmountToMax.callCount).toStrictEqual(1);
|
||||
expect(propsMethodSpies.setAmountToMax.getCall(0).args).toStrictEqual([
|
||||
{
|
||||
balance: 'mockBalance',
|
||||
gasTotal: 'mockGasTotal',
|
||||
sendToken: { address: 'mockTokenAddress' },
|
||||
tokenBalance: 'mockTokenBalance',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
it('should render an element with a send-v2__amount-max class', () => {
|
||||
expect(wrapper.find('.send-v2__amount-max')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call setMaxModeTo and setMaxAmount when the checkbox is checked', () => {
|
||||
const { onClick } = wrapper.find('.send-v2__amount-max').props();
|
||||
|
||||
expect(AmountMaxButton.prototype.setMaxAmount.callCount).toStrictEqual(0);
|
||||
expect(propsMethodSpies.setMaxModeTo.callCount).toStrictEqual(0);
|
||||
onClick(MOCK_EVENT);
|
||||
expect(AmountMaxButton.prototype.setMaxAmount.callCount).toStrictEqual(1);
|
||||
expect(propsMethodSpies.setMaxModeTo.callCount).toStrictEqual(1);
|
||||
expect(propsMethodSpies.setMaxModeTo.getCall(0).args).toStrictEqual([
|
||||
true,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render the expected text when maxModeOn is false', () => {
|
||||
wrapper.setProps({ maxModeOn: false });
|
||||
expect(wrapper.find('.send-v2__amount-max').text()).toStrictEqual(
|
||||
'max_t',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,42 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getGasTotal,
|
||||
getSendToken,
|
||||
getSendFromBalance,
|
||||
getTokenBalance,
|
||||
getSendMaxModeState,
|
||||
getBasicGasEstimateLoadingStatus,
|
||||
} from '../../../../../selectors';
|
||||
import {
|
||||
updateSendErrors,
|
||||
updateSendAmount,
|
||||
setMaxModeTo,
|
||||
} from '../../../../../ducks/send/send.duck';
|
||||
import { calcMaxAmount } from './amount-max-button.utils';
|
||||
import AmountMaxButton from './amount-max-button.component';
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
balance: getSendFromBalance(state),
|
||||
buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
maxModeOn: getSendMaxModeState(state),
|
||||
sendToken: getSendToken(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
setAmountToMax: (maxAmountDataObject) => {
|
||||
dispatch(updateSendErrors({ amount: null }));
|
||||
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)));
|
||||
},
|
||||
clearMaxAmount: () => {
|
||||
dispatch(updateSendAmount('0'));
|
||||
},
|
||||
setMaxModeTo: (bool) => dispatch(setMaxModeTo(bool)),
|
||||
};
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
updateSendErrors,
|
||||
setMaxModeTo,
|
||||
updateSendAmount,
|
||||
} from '../../../../../ducks/send/send.duck';
|
||||
|
||||
let mapStateToProps;
|
||||
let mapDispatchToProps;
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms;
|
||||
mapDispatchToProps = md;
|
||||
return () => ({});
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../selectors', () => ({
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
getSendToken: (s) => `mockSendToken:${s}`,
|
||||
getSendFromBalance: (s) => `mockBalance:${s}`,
|
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`,
|
||||
getSendMaxModeState: (s) => `mockMaxModeOn:${s}`,
|
||||
getBasicGasEstimateLoadingStatus: (s) => `mockButtonDataLoading:${s}`,
|
||||
}));
|
||||
|
||||
jest.mock('./amount-max-button.utils.js', () => ({
|
||||
calcMaxAmount: (mockObj) => mockObj.val + 1,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../ducks/send/send.duck', () => ({
|
||||
setMaxModeTo: jest.fn(),
|
||||
updateSendAmount: jest.fn(),
|
||||
updateSendErrors: jest.fn(),
|
||||
}));
|
||||
|
||||
require('./amount-max-button.container.js');
|
||||
|
||||
describe('amount-max-button container', () => {
|
||||
describe('mapStateToProps()', () => {
|
||||
it('should map the correct properties to props', () => {
|
||||
expect(mapStateToProps('mockState')).toStrictEqual({
|
||||
balance: 'mockBalance:mockState',
|
||||
buttonDataLoading: 'mockButtonDataLoading:mockState',
|
||||
gasTotal: 'mockGasTotal:mockState',
|
||||
maxModeOn: 'mockMaxModeOn:mockState',
|
||||
sendToken: 'mockSendToken:mockState',
|
||||
tokenBalance: 'mockTokenBalance:mockState',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
let dispatchSpy;
|
||||
let mapDispatchToPropsObject;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = sinon.spy();
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy);
|
||||
});
|
||||
|
||||
describe('setAmountToMax()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' });
|
||||
expect(dispatchSpy.calledTwice).toStrictEqual(true);
|
||||
expect(updateSendErrors).toHaveBeenCalled();
|
||||
expect(updateSendErrors).toHaveBeenCalledWith({ amount: null });
|
||||
expect(updateSendAmount).toHaveBeenCalled();
|
||||
expect(updateSendAmount).toHaveBeenCalledWith(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMaxModeTo()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.setMaxModeTo('mockVal');
|
||||
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||
expect(setMaxModeTo).toHaveBeenCalledWith('mockVal');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getBasicGasEstimateLoadingStatus } from '../../../../../selectors';
|
||||
import {
|
||||
getSendMaxModeState,
|
||||
isSendFormInvalid,
|
||||
toggleSendMaxMode,
|
||||
} from '../../../../../ducks/send';
|
||||
import { useI18nContext } from '../../../../../hooks/useI18nContext';
|
||||
import { useMetricEvent } from '../../../../../hooks/useMetricEvent';
|
||||
|
||||
export default function AmountMaxButton() {
|
||||
const buttonDataLoading = useSelector(getBasicGasEstimateLoadingStatus);
|
||||
const isDraftTransactionInvalid = useSelector(isSendFormInvalid);
|
||||
const maxModeOn = useSelector(getSendMaxModeState);
|
||||
const dispatch = useDispatch();
|
||||
const trackClickedMax = useMetricEvent({
|
||||
eventOpts: {
|
||||
category: 'Transactions',
|
||||
action: 'Edit Screen',
|
||||
name: 'Clicked "Amount Max"',
|
||||
},
|
||||
});
|
||||
const t = useI18nContext();
|
||||
|
||||
const onMaxClick = () => {
|
||||
trackClickedMax();
|
||||
dispatch(toggleSendMaxMode());
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="send-v2__amount-max"
|
||||
disabled={buttonDataLoading || isDraftTransactionInvalid}
|
||||
onClick={onMaxClick}
|
||||
>
|
||||
<input type="checkbox" checked={maxModeOn} readOnly />
|
||||
<div
|
||||
className={classnames('send-v2__amount-max__button', {
|
||||
'send-v2__amount-max__button__disabled':
|
||||
buttonDataLoading || isDraftTransactionInvalid,
|
||||
})}
|
||||
>
|
||||
{t('max')}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { initialState, SEND_STATUSES } from '../../../../../ducks/send';
|
||||
import { renderWithProvider } from '../../../../../../test/jest';
|
||||
import AmountMaxButton from './amount-max-button';
|
||||
|
||||
const middleware = [thunk];
|
||||
|
||||
describe('AmountMaxButton Component', () => {
|
||||
describe('render', () => {
|
||||
it('should render a "Max" button', () => {
|
||||
const { getByText } = renderWithProvider(
|
||||
<AmountMaxButton />,
|
||||
configureMockStore(middleware)({
|
||||
send: initialState,
|
||||
gas: { basicEstimateStatus: 'LOADING' },
|
||||
}),
|
||||
);
|
||||
expect(getByText('Max')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should dispatch action to set mode to MAX', () => {
|
||||
const store = configureMockStore(middleware)({
|
||||
send: { ...initialState, status: SEND_STATUSES.VALID },
|
||||
gas: { basicEstimateStatus: 'READY' },
|
||||
});
|
||||
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'send/updateAmountMode', payload: 'MAX' },
|
||||
];
|
||||
|
||||
fireEvent.click(getByText('Max'), { bubbles: true });
|
||||
const actions = store.getActions();
|
||||
expect(actions).toStrictEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('should dispatch action to set amount mode to INPUT', () => {
|
||||
const store = configureMockStore(middleware)({
|
||||
send: {
|
||||
...initialState,
|
||||
status: SEND_STATUSES.VALID,
|
||||
amount: { ...initialState.amount, mode: 'MAX' },
|
||||
},
|
||||
gas: { basicEstimateStatus: 'READY' },
|
||||
});
|
||||
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'send/updateAmountMode', payload: 'INPUT' },
|
||||
];
|
||||
|
||||
fireEvent.click(getByText('Max'), { bubbles: true });
|
||||
const actions = store.getActions();
|
||||
expect(actions).toStrictEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,22 +0,0 @@
|
||||
import {
|
||||
multiplyCurrencies,
|
||||
subtractCurrencies,
|
||||
} from '../../../../../helpers/utils/conversion-util';
|
||||
import { addHexPrefix } from '../../../../../../app/scripts/lib/util';
|
||||
|
||||
export function calcMaxAmount({ balance, gasTotal, sendToken, tokenBalance }) {
|
||||
const { decimals } = sendToken || {};
|
||||
const multiplier = Math.pow(10, Number(decimals || 0));
|
||||
|
||||
return sendToken
|
||||
? multiplyCurrencies(tokenBalance, multiplier, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 10,
|
||||
})
|
||||
: subtractCurrencies(addHexPrefix(balance), addHexPrefix(gasTotal), {
|
||||
toNumericBase: 'hex',
|
||||
aBase: 16,
|
||||
bBase: 16,
|
||||
});
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { calcMaxAmount } from './amount-max-button.utils';
|
||||
|
||||
describe('amount-max-button utils', () => {
|
||||
describe('calcMaxAmount()', () => {
|
||||
it('should calculate the correct amount when no sendToken defined', () => {
|
||||
expect(
|
||||
calcMaxAmount({
|
||||
balance: 'ffffff',
|
||||
gasTotal: 'ff',
|
||||
sendToken: false,
|
||||
}),
|
||||
).toStrictEqual('ffff00');
|
||||
});
|
||||
|
||||
it('should calculate the correct amount when a sendToken is defined', () => {
|
||||
expect(
|
||||
calcMaxAmount({
|
||||
sendToken: {
|
||||
decimals: 10,
|
||||
},
|
||||
tokenBalance: '64',
|
||||
}),
|
||||
).toStrictEqual('e8d4a51000');
|
||||
});
|
||||
});
|
||||
});
|
@ -1 +1 @@
|
||||
export { default } from './amount-max-button.container';
|
||||
export { default } from './amount-max-button';
|
||||
|
@ -1,111 +1,35 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
import SendRowWrapper from '../send-row-wrapper';
|
||||
import UserPreferencedCurrencyInput from '../../../../components/app/user-preferenced-currency-input';
|
||||
import UserPreferencedTokenInput from '../../../../components/app/user-preferenced-token-input';
|
||||
import { ASSET_TYPES } from '../../../../ducks/send';
|
||||
import AmountMaxButton from './amount-max-button';
|
||||
|
||||
export default class SendAmountRow extends Component {
|
||||
static propTypes = {
|
||||
amount: PropTypes.string,
|
||||
balance: PropTypes.string,
|
||||
conversionRate: PropTypes.number,
|
||||
gasTotal: PropTypes.string,
|
||||
inError: PropTypes.bool,
|
||||
primaryCurrency: PropTypes.string,
|
||||
sendToken: PropTypes.object,
|
||||
setMaxModeTo: PropTypes.func,
|
||||
tokenBalance: PropTypes.string,
|
||||
updateGasFeeError: PropTypes.func,
|
||||
asset: PropTypes.object,
|
||||
updateSendAmount: PropTypes.func,
|
||||
updateSendAmountError: PropTypes.func,
|
||||
updateGas: PropTypes.func,
|
||||
maxModeOn: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { maxModeOn: prevMaxModeOn, gasTotal: prevGasTotal } = prevProps;
|
||||
const { maxModeOn, amount, gasTotal, sendToken } = this.props;
|
||||
|
||||
if (maxModeOn && sendToken && !prevMaxModeOn) {
|
||||
this.updateGas(amount);
|
||||
}
|
||||
|
||||
if (prevGasTotal !== gasTotal) {
|
||||
this.validateAmount(amount);
|
||||
}
|
||||
}
|
||||
|
||||
updateGas = debounce(this.updateGas.bind(this), 500);
|
||||
|
||||
validateAmount(amount) {
|
||||
const {
|
||||
balance,
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
primaryCurrency,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
updateGasFeeError,
|
||||
updateSendAmountError,
|
||||
} = this.props;
|
||||
|
||||
updateSendAmountError({
|
||||
amount,
|
||||
balance,
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
primaryCurrency,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
});
|
||||
|
||||
if (sendToken) {
|
||||
updateGasFeeError({
|
||||
balance,
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
primaryCurrency,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateAmount(amount) {
|
||||
const { updateSendAmount, setMaxModeTo } = this.props;
|
||||
|
||||
setMaxModeTo(false);
|
||||
updateSendAmount(amount);
|
||||
}
|
||||
|
||||
updateGas(amount) {
|
||||
const { sendToken, updateGas } = this.props;
|
||||
|
||||
if (sendToken) {
|
||||
updateGas({ amount });
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (newAmount) => {
|
||||
this.validateAmount(newAmount);
|
||||
this.updateGas(newAmount);
|
||||
this.updateAmount(newAmount);
|
||||
this.props.updateSendAmount(newAmount);
|
||||
};
|
||||
|
||||
renderInput() {
|
||||
const { amount, inError, sendToken } = this.props;
|
||||
const { amount, inError, asset } = this.props;
|
||||
|
||||
return sendToken ? (
|
||||
return asset.type === ASSET_TYPES.TOKEN ? (
|
||||
<UserPreferencedTokenInput
|
||||
error={inError}
|
||||
onChange={this.handleChange}
|
||||
token={sendToken}
|
||||
token={asset.details}
|
||||
value={amount}
|
||||
/>
|
||||
) : (
|
||||
@ -118,7 +42,7 @@ export default class SendAmountRow extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { gasTotal, inError } = this.props;
|
||||
const { inError } = this.props;
|
||||
|
||||
return (
|
||||
<SendRowWrapper
|
||||
@ -126,7 +50,7 @@ export default class SendAmountRow extends Component {
|
||||
showError={inError}
|
||||
errorType="amount"
|
||||
>
|
||||
{gasTotal && <AmountMaxButton inError={inError} />}
|
||||
<AmountMaxButton inError={inError} />
|
||||
{this.renderInput()}
|
||||
</SendRowWrapper>
|
||||
);
|
||||
|
@ -3,88 +3,13 @@ import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component';
|
||||
import UserPreferencedTokenInput from '../../../../components/app/user-preferenced-token-input';
|
||||
import { ASSET_TYPES } from '../../../../ducks/send';
|
||||
import SendAmountRow from './send-amount-row.component';
|
||||
|
||||
import AmountMaxButton from './amount-max-button/amount-max-button.container';
|
||||
import AmountMaxButton from './amount-max-button/amount-max-button';
|
||||
|
||||
describe('SendAmountRow Component', () => {
|
||||
describe('validateAmount', () => {
|
||||
it('should call updateSendAmountError with the correct params', () => {
|
||||
const {
|
||||
instance,
|
||||
propsMethodSpies: { updateSendAmountError },
|
||||
} = shallowRenderSendAmountRow();
|
||||
|
||||
expect(updateSendAmountError.callCount).toStrictEqual(0);
|
||||
|
||||
instance.validateAmount('someAmount');
|
||||
|
||||
expect(
|
||||
updateSendAmountError.calledOnceWithExactly({
|
||||
amount: 'someAmount',
|
||||
balance: 'mockBalance',
|
||||
conversionRate: 7,
|
||||
gasTotal: 'mockGasTotal',
|
||||
primaryCurrency: 'mockPrimaryCurrency',
|
||||
sendToken: { address: 'mockTokenAddress' },
|
||||
tokenBalance: 'mockTokenBalance',
|
||||
}),
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should call updateGasFeeError if sendToken is truthy', () => {
|
||||
const {
|
||||
instance,
|
||||
propsMethodSpies: { updateGasFeeError },
|
||||
} = shallowRenderSendAmountRow();
|
||||
|
||||
expect(updateGasFeeError.callCount).toStrictEqual(0);
|
||||
|
||||
instance.validateAmount('someAmount');
|
||||
|
||||
expect(
|
||||
updateGasFeeError.calledOnceWithExactly({
|
||||
balance: 'mockBalance',
|
||||
conversionRate: 7,
|
||||
gasTotal: 'mockGasTotal',
|
||||
primaryCurrency: 'mockPrimaryCurrency',
|
||||
sendToken: { address: 'mockTokenAddress' },
|
||||
tokenBalance: 'mockTokenBalance',
|
||||
}),
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should call not updateGasFeeError if sendToken is falsey', () => {
|
||||
const {
|
||||
wrapper,
|
||||
instance,
|
||||
propsMethodSpies: { updateGasFeeError },
|
||||
} = shallowRenderSendAmountRow();
|
||||
|
||||
wrapper.setProps({ sendToken: null });
|
||||
|
||||
expect(updateGasFeeError.callCount).toStrictEqual(0);
|
||||
|
||||
instance.validateAmount('someAmount');
|
||||
|
||||
expect(updateGasFeeError.callCount).toStrictEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAmount', () => {
|
||||
it('should call setMaxModeTo', () => {
|
||||
const {
|
||||
instance,
|
||||
propsMethodSpies: { setMaxModeTo },
|
||||
} = shallowRenderSendAmountRow();
|
||||
|
||||
expect(setMaxModeTo.callCount).toStrictEqual(0);
|
||||
|
||||
instance.updateAmount('someAmount');
|
||||
|
||||
expect(setMaxModeTo.calledOnceWithExactly(false)).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should call updateSendAmount', () => {
|
||||
const {
|
||||
instance,
|
||||
@ -93,7 +18,7 @@ describe('SendAmountRow Component', () => {
|
||||
|
||||
expect(updateSendAmount.callCount).toStrictEqual(0);
|
||||
|
||||
instance.updateAmount('someAmount');
|
||||
instance.handleChange('someAmount');
|
||||
|
||||
expect(
|
||||
updateSendAmount.calledOnceWithExactly('someAmount'),
|
||||
@ -136,10 +61,7 @@ describe('SendAmountRow Component', () => {
|
||||
});
|
||||
|
||||
it('should render the UserPreferencedTokenInput with the correct props', () => {
|
||||
const {
|
||||
wrapper,
|
||||
instanceSpies: { updateGas, updateAmount, validateAmount },
|
||||
} = shallowRenderSendAmountRow();
|
||||
const { wrapper } = shallowRenderSendAmountRow();
|
||||
const { onChange, error, value } = wrapper
|
||||
.find(SendRowWrapper)
|
||||
.childAt(1)
|
||||
@ -147,67 +69,34 @@ describe('SendAmountRow Component', () => {
|
||||
|
||||
expect(error).toStrictEqual(false);
|
||||
expect(value).toStrictEqual('mockAmount');
|
||||
expect(updateGas.callCount).toStrictEqual(0);
|
||||
expect(updateAmount.callCount).toStrictEqual(0);
|
||||
expect(validateAmount.callCount).toStrictEqual(0);
|
||||
|
||||
onChange('mockNewAmount');
|
||||
|
||||
expect(updateGas.calledOnceWithExactly('mockNewAmount')).toStrictEqual(
|
||||
true,
|
||||
);
|
||||
expect(updateAmount.calledOnceWithExactly('mockNewAmount')).toStrictEqual(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
validateAmount.calledOnceWithExactly('mockNewAmount'),
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function shallowRenderSendAmountRow() {
|
||||
const setMaxModeTo = sinon.spy();
|
||||
const updateGasFeeError = sinon.spy();
|
||||
const updateSendAmount = sinon.spy();
|
||||
const updateSendAmountError = sinon.spy();
|
||||
const wrapper = shallow(
|
||||
<SendAmountRow
|
||||
amount="mockAmount"
|
||||
balance="mockBalance"
|
||||
conversionRate={7}
|
||||
convertedCurrency="mockConvertedCurrency"
|
||||
gasTotal="mockGasTotal"
|
||||
inError={false}
|
||||
primaryCurrency="mockPrimaryCurrency"
|
||||
sendToken={{ address: 'mockTokenAddress' }}
|
||||
setMaxModeTo={setMaxModeTo}
|
||||
tokenBalance="mockTokenBalance"
|
||||
updateGasFeeError={updateGasFeeError}
|
||||
asset={{
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
balance: 'mockTokenBalance',
|
||||
details: { address: 'mockTokenAddress' },
|
||||
}}
|
||||
updateSendAmount={updateSendAmount}
|
||||
updateSendAmountError={updateSendAmountError}
|
||||
updateGas={() => undefined}
|
||||
/>,
|
||||
{ context: { t: (str) => `${str}_t` } },
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
const updateAmount = sinon.spy(instance, 'updateAmount');
|
||||
const updateGas = sinon.spy(instance, 'updateGas');
|
||||
const validateAmount = sinon.spy(instance, 'validateAmount');
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
propsMethodSpies: {
|
||||
setMaxModeTo,
|
||||
updateGasFeeError,
|
||||
updateSendAmount,
|
||||
updateSendAmountError,
|
||||
},
|
||||
instanceSpies: {
|
||||
updateAmount,
|
||||
updateGas,
|
||||
validateAmount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,21 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getGasTotal,
|
||||
getPrimaryCurrency,
|
||||
getSendToken,
|
||||
getSendAmount,
|
||||
getSendFromBalance,
|
||||
getTokenBalance,
|
||||
getSendMaxModeState,
|
||||
sendAmountIsInError,
|
||||
} from '../../../../selectors';
|
||||
import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils';
|
||||
import {
|
||||
updateSendErrors,
|
||||
setMaxModeTo,
|
||||
updateSendAmount,
|
||||
} from '../../../../ducks/send/send.duck';
|
||||
import { getConversionRate } from '../../../../ducks/metamask/metamask';
|
||||
getSendAmount,
|
||||
sendAmountIsInError,
|
||||
getSendAsset,
|
||||
} from '../../../../ducks/send';
|
||||
import SendAmountRow from './send-amount-row.component';
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow);
|
||||
@ -23,26 +12,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
amount: getSendAmount(state),
|
||||
balance: getSendFromBalance(state),
|
||||
conversionRate: getConversionRate(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
inError: sendAmountIsInError(state),
|
||||
primaryCurrency: getPrimaryCurrency(state),
|
||||
sendToken: getSendToken(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
maxModeOn: getSendMaxModeState(state),
|
||||
asset: getSendAsset(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
setMaxModeTo: (bool) => dispatch(setMaxModeTo(bool)),
|
||||
updateSendAmount: (newAmount) => dispatch(updateSendAmount(newAmount)),
|
||||
updateGasFeeError: (amountDataObject) => {
|
||||
dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject)));
|
||||
},
|
||||
updateSendAmountError: (amountDataObject) => {
|
||||
dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
updateSendErrors,
|
||||
setMaxModeTo,
|
||||
updateSendAmount,
|
||||
} from '../../../../ducks/send/send.duck';
|
||||
import { updateSendAmount } from '../../../../ducks/send';
|
||||
|
||||
let mapDispatchToProps;
|
||||
|
||||
@ -15,24 +11,7 @@ jest.mock('react-redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../selectors/send.js', () => ({
|
||||
sendAmountIsInError: (s) => `mockInError:${s}`,
|
||||
}));
|
||||
|
||||
jest.mock('../../send.utils', () => ({
|
||||
getAmountErrorObject: (mockDataObject) => ({
|
||||
...mockDataObject,
|
||||
mockChange: true,
|
||||
}),
|
||||
getGasFeeErrorObject: (mockDataObject) => ({
|
||||
...mockDataObject,
|
||||
mockGasFeeErrorChange: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../ducks/send/send.duck', () => ({
|
||||
updateSendErrors: jest.fn(),
|
||||
setMaxModeTo: jest.fn(),
|
||||
jest.mock('../../../../ducks/send', () => ({
|
||||
updateSendAmount: jest.fn(),
|
||||
}));
|
||||
|
||||
@ -48,15 +27,6 @@ describe('send-amount-row container', () => {
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy);
|
||||
});
|
||||
|
||||
describe('setMaxModeTo()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.setMaxModeTo('mockBool');
|
||||
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||
expect(setMaxModeTo).toHaveBeenCalled();
|
||||
expect(setMaxModeTo).toHaveBeenCalledWith('mockBool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSendAmount()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.updateSendAmount('mockAmount');
|
||||
@ -65,29 +35,5 @@ describe('send-amount-row container', () => {
|
||||
expect(updateSendAmount).toHaveBeenCalledWith('mockAmount');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateGasFeeError()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.updateGasFeeError({ some: 'data' });
|
||||
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||
expect(updateSendErrors).toHaveBeenCalled();
|
||||
expect(updateSendErrors).toHaveBeenCalledWith({
|
||||
some: 'data',
|
||||
mockGasFeeErrorChange: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSendAmountError()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.updateSendAmountError({ some: 'data' });
|
||||
expect(dispatchSpy.calledOnce).toStrictEqual(true);
|
||||
expect(updateSendErrors).toHaveBeenCalled();
|
||||
expect(updateSendErrors).toHaveBeenCalledWith({
|
||||
some: 'data',
|
||||
mockChange: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import Identicon from '../../../../components/ui/identicon/identicon.component';
|
||||
import TokenBalance from '../../../../components/ui/token-balance';
|
||||
import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display';
|
||||
import { ERC20, PRIMARY } from '../../../../helpers/constants/common';
|
||||
import { ASSET_TYPES } from '../../../../ducks/send';
|
||||
|
||||
export default class SendAssetRow extends Component {
|
||||
static propTypes = {
|
||||
@ -18,13 +19,10 @@ export default class SendAssetRow extends Component {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
assetImages: PropTypes.object,
|
||||
selectedAddress: PropTypes.string.isRequired,
|
||||
sendTokenAddress: PropTypes.string,
|
||||
setSendToken: PropTypes.func.isRequired,
|
||||
sendAssetAddress: PropTypes.string,
|
||||
updateSendAsset: PropTypes.func.isRequired,
|
||||
nativeCurrency: PropTypes.string,
|
||||
nativeCurrencyImage: PropTypes.string,
|
||||
setUnsendableAssetError: PropTypes.func.isRequired,
|
||||
updateSendErrors: PropTypes.func.isRequired,
|
||||
updateTokenType: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@ -46,29 +44,7 @@ export default class SendAssetRow extends Component {
|
||||
|
||||
closeDropdown = () => this.setState({ isShowingDropdown: false });
|
||||
|
||||
clearUnsendableAssetError = () => {
|
||||
this.props.setUnsendableAssetError(false);
|
||||
this.props.updateSendErrors({
|
||||
unsendableAssetError: null,
|
||||
gasLoadingError: null,
|
||||
});
|
||||
};
|
||||
|
||||
selectToken = async (token) => {
|
||||
if (token && token.isERC721 === undefined) {
|
||||
const updatedToken = await this.props.updateTokenType(token.address);
|
||||
if (updatedToken.isERC721) {
|
||||
this.props.setUnsendableAssetError(true);
|
||||
this.props.updateSendErrors({
|
||||
unsendableAssetError: 'unsendableAssetError',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((token && token.isERC721 === false) || token === undefined) {
|
||||
this.clearUnsendableAssetError();
|
||||
}
|
||||
|
||||
selectToken = (type, token) => {
|
||||
this.setState(
|
||||
{
|
||||
isShowingDropdown: false,
|
||||
@ -84,7 +60,10 @@ export default class SendAssetRow extends Component {
|
||||
assetSelected: token ? ERC20 : this.props.nativeCurrency,
|
||||
},
|
||||
});
|
||||
this.props.setSendToken(token);
|
||||
this.props.updateSendAsset({
|
||||
type,
|
||||
details: type === ASSET_TYPES.NATIVE ? null : token,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
@ -105,9 +84,9 @@ export default class SendAssetRow extends Component {
|
||||
}
|
||||
|
||||
renderSendToken() {
|
||||
const { sendTokenAddress } = this.props;
|
||||
const { sendAssetAddress } = this.props;
|
||||
const token = this.props.tokens.find(
|
||||
({ address }) => address === sendTokenAddress,
|
||||
({ address }) => address === sendAssetAddress,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
@ -158,7 +137,7 @@ export default class SendAssetRow extends Component {
|
||||
? 'send-v2__asset-dropdown__asset'
|
||||
: 'send-v2__asset-dropdown__single-asset'
|
||||
}
|
||||
onClick={() => this.selectToken()}
|
||||
onClick={() => this.selectToken(ASSET_TYPES.NATIVE)}
|
||||
>
|
||||
<div className="send-v2__asset-dropdown__asset-icon">
|
||||
<Identicon
|
||||
@ -197,7 +176,7 @@ export default class SendAssetRow extends Component {
|
||||
<div
|
||||
key={address}
|
||||
className="send-v2__asset-dropdown__asset"
|
||||
onClick={() => this.selectToken(token)}
|
||||
onClick={() => this.selectToken(ASSET_TYPES.TOKEN, token)}
|
||||
>
|
||||
<div className="send-v2__asset-dropdown__asset-icon">
|
||||
<Identicon
|
||||
|
@ -3,21 +3,16 @@ import { getNativeCurrency } from '../../../../ducks/metamask/metamask';
|
||||
import {
|
||||
getMetaMaskAccounts,
|
||||
getNativeCurrencyImage,
|
||||
getSendTokenAddress,
|
||||
getAssetImages,
|
||||
} from '../../../../selectors';
|
||||
import { updateTokenType } from '../../../../store/actions';
|
||||
import {
|
||||
updateSendErrors,
|
||||
updateSendToken,
|
||||
} from '../../../../ducks/send/send.duck';
|
||||
import { updateSendAsset, getSendAssetAddress } from '../../../../ducks/send';
|
||||
import SendAssetRow from './send-asset-row.component';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tokens: state.metamask.tokens,
|
||||
selectedAddress: state.metamask.selectedAddress,
|
||||
sendTokenAddress: getSendTokenAddress(state),
|
||||
sendAssetAddress: getSendAssetAddress(state),
|
||||
accounts: getMetaMaskAccounts(state),
|
||||
nativeCurrency: getNativeCurrency(state),
|
||||
nativeCurrencyImage: getNativeCurrencyImage(state),
|
||||
@ -27,11 +22,8 @@ function mapStateToProps(state) {
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
setSendToken: (token) => dispatch(updateSendToken(token)),
|
||||
updateTokenType: (tokenAddress) => dispatch(updateTokenType(tokenAddress)),
|
||||
updateSendErrors: (error) => {
|
||||
dispatch(updateSendErrors(error));
|
||||
},
|
||||
updateSendAsset: ({ type, details }) =>
|
||||
dispatch(updateSendAsset({ type, details })),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -18,12 +18,8 @@ export default class SendContent extends Component {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
unsendableAssetError: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
updateGas: PropTypes.func,
|
||||
isAssetSendable: PropTypes.bool,
|
||||
showAddToAddressBookModal: PropTypes.func,
|
||||
showHexData: PropTypes.bool,
|
||||
contact: PropTypes.object,
|
||||
@ -35,11 +31,6 @@ export default class SendContent extends Component {
|
||||
noGasPrice: PropTypes.bool,
|
||||
};
|
||||
|
||||
updateGas = (updateData) => this.props.updateGas(updateData);
|
||||
|
||||
setUnsendableAssetError = (unsendableAssetError) =>
|
||||
this.setState({ unsendableAssetError });
|
||||
|
||||
render() {
|
||||
const {
|
||||
warning,
|
||||
@ -47,9 +38,9 @@ export default class SendContent extends Component {
|
||||
gasIsExcessive,
|
||||
isEthGasPrice,
|
||||
noGasPrice,
|
||||
isAssetSendable,
|
||||
} = this.props;
|
||||
|
||||
const { unsendableAssetError } = this.state;
|
||||
let gasError;
|
||||
if (gasIsExcessive) gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY;
|
||||
else if (noGasPrice) gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY;
|
||||
@ -59,18 +50,15 @@ export default class SendContent extends Component {
|
||||
<div className="send-v2__form">
|
||||
{gasError && this.renderError(gasError)}
|
||||
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
|
||||
{unsendableAssetError && this.renderError(UNSENDABLE_ASSET_ERROR_KEY)}
|
||||
{isAssetSendable === false &&
|
||||
this.renderError(UNSENDABLE_ASSET_ERROR_KEY)}
|
||||
{error && this.renderError(error)}
|
||||
{warning && this.renderWarning()}
|
||||
{this.maybeRenderAddContact()}
|
||||
<SendAssetRow
|
||||
setUnsendableAssetError={this.setUnsendableAssetError}
|
||||
/>
|
||||
<SendAmountRow updateGas={this.updateGas} />
|
||||
<SendAssetRow />
|
||||
<SendAmountRow />
|
||||
<SendGasRow />
|
||||
{this.props.showHexData && (
|
||||
<SendHexDataRow updateGas={this.updateGas} />
|
||||
)}
|
||||
{this.props.showHexData && <SendHexDataRow />}
|
||||
</div>
|
||||
</PageContainerContent>
|
||||
);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getSendTo,
|
||||
accountsWithSendEtherInfoSelector,
|
||||
getAddressBookEntry,
|
||||
getIsEthGasPriceFetched,
|
||||
getNoGasPriceFetched,
|
||||
} from '../../../selectors';
|
||||
|
||||
import { getIsAssetSendable, getSendTo } from '../../../ducks/send';
|
||||
|
||||
import * as actions from '../../../store/actions';
|
||||
import SendContent from './send-content.component';
|
||||
|
||||
@ -14,15 +15,16 @@ function mapStateToProps(state) {
|
||||
const ownedAccounts = accountsWithSendEtherInfoSelector(state);
|
||||
const to = getSendTo(state);
|
||||
return {
|
||||
isAssetSendable: getIsAssetSendable(state),
|
||||
isOwnedAccount: Boolean(
|
||||
ownedAccounts.find(
|
||||
({ address }) => address.toLowerCase() === to.toLowerCase(),
|
||||
),
|
||||
),
|
||||
contact: getAddressBookEntry(state, to),
|
||||
to,
|
||||
isEthGasPrice: getIsEthGasPriceFetched(state),
|
||||
noGasPrice: getNoGasPriceFetched(state),
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3,31 +3,25 @@ import PropTypes from 'prop-types';
|
||||
import SendRowWrapper from '../send-row-wrapper';
|
||||
import GasPriceButtonGroup from '../../../../components/app/gas-customization/gas-price-button-group';
|
||||
import AdvancedGasInputs from '../../../../components/app/gas-customization/advanced-gas-inputs';
|
||||
import { GAS_INPUT_MODES } from '../../../../ducks/send';
|
||||
import GasFeeDisplay from './gas-fee-display/gas-fee-display.component';
|
||||
|
||||
export default class SendGasRow extends Component {
|
||||
static propTypes = {
|
||||
balance: PropTypes.string,
|
||||
gasFeeError: PropTypes.bool,
|
||||
gasLoadingError: PropTypes.bool,
|
||||
gasTotal: PropTypes.string,
|
||||
maxModeOn: PropTypes.bool,
|
||||
showCustomizeGasModal: PropTypes.func,
|
||||
sendToken: PropTypes.object,
|
||||
setAmountToMax: PropTypes.func,
|
||||
setGasPrice: PropTypes.func,
|
||||
setGasLimit: PropTypes.func,
|
||||
tokenBalance: PropTypes.string,
|
||||
updateGasPrice: PropTypes.func,
|
||||
updateGasLimit: PropTypes.func,
|
||||
gasInputMode: PropTypes.oneOf(Object.values(GAS_INPUT_MODES)),
|
||||
gasPriceButtonGroupProps: PropTypes.object,
|
||||
gasButtonGroupShown: PropTypes.bool,
|
||||
advancedInlineGasShown: PropTypes.bool,
|
||||
resetGasButtons: PropTypes.func,
|
||||
gasPrice: PropTypes.string,
|
||||
gasLimit: PropTypes.string,
|
||||
insufficientBalance: PropTypes.bool,
|
||||
isMainnet: PropTypes.bool,
|
||||
isEthGasPrice: PropTypes.bool,
|
||||
noGasPrice: PropTypes.bool,
|
||||
minimumGasLimit: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@ -37,19 +31,7 @@ export default class SendGasRow extends Component {
|
||||
|
||||
renderAdvancedOptionsButton() {
|
||||
const { trackEvent } = this.context;
|
||||
const {
|
||||
showCustomizeGasModal,
|
||||
isMainnet,
|
||||
isEthGasPrice,
|
||||
noGasPrice,
|
||||
} = this.props;
|
||||
// Tests should behave in same way as mainnet, but are using Localhost
|
||||
if (!isMainnet && !process.env.IN_TEST) {
|
||||
return null;
|
||||
}
|
||||
if (isEthGasPrice || noGasPrice) {
|
||||
return null;
|
||||
}
|
||||
const { showCustomizeGasModal } = this.props;
|
||||
return (
|
||||
<div
|
||||
className="advanced-gas-options-btn"
|
||||
@ -66,44 +48,22 @@ export default class SendGasRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
setMaxAmount() {
|
||||
const {
|
||||
balance,
|
||||
gasTotal,
|
||||
sendToken,
|
||||
setAmountToMax,
|
||||
tokenBalance,
|
||||
} = this.props;
|
||||
|
||||
setAmountToMax({
|
||||
balance,
|
||||
gasTotal,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
});
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
gasLoadingError,
|
||||
gasTotal,
|
||||
showCustomizeGasModal,
|
||||
gasPriceButtonGroupProps,
|
||||
gasButtonGroupShown,
|
||||
advancedInlineGasShown,
|
||||
maxModeOn,
|
||||
gasInputMode,
|
||||
resetGasButtons,
|
||||
setGasPrice,
|
||||
setGasLimit,
|
||||
updateGasPrice,
|
||||
updateGasLimit,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
insufficientBalance,
|
||||
isMainnet,
|
||||
isEthGasPrice,
|
||||
noGasPrice,
|
||||
minimumGasLimit,
|
||||
} = this.props;
|
||||
const { trackEvent } = this.context;
|
||||
const gasPriceFetchFailure = isEthGasPrice || noGasPrice;
|
||||
|
||||
const gasPriceButtonGroup = (
|
||||
<div>
|
||||
@ -120,9 +80,6 @@ export default class SendGasRow extends Component {
|
||||
},
|
||||
});
|
||||
await gasPriceButtonGroupProps.handleGasPriceSelection(opts);
|
||||
if (maxModeOn) {
|
||||
this.setMaxAmount();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -131,51 +88,38 @@ export default class SendGasRow extends Component {
|
||||
<GasFeeDisplay
|
||||
gasLoadingError={gasLoadingError}
|
||||
gasTotal={gasTotal}
|
||||
onReset={() => {
|
||||
resetGasButtons();
|
||||
if (maxModeOn) {
|
||||
this.setMaxAmount();
|
||||
}
|
||||
}}
|
||||
onClick={() => showCustomizeGasModal()}
|
||||
onReset={resetGasButtons}
|
||||
onClick={showCustomizeGasModal}
|
||||
/>
|
||||
);
|
||||
const advancedGasInputs = (
|
||||
<div>
|
||||
<AdvancedGasInputs
|
||||
updateCustomGasPrice={(newGasPrice) =>
|
||||
setGasPrice({ gasPrice: newGasPrice, gasLimit })
|
||||
}
|
||||
updateCustomGasLimit={(newGasLimit) =>
|
||||
setGasLimit(newGasLimit, gasPrice)
|
||||
}
|
||||
updateCustomGasPrice={updateGasPrice}
|
||||
updateCustomGasLimit={updateGasLimit}
|
||||
customGasPrice={gasPrice}
|
||||
customGasLimit={gasLimit}
|
||||
insufficientBalance={insufficientBalance}
|
||||
minimumGasLimit={minimumGasLimit}
|
||||
customPriceIsSafe
|
||||
isSpeedUp={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
// Tests should behave in same way as mainnet, but are using Localhost
|
||||
if (
|
||||
advancedInlineGasShown ||
|
||||
(!isMainnet && !process.env.IN_TEST) ||
|
||||
gasPriceFetchFailure
|
||||
) {
|
||||
return advancedGasInputs;
|
||||
} else if (gasButtonGroupShown) {
|
||||
return gasPriceButtonGroup;
|
||||
switch (gasInputMode) {
|
||||
case GAS_INPUT_MODES.BASIC:
|
||||
return gasPriceButtonGroup;
|
||||
case GAS_INPUT_MODES.INLINE:
|
||||
return advancedGasInputs;
|
||||
case GAS_INPUT_MODES.CUSTOM:
|
||||
default:
|
||||
return gasFeeDisplay;
|
||||
}
|
||||
return gasFeeDisplay;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
gasFeeError,
|
||||
gasButtonGroupShown,
|
||||
advancedInlineGasShown,
|
||||
} = this.props;
|
||||
const { gasFeeError, gasInputMode, advancedInlineGasShown } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -186,7 +130,7 @@ export default class SendGasRow extends Component {
|
||||
>
|
||||
{this.renderContent()}
|
||||
</SendRowWrapper>
|
||||
{gasButtonGroupShown || advancedInlineGasShown ? (
|
||||
{gasInputMode === GAS_INPUT_MODES.BASIC || advancedInlineGasShown ? (
|
||||
<SendRowWrapper>{this.renderAdvancedOptionsButton()}</SendRowWrapper>
|
||||
) : null}
|
||||
</>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user