Merge pull request #11439 from MetaMask/Version-v9.8.0

Version v9.8.0 RC
feature/default_network_editable
ryanml 3 years ago committed by GitHub
commit c87dce9ce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      CHANGELOG.md
  2. 3
      app/_locales/am/messages.json
  3. 3
      app/_locales/ar/messages.json
  4. 3
      app/_locales/bg/messages.json
  5. 3
      app/_locales/bn/messages.json
  6. 3
      app/_locales/ca/messages.json
  7. 3
      app/_locales/cs/messages.json
  8. 3
      app/_locales/da/messages.json
  9. 3
      app/_locales/de/messages.json
  10. 3
      app/_locales/el/messages.json
  11. 19
      app/_locales/en/messages.json
  12. 3
      app/_locales/es/messages.json
  13. 3
      app/_locales/es_419/messages.json
  14. 3
      app/_locales/et/messages.json
  15. 3
      app/_locales/fa/messages.json
  16. 3
      app/_locales/fi/messages.json
  17. 3
      app/_locales/fil/messages.json
  18. 3
      app/_locales/fr/messages.json
  19. 3
      app/_locales/he/messages.json
  20. 3
      app/_locales/hi/messages.json
  21. 3
      app/_locales/hn/messages.json
  22. 3
      app/_locales/hr/messages.json
  23. 3
      app/_locales/ht/messages.json
  24. 3
      app/_locales/hu/messages.json
  25. 3
      app/_locales/id/messages.json
  26. 3
      app/_locales/it/messages.json
  27. 3
      app/_locales/ja/messages.json
  28. 3
      app/_locales/kn/messages.json
  29. 3
      app/_locales/ko/messages.json
  30. 3
      app/_locales/lt/messages.json
  31. 3
      app/_locales/lv/messages.json
  32. 3
      app/_locales/ms/messages.json
  33. 3
      app/_locales/nl/messages.json
  34. 3
      app/_locales/no/messages.json
  35. 3
      app/_locales/ph/messages.json
  36. 3
      app/_locales/pl/messages.json
  37. 3
      app/_locales/pt/messages.json
  38. 3
      app/_locales/pt_BR/messages.json
  39. 3
      app/_locales/ro/messages.json
  40. 3
      app/_locales/ru/messages.json
  41. 3
      app/_locales/sk/messages.json
  42. 3
      app/_locales/sl/messages.json
  43. 3
      app/_locales/sr/messages.json
  44. 3
      app/_locales/sv/messages.json
  45. 3
      app/_locales/sw/messages.json
  46. 3
      app/_locales/ta/messages.json
  47. 3
      app/_locales/th/messages.json
  48. 3
      app/_locales/tl/messages.json
  49. 3
      app/_locales/tr/messages.json
  50. 3
      app/_locales/uk/messages.json
  51. 3
      app/_locales/vi/messages.json
  52. 3
      app/_locales/zh_CN/messages.json
  53. 3
      app/_locales/zh_TW/messages.json
  54. 15
      app/scripts/controllers/detect-tokens.test.js
  55. 93
      app/scripts/controllers/preferences.js
  56. 143
      app/scripts/controllers/preferences.test.js
  57. 28
      app/scripts/controllers/swaps.js
  58. 19
      app/scripts/controllers/swaps.test.js
  59. 43
      app/scripts/controllers/threebox.js
  60. 10
      app/scripts/controllers/transactions/index.js
  61. 14
      app/scripts/lib/util.js
  62. 18
      app/scripts/metamask-controller.js
  63. 6
      jest.config.js
  64. 5
      package.json
  65. 8
      shared/constants/network.js
  66. 4
      shared/constants/swaps.js
  67. 18
      test/data/fetch-mocks.json
  68. 219
      test/e2e/tests/send-eth.spec.js
  69. 6
      test/e2e/webdriver/index.js
  70. 1
      test/jest/constants.js
  71. 1
      test/jest/mock-store.js
  72. 20
      test/jest/mocks.js
  73. 16
      ui/components/app/asset-list-item/asset-list-item.js
  74. 14
      ui/components/app/asset-list/asset-list.js
  75. 54
      ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js
  76. 78
      ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  77. 3
      ui/components/app/token-cell/token-cell.js
  78. 6
      ui/components/app/transaction-breakdown/transaction-breakdown.container.js
  79. 12
      ui/components/app/wallet-overview/token-overview.js
  80. 7
      ui/components/ui/currency-input/currency-input.container.js
  81. 9
      ui/components/ui/list-item/index.scss
  82. 6
      ui/components/ui/list-item/list-item.component.js
  83. 2
      ui/components/ui/token-input/token-input.component.test.js
  84. 10
      ui/components/ui/token-input/token-input.container.js
  85. 5
      ui/components/ui/unit-input/unit-input.component.js
  86. 14
      ui/contexts/metametrics.js
  87. 200
      ui/ducks/ens.js
  88. 14
      ui/ducks/gas/gas-action-constants.js
  89. 15
      ui/ducks/gas/gas-duck.test.js
  90. 16
      ui/ducks/gas/gas.duck.js
  91. 4
      ui/ducks/index.js
  92. 1
      ui/ducks/send/index.js
  93. 142
      ui/ducks/send/send-duck.test.js
  94. 382
      ui/ducks/send/send.duck.js
  95. 1508
      ui/ducks/send/send.js
  96. 1859
      ui/ducks/send/send.test.js
  97. 54
      ui/ducks/swaps/swaps.js
  98. 107
      ui/ducks/swaps/swaps.test.js
  99. 1
      ui/helpers/constants/error-keys.js
  100. 8
      ui/helpers/utils/conversion-util.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [9.8.0]
### Added
- [#11435](https://github.com/MetaMask/metamask-extension/pull/11435): Add gas limit buffers for optimism network
### Changed
- [#11210](https://github.com/MetaMask/metamask-extension/pull/11210): Disable sending ERC-721 assets (NFTs)
- [#11418](https://github.com/MetaMask/metamask-extension/pull/11418): Use network gas estimate for gas limits of simple sends on custom networks
## [9.7.1] ## [9.7.1]
### Fixed ### Fixed
- [#11426](https://github.com/MetaMask/metamask-extension/pull/11426): Fixed bug that broke transaction speed up and cancel, when attempting those actions immediately after opening MetaMask - [#11426](https://github.com/MetaMask/metamask-extension/pull/11426): Fixed bug that broke transaction speed up and cancel, when attempting those actions immediately after opening MetaMask
@ -2321,7 +2329,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized ### Uncategorized
- Added the ability to restore accounts from seed words. - Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.7.1...HEAD [Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.8.0...HEAD
[9.8.0]: https://github.com/MetaMask/metamask-extension/compare/v9.7.1...v9.8.0
[9.7.1]: https://github.com/MetaMask/metamask-extension/compare/v9.7.0...v9.7.1 [9.7.1]: https://github.com/MetaMask/metamask-extension/compare/v9.7.0...v9.7.1
[9.7.0]: https://github.com/MetaMask/metamask-extension/compare/v9.6.1...v9.7.0 [9.7.0]: https://github.com/MetaMask/metamask-extension/compare/v9.6.1...v9.7.0
[9.6.1]: https://github.com/MetaMask/metamask-extension/compare/v9.6.0...v9.6.1 [9.6.1]: https://github.com/MetaMask/metamask-extension/compare/v9.6.0...v9.6.1

@ -751,9 +751,6 @@
"recents": { "recents": {
"message": "የቅርብ ጊዜያት" "message": "የቅርብ ጊዜያት"
}, },
"recipientAddress": {
"message": "የተቀባይ አድራሻ"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "ፍለጋ፣ ለሕዝብ ክፍት የሆነ አድራሻ (0x), ወይም ENS" "message": "ፍለጋ፣ ለሕዝብ ክፍት የሆነ አድራሻ (0x), ወይም ENS"
}, },

@ -747,9 +747,6 @@
"recents": { "recents": {
"message": "الحديث" "message": "الحديث"
}, },
"recipientAddress": {
"message": "عنوان المستلم"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "البحث، العنوان العام (0x)، أو ENS" "message": "البحث، العنوان العام (0x)، أو ENS"
}, },

@ -750,9 +750,6 @@
"recents": { "recents": {
"message": "Скорошни" "message": "Скорошни"
}, },
"recipientAddress": {
"message": "Адрес на получателя"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Търсене, публичен адрес (0x) или ENS" "message": "Търсене, публичен адрес (0x) или ENS"
}, },

@ -754,9 +754,6 @@
"recents": { "recents": {
"message": "সরতিকগি" "message": "সরতিকগি"
}, },
"recipientAddress": {
"message": "পপকর ঠি"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "অনসনন, সবজনন ঠি (0x), ব ENS" "message": "অনসনন, সবজনন ঠি (0x), ব ENS"
}, },

@ -732,9 +732,6 @@
"readdToken": { "readdToken": {
"message": "Pots tornar a afegir aquesta fitxa en el futur anant a \"Afegir fitxa\" al menu d'opcions dels teus comptes." "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": { "recipientAddressPlaceholder": {
"message": "Cerca, adreça pública (0x), o ENS" "message": "Cerca, adreça pública (0x), o ENS"
}, },

@ -308,9 +308,6 @@
"readdToken": { "readdToken": {
"message": "Tento token můžete v budoucnu přidat zpět s „Přidat token“ v nastavení účtu." "message": "Tento token můžete v budoucnu přidat zpět s „Přidat token“ v nastavení účtu."
}, },
"recipientAddress": {
"message": "Adresa příjemce"
},
"reject": { "reject": {
"message": "Odmítnout" "message": "Odmítnout"
}, },

@ -735,9 +735,6 @@
"recents": { "recents": {
"message": "Seneste" "message": "Seneste"
}, },
"recipientAddress": {
"message": "Modtagerens adresse"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Søg, offentlig adresse (0x) eller ENS" "message": "Søg, offentlig adresse (0x) eller ENS"
}, },

@ -723,9 +723,6 @@
"recents": { "recents": {
"message": "Letzte" "message": "Letzte"
}, },
"recipientAddress": {
"message": "Empfängeradresse"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Suchen, öffentliche Adresse (0x) oder ENS" "message": "Suchen, öffentliche Adresse (0x) oder ENS"
}, },

@ -751,9 +751,6 @@
"recents": { "recents": {
"message": "Πρόσφατα" "message": "Πρόσφατα"
}, },
"recipientAddress": {
"message": "Διεύθυνση Παραλήπτη"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Αναζήτηση, δημόσια διεύθυνση (0x) ή ENS" "message": "Αναζήτηση, δημόσια διεύθυνση (0x) ή ENS"
}, },

@ -650,12 +650,21 @@
"message": "The endpoint returned a different chain ID: $1", "message": "The endpoint returned a different chain ID: $1",
"description": "$1 is the return value of eth_chainId from an RPC endpoint" "description": "$1 is the return value of eth_chainId from an RPC endpoint"
}, },
"ensIllegalCharacter": {
"message": "Illegal Character for ENS."
},
"ensNotFoundOnCurrentNetwork": { "ensNotFoundOnCurrentNetwork": {
"message": "ENS name not found on the current network. Try switching to Ethereum Mainnet." "message": "ENS name not found on the current network. Try switching to Ethereum Mainnet."
}, },
"ensNotSupportedOnNetwork": {
"message": "Network does not support ENS"
},
"ensRegistrationError": { "ensRegistrationError": {
"message": "Error in ENS name registration" "message": "Error in ENS name registration"
}, },
"ensUnknownError": {
"message": "ENS Lookup failed."
},
"enterAnAlias": { "enterAnAlias": {
"message": "Enter an alias" "message": "Enter an alias"
}, },
@ -1174,6 +1183,9 @@
"networkNameEthereum": { "networkNameEthereum": {
"message": "Ethereum" "message": "Ethereum"
}, },
"networkNamePolygon": {
"message": "Polygon"
},
"networkNameTestnet": { "networkNameTestnet": {
"message": "Testnet" "message": "Testnet"
}, },
@ -1451,9 +1463,6 @@
"recents": { "recents": {
"message": "Recents" "message": "Recents"
}, },
"recipientAddress": {
"message": "Recipient Address"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Search, public address (0x), or ENS" "message": "Search, public address (0x), or ENS"
}, },
@ -2369,6 +2378,10 @@
"message": "verify the network details", "message": "verify the network details",
"description": "Serves as link text for the 'unrecognizedChain' key. This text will be embedded inside the translation for that key." "description": "Serves as link text for the 'unrecognizedChain' key. This text will be embedded inside the translation for that key."
}, },
"unsendableAsset": {
"message": "Sending collectible (ERC-721) tokens is not currently supported",
"description": "This is an error message we show the user if they attempt to send a collectible asset type, for which currently don't support sending"
},
"updatedWithDate": { "updatedWithDate": {
"message": "Updated $1" "message": "Updated $1"
}, },

@ -1411,9 +1411,6 @@
"recents": { "recents": {
"message": "Recientes" "message": "Recientes"
}, },
"recipientAddress": {
"message": "Dirección del destinatario"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Búsqueda, dirección pública (0x) o ENS" "message": "Búsqueda, dirección pública (0x) o ENS"
}, },

@ -1419,9 +1419,6 @@
"recents": { "recents": {
"message": "Recientes" "message": "Recientes"
}, },
"recipientAddress": {
"message": "Dirección del destinatario"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Búsqueda, dirección pública (0x) o ENS" "message": "Búsqueda, dirección pública (0x) o ENS"
}, },

@ -744,9 +744,6 @@
"recents": { "recents": {
"message": "Hiljutised" "message": "Hiljutised"
}, },
"recipientAddress": {
"message": "Saaja aadress"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Otsing, avalik aadress (0x) või ENS" "message": "Otsing, avalik aadress (0x) või ENS"
}, },

@ -754,9 +754,6 @@
"recents": { "recents": {
"message": "واپسین" "message": "واپسین"
}, },
"recipientAddress": {
"message": "آدرس دریافت کننده"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "جستجو، آدرس عمومی (0x)، یا ENS" "message": "جستجو، آدرس عمومی (0x)، یا ENS"
}, },

@ -751,9 +751,6 @@
"recents": { "recents": {
"message": "Viimeaikaiset" "message": "Viimeaikaiset"
}, },
"recipientAddress": {
"message": "Vastaanottajan osoite"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Haku, julkinen osoite (0x) tai ENS" "message": "Haku, julkinen osoite (0x) tai ENS"
}, },

@ -678,9 +678,6 @@
"recents": { "recents": {
"message": "Kamakailan" "message": "Kamakailan"
}, },
"recipientAddress": {
"message": "Address ng Recipient"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Maghanap, pampublikong address (0x), o ENS" "message": "Maghanap, pampublikong address (0x), o ENS"
}, },

@ -736,9 +736,6 @@
"recents": { "recents": {
"message": "Récents" "message": "Récents"
}, },
"recipientAddress": {
"message": "Adresse du destinataire"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Recherche, adresse publique (0x) ou ENS" "message": "Recherche, adresse publique (0x) ou ENS"
}, },

@ -751,9 +751,6 @@
"recents": { "recents": {
"message": "אחרונים" "message": "אחרונים"
}, },
"recipientAddress": {
"message": "כתובת הנמען"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "חיפוש, כתובת ציבורית (0x), או ENS" "message": "חיפוש, כתובת ציבורית (0x), או ENS"
}, },

@ -1411,9 +1411,6 @@
"recents": { "recents": {
"message": "हल ह" "message": "हल ह"
}, },
"recipientAddress": {
"message": "पतकर पत"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "खज, सवजनिक पत (0x) य ENS" "message": "खज, सवजनिक पत (0x) य ENS"
}, },

@ -285,9 +285,6 @@
"readdToken": { "readdToken": {
"message": "आप अपनिकलप म .टकन ज. पर जकर भविय म इस टकन कपस ज सकत।" "message": "आप अपनिकलप म .टकन ज. पर जकर भविय म इस टकन कपस ज सकत।"
}, },
"recipientAddress": {
"message": "पतकर पत"
},
"reject": { "reject": {
"message": "असर" "message": "असर"
}, },

@ -747,9 +747,6 @@
"recents": { "recents": {
"message": "Nedavno" "message": "Nedavno"
}, },
"recipientAddress": {
"message": "Adresa primatelja"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Pretraži, javne adrese (0x) ili ENS" "message": "Pretraži, javne adrese (0x) ili ENS"
}, },

@ -450,9 +450,6 @@
"readdToken": { "readdToken": {
"message": "Ou ka ajoute token sa aprè sa ankò ou prale nan \"Ajoute token\" nan opsyon meni kont ou an." "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": { "reject": {
"message": "Rejte" "message": "Rejte"
}, },

@ -747,9 +747,6 @@
"recents": { "recents": {
"message": "Legutóbbiak" "message": "Legutóbbiak"
}, },
"recipientAddress": {
"message": "Címzett címe"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Keresés, nyilvános cím (0x) vagy ENS" "message": "Keresés, nyilvános cím (0x) vagy ENS"
}, },

@ -1411,9 +1411,6 @@
"recents": { "recents": {
"message": "Terkini" "message": "Terkini"
}, },
"recipientAddress": {
"message": "Alamat Penerima"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Cari, alamat publik (0x), atau ENS" "message": "Cari, alamat publik (0x), atau ENS"
}, },

@ -1201,9 +1201,6 @@
"recents": { "recents": {
"message": "Recenti" "message": "Recenti"
}, },
"recipientAddress": {
"message": "Indirizzo Destinatario"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Ricerca, indirizzo pubblico (0x) o ENS" "message": "Ricerca, indirizzo pubblico (0x) o ENS"
}, },

@ -1411,9 +1411,6 @@
"recents": { "recents": {
"message": "最近" "message": "最近"
}, },
"recipientAddress": {
"message": "受信者のアドレス"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "検索、パブリック アドレス (0x)、または ENS" "message": "検索、パブリック アドレス (0x)、または ENS"
}, },

@ -754,9 +754,6 @@
"recents": { "recents": {
"message": "ಇತಿನವಗಳ" "message": "ಇತಿನವಗಳ"
}, },
"recipientAddress": {
"message": "ಸಕರಿವವರ ವಿಸ"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "ಸವಜನಿಕ ವಿಸ (0x) ಅಥವ ENS ಹಿ" "message": "ಸವಜನಿಕ ವಿಸ (0x) ಅಥವ ENS ಹಿ"
}, },

@ -1415,9 +1415,6 @@
"recents": { "recents": {
"message": "최근" "message": "최근"
}, },
"recipientAddress": {
"message": "수신인 주소"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "검색, 공개 주소(0x) 또는 ENS" "message": "검색, 공개 주소(0x) 또는 ENS"
}, },

@ -754,9 +754,6 @@
"recents": { "recents": {
"message": "Naujausi" "message": "Naujausi"
}, },
"recipientAddress": {
"message": "Gavėjo adresas"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Ieška, viešieji adresai (0x) arba ENS" "message": "Ieška, viešieji adresai (0x) arba ENS"
}, },

@ -750,9 +750,6 @@
"recents": { "recents": {
"message": "Nesenie" "message": "Nesenie"
}, },
"recipientAddress": {
"message": "Saņēmēja adrese"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Meklēšana, publiskā adrese (0x) vai ENS" "message": "Meklēšana, publiskā adrese (0x) vai ENS"
}, },

@ -731,9 +731,6 @@
"recents": { "recents": {
"message": "Baru-baru ini" "message": "Baru-baru ini"
}, },
"recipientAddress": {
"message": "Alamat Penerima"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Cari, alamat awam (0x), atau ENS" "message": "Cari, alamat awam (0x), atau ENS"
}, },

@ -272,9 +272,6 @@
"readdToken": { "readdToken": {
"message": "U kunt dit token in de toekomst weer toevoegen door naar \"Token toevoegen\" te gaan in het menu met accountopties." "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": { "reject": {
"message": "Afwijzen" "message": "Afwijzen"
}, },

@ -741,9 +741,6 @@
"recents": { "recents": {
"message": "Nylige" "message": "Nylige"
}, },
"recipientAddress": {
"message": "Mottakeradresse"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Søk, offentlig adresse (0x) eller ENS" "message": "Søk, offentlig adresse (0x) eller ENS"
}, },

@ -1419,9 +1419,6 @@
"recents": { "recents": {
"message": "Mga Kamakailan" "message": "Mga Kamakailan"
}, },
"recipientAddress": {
"message": "Address ng Tatanggap"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Maghanap, pampublikong address (0x), o ENS" "message": "Maghanap, pampublikong address (0x), o ENS"
}, },

@ -748,9 +748,6 @@
"recents": { "recents": {
"message": "Ostatnie" "message": "Ostatnie"
}, },
"recipientAddress": {
"message": "Adres odbiorcy"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Szukaj, adres publiczny (0x) lub ENS" "message": "Szukaj, adres publiczny (0x) lub ENS"
}, },

@ -282,9 +282,6 @@
"readdToken": { "readdToken": {
"message": "Pode adicionar este token de novo clicando na opção “Adicionar token” no menu de opções da sua conta." "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": { "reject": {
"message": "Rejeitar" "message": "Rejeitar"
}, },

@ -1405,9 +1405,6 @@
"recents": { "recents": {
"message": "Recentes" "message": "Recentes"
}, },
"recipientAddress": {
"message": "Endereço do destinatário"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Busca, endereço público (0x) ou ENS" "message": "Busca, endereço público (0x) ou ENS"
}, },

@ -741,9 +741,6 @@
"recents": { "recents": {
"message": "Recente" "message": "Recente"
}, },
"recipientAddress": {
"message": "Adresă destinatar"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Căutare, adresa publică (0x) sau ENS" "message": "Căutare, adresa publică (0x) sau ENS"
}, },

@ -1411,9 +1411,6 @@
"recents": { "recents": {
"message": "Недавние" "message": "Недавние"
}, },
"recipientAddress": {
"message": "Адрес получателя"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Поиск, публичный адрес (0x) или ENS" "message": "Поиск, публичный адрес (0x) или ENS"
}, },

@ -723,9 +723,6 @@
"recents": { "recents": {
"message": "Posledné" "message": "Posledné"
}, },
"recipientAddress": {
"message": "Adresa příjemce"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Vyhľadávať verejnú adresu (0x) alebo ENS" "message": "Vyhľadávať verejnú adresu (0x) alebo ENS"
}, },

@ -742,9 +742,6 @@
"recents": { "recents": {
"message": "Nedavno" "message": "Nedavno"
}, },
"recipientAddress": {
"message": "Prejemnikov naslov"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Iskanje, javni naslov (0x) ali ENS" "message": "Iskanje, javni naslov (0x) ali ENS"
}, },

@ -745,9 +745,6 @@
"recents": { "recents": {
"message": "Skorašnje" "message": "Skorašnje"
}, },
"recipientAddress": {
"message": "Adresa primaoca"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Pretraga, javna adresa (0x) ili ENS" "message": "Pretraga, javna adresa (0x) ili ENS"
}, },

@ -738,9 +738,6 @@
"recents": { "recents": {
"message": "Senaste" "message": "Senaste"
}, },
"recipientAddress": {
"message": "Mottagaradress"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Sök, allmän adress (0x) eller ENS" "message": "Sök, allmän adress (0x) eller ENS"
}, },

@ -732,9 +732,6 @@
"recents": { "recents": {
"message": "Za hivi karibuni" "message": "Za hivi karibuni"
}, },
"recipientAddress": {
"message": "Anwani ya Mpokeaji"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Tafuta, anwani za umma (0x), au ENS" "message": "Tafuta, anwani za umma (0x), au ENS"
}, },

@ -372,9 +372,6 @@
"readdToken": { "readdToken": {
"message": "உஙகள கணகிபஙகளி \"டகன\" எனபதனலமகள எதிலதி இநத டகனகல." "message": "உஙகள கணகிபஙகளி \"டகன\" எனபதனலமகள எதிலதி இநத டகனகல."
}, },
"recipientAddress": {
"message": "பநரகவரி"
},
"reject": { "reject": {
"message": "நிகரி" "message": "நிகரி"
}, },

@ -375,9 +375,6 @@
"readdToken": { "readdToken": {
"message": "คณสามารถเพมโทเคนนในอนาคตไดโดยไปท “เพมโทเคน” ในเมนวเลอกบญชของคณ" "message": "คณสามารถเพมโทเคนนในอนาคตไดโดยไปท “เพมโทเคน” ในเมนวเลอกบญชของคณ"
}, },
"recipientAddress": {
"message": "แอดแดรสผบ"
},
"reject": { "reject": {
"message": "ปฏเสธ" "message": "ปฏเสธ"
}, },

@ -1192,9 +1192,6 @@
"recents": { "recents": {
"message": "Mga Kamakailan" "message": "Mga Kamakailan"
}, },
"recipientAddress": {
"message": "Address ng Tatanggap"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Maghanap, pampublikong address (0x), o ENS" "message": "Maghanap, pampublikong address (0x), o ENS"
}, },

@ -324,9 +324,6 @@
"readdToken": { "readdToken": {
"message": "Gelecekte Bu jetonu hesap seçenekleri menüsünde “Jeton ekle”'ye giderek geri ekleyebilirsiniz." "message": "Gelecekte Bu jetonu hesap seçenekleri menüsünde “Jeton ekle”'ye giderek geri ekleyebilirsiniz."
}, },
"recipientAddress": {
"message": "Alıcı adresi"
},
"reject": { "reject": {
"message": "Reddetmek" "message": "Reddetmek"
}, },

@ -754,9 +754,6 @@
"recents": { "recents": {
"message": "Останні" "message": "Останні"
}, },
"recipientAddress": {
"message": "Адреса отримувача"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Пошук, публічна адреса (0x), або ENS" "message": "Пошук, публічна адреса (0x), або ENS"
}, },

@ -1411,9 +1411,6 @@
"recents": { "recents": {
"message": "Gần đây" "message": "Gần đây"
}, },
"recipientAddress": {
"message": "Địa chỉ người nhận"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Tìm kiếm, địa chỉ công khai (0x) hoặc ENS" "message": "Tìm kiếm, địa chỉ công khai (0x) hoặc ENS"
}, },

@ -1195,9 +1195,6 @@
"recents": { "recents": {
"message": "最近记录" "message": "最近记录"
}, },
"recipientAddress": {
"message": "接收地址"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "查找、公用地址 (0x) 或 ENS" "message": "查找、公用地址 (0x) 或 ENS"
}, },

@ -751,9 +751,6 @@
"recents": { "recents": {
"message": "最近" "message": "最近"
}, },
"recipientAddress": {
"message": "接收位址"
},
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "搜尋,公開地址 (0x),或 ENS" "message": "搜尋,公開地址 (0x),或 ENS"
}, },

@ -11,7 +11,7 @@ import PreferencesController from './preferences';
describe('DetectTokensController', function () { describe('DetectTokensController', function () {
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
let keyringMemStore, network, preferences; let keyringMemStore, network, preferences, provider;
const noop = () => undefined; const noop = () => undefined;
@ -23,12 +23,16 @@ describe('DetectTokensController', function () {
keyringMemStore = new ObservableStore({ isUnlocked: false }); keyringMemStore = new ObservableStore({ isUnlocked: false });
network = new NetworkController(); network = new NetworkController();
network.setInfuraProjectId('foo'); network.setInfuraProjectId('foo');
preferences = new PreferencesController({ network }); network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
preferences = new PreferencesController({ network, provider });
preferences.setAddresses([ preferences.setAddresses([
'0x7e57e2', '0x7e57e2',
'0xbc86727e770de68b1060c91f6bb6945c73e10388', '0xbc86727e770de68b1060c91f6bb6945c73e10388',
]); ]);
network.initializeProvider(networkControllerProviderConfig); sandbox
.stub(preferences, '_detectIsERC721')
.returns(Promise.resolve(false));
}); });
after(function () { after(function () {
@ -125,6 +129,7 @@ describe('DetectTokensController', function () {
address: existingTokenAddress.toLowerCase(), address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals, decimals: existingToken.decimals,
symbol: existingToken.symbol, symbol: existingToken.symbol,
isERC721: false,
}, },
]); ]);
}); });
@ -177,11 +182,13 @@ describe('DetectTokensController', function () {
address: existingTokenAddress.toLowerCase(), address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals, decimals: existingToken.decimals,
symbol: existingToken.symbol, symbol: existingToken.symbol,
isERC721: false,
}, },
{ {
address: tokenAddressToAdd.toLowerCase(), address: tokenAddressToAdd.toLowerCase(),
decimals: tokenToAdd.decimals, decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol, symbol: tokenToAdd.symbol,
isERC721: false,
}, },
]); ]);
}); });
@ -234,11 +241,13 @@ describe('DetectTokensController', function () {
address: existingTokenAddress.toLowerCase(), address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals, decimals: existingToken.decimals,
symbol: existingToken.symbol, symbol: existingToken.symbol,
isERC721: false,
}, },
{ {
address: tokenAddressToAdd.toLowerCase(), address: tokenAddressToAdd.toLowerCase(),
decimals: tokenToAdd.decimals, decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol, symbol: tokenToAdd.symbol,
isERC721: false,
}, },
]); ]);
}); });

@ -2,14 +2,21 @@ import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors'; import { ethErrors } from 'eth-rpc-errors';
import { normalize as normalizeAddress } from 'eth-sig-util'; import { normalize as normalizeAddress } from 'eth-sig-util';
import ethers from 'ethers'; import { ethers } from 'ethers';
import log from 'loglevel'; import log from 'loglevel';
import abiERC721 from 'human-standard-collectible-abi';
import contractsMap from '@metamask/contract-metadata';
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens'; import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'; import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; import {
isValidHexAddress,
toChecksumHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { NETWORK_EVENTS } from './network'; import { NETWORK_EVENTS } from './network';
const ERC721METADATA_INTERFACE_ID = '0x5b5e139f';
export default class PreferencesController { export default class PreferencesController {
/** /**
* *
@ -73,11 +80,18 @@ export default class PreferencesController {
}; };
this.network = opts.network; this.network = opts.network;
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
this.store = new ObservableStore(initState); this.store = new ObservableStore(initState);
this.store.setMaxListeners(12); this.store.setMaxListeners(12);
this.openPopup = opts.openPopup; this.openPopup = opts.openPopup;
this.migrateAddressBookState = opts.migrateAddressBookState; this.migrateAddressBookState = opts.migrateAddressBookState;
this._subscribeToNetworkDidChange();
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
this._subscribeToInfuraAvailability(); this._subscribeToInfuraAvailability();
global.setPreference = (key, value) => { global.setPreference = (key, value) => {
@ -393,6 +407,8 @@ export default class PreferencesController {
}); });
const previousIndex = tokens.indexOf(previousEntry); const previousIndex = tokens.indexOf(previousEntry);
newEntry.isERC721 = await this._detectIsERC721(newEntry.address);
if (previousEntry) { if (previousEntry) {
tokens[previousIndex] = newEntry; tokens[previousIndex] = newEntry;
} else { } else {
@ -403,6 +419,24 @@ export default class PreferencesController {
return Promise.resolve(tokens); return Promise.resolve(tokens);
} }
/**
* Adds isERC721 field to token object
* (Called when a user attempts to add tokens that were previously added which do not yet had isERC721 field)
*
* @param {string} tokenAddress - The contract address of the token requiring the isERC721 field added.
* @returns {Promise<object>} The new token object with the added isERC721 field.
*
*/
async updateTokenType(tokenAddress) {
const { tokens } = this.store.getState();
const tokenIndex = tokens.findIndex((token) => {
return token.address === tokenAddress;
});
tokens[tokenIndex].isERC721 = await this._detectIsERC721(tokenAddress);
this.store.updateState({ tokens });
return Promise.resolve(tokens[tokenIndex]);
}
/** /**
* Removes a specified token from the tokens array and adds it to hiddenTokens array * Removes a specified token from the tokens array and adds it to hiddenTokens array
* *
@ -480,11 +514,8 @@ export default class PreferencesController {
let addressBookKey = rpcDetail.chainId; let addressBookKey = rpcDetail.chainId;
if (!addressBookKey) { if (!addressBookKey) {
// We need to find the networkId to determine what these addresses were keyed by // We need to find the networkId to determine what these addresses were keyed by
const provider = new ethers.providers.JsonRpcProvider(
rpcDetail.rpcUrl,
);
try { try {
addressBookKey = await provider.send('net_version'); addressBookKey = await this.ethersProvider.send('net_version');
assert(typeof addressBookKey === 'string'); assert(typeof addressBookKey === 'string');
} catch (error) { } catch (error) {
log.debug(error); log.debug(error);
@ -701,17 +732,6 @@ export default class PreferencesController {
// PRIVATE METHODS // PRIVATE METHODS
// //
/**
* Handle updating token list to reflect current network by listening for the
* NETWORK_DID_CHANGE event.
*/
_subscribeToNetworkDidChange() {
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
}
_subscribeToInfuraAvailability() { _subscribeToInfuraAvailability() {
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => { this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
this._setInfuraBlocked(true); this._setInfuraBlocked(true);
@ -763,6 +783,43 @@ export default class PreferencesController {
}); });
} }
/**
* Detects whether or not a token is ERC-721 compatible.
*
* @param {string} tokensAddress - the token contract address.
*
*/
async _detectIsERC721(tokenAddress) {
const checksumAddress = toChecksumHexAddress(tokenAddress);
// if this token is already in our contract metadata map we don't need
// to check against the contract
if (contractsMap[checksumAddress]?.erc721 === true) {
return Promise.resolve(true);
}
const tokenContract = await this._createEthersContract(
tokenAddress,
abiERC721,
this.ethersProvider,
);
return await tokenContract
.supportsInterface(ERC721METADATA_INTERFACE_ID)
.catch((error) => {
console.log('error', error);
log.debug(error);
return false;
});
}
async _createEthersContract(tokenAddress, abi, ethersProvider) {
const tokenContract = await new ethers.Contract(
tokenAddress,
abi,
ethersProvider,
);
return tokenContract;
}
/** /**
* Updates `tokens` and `hiddenTokens` of current account and network. * Updates `tokens` and `hiddenTokens` of current account and network.
* *

@ -1,10 +1,13 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import contractMaps from '@metamask/contract-metadata';
import abiERC721 from 'human-standard-collectible-abi';
import { import {
MAINNET_CHAIN_ID, MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID, RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network'; } from '../../../shared/constants/network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
import NetworkController from './network';
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController; let preferencesController;
@ -13,19 +16,32 @@ describe('preferences controller', function () {
let triggerNetworkChange; let triggerNetworkChange;
let switchToMainnet; let switchToMainnet;
let switchToRinkeby; let switchToRinkeby;
let provider;
const migrateAddressBookState = sinon.stub(); const migrateAddressBookState = sinon.stub();
beforeEach(function () { beforeEach(function () {
const sandbox = sinon.createSandbox();
currentChainId = MAINNET_CHAIN_ID; currentChainId = MAINNET_CHAIN_ID;
network = { const networkControllerProviderConfig = {
getCurrentChainId: () => currentChainId, getAccounts: () => undefined,
on: sinon.spy(),
}; };
network = new NetworkController();
network.setInfuraProjectId('foo');
network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
sandbox.stub(network, 'getCurrentChainId').callsFake(() => currentChainId);
sandbox
.stub(network, 'getProviderConfig')
.callsFake(() => ({ type: 'mainnet' }));
const spy = sandbox.spy(network, 'on');
preferencesController = new PreferencesController({ preferencesController = new PreferencesController({
migrateAddressBookState, migrateAddressBookState,
network, network,
provider,
}); });
triggerNetworkChange = network.on.firstCall.args[1]; triggerNetworkChange = spy.firstCall.args[1];
switchToMainnet = () => { switchToMainnet = () => {
currentChainId = MAINNET_CHAIN_ID; currentChainId = MAINNET_CHAIN_ID;
triggerNetworkChange(); triggerNetworkChange();
@ -86,6 +102,104 @@ describe('preferences controller', function () {
}); });
}); });
describe('updateTokenType', function () {
it('should add isERC721 = true to token object in state when token is collectible and in our contract-metadata repo', async function () {
const contractAddresses = Object.keys(contractMaps);
const erc721ContractAddresses = contractAddresses.filter(
(contractAddress) => contractMaps[contractAddress].erc721 === true,
);
const address = erc721ContractAddresses[0];
const { symbol, decimals } = contractMaps[address];
preferencesController.store.updateState({
tokens: [{ address, symbol, decimals }],
});
const result = await preferencesController.updateTokenType(address);
assert.equal(result.isERC721, true);
});
it('should add isERC721 = true to token object in state when token is collectible and not in our contract-metadata repo', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
preferencesController.store.updateState({
tokens: [
{
address: tokenAddress,
symbol: 'TESTNFT',
decimals: '0',
},
],
});
sinon
.stub(preferencesController, '_detectIsERC721')
.callsFake(() => true);
const result = await preferencesController.updateTokenType(tokenAddress);
assert.equal(
preferencesController._detectIsERC721.getCall(0).args[0],
tokenAddress,
);
assert.equal(result.isERC721, true);
});
});
describe('_detectIsERC721', function () {
it('should return true when token is in our contract-metadata repo', async function () {
const tokenAddress = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d';
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(result, true);
});
it('should return true when the token is not in our contract-metadata repo but tokenContract.supportsInterface returns true', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
const supportsInterfaceStub = sinon.stub().returns(Promise.resolve(true));
sinon
.stub(preferencesController, '_createEthersContract')
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[0],
tokenAddress,
);
assert.deepEqual(
preferencesController._createEthersContract.getCall(0).args[1],
abiERC721,
);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[2],
preferencesController.ethersProvider,
);
assert.equal(result, true);
});
it('should return false when the token is not in our contract-metadata repo and tokenContract.supportsInterface returns false', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
const supportsInterfaceStub = sinon
.stub()
.returns(Promise.resolve(false));
sinon
.stub(preferencesController, '_createEthersContract')
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[0],
tokenAddress,
);
assert.deepEqual(
preferencesController._createEthersContract.getCall(0).args[1],
abiERC721,
);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[2],
preferencesController.ethersProvider,
);
assert.equal(result, false);
});
});
describe('removeAddress', function () { describe('removeAddress', function () {
it('should remove an address from state', function () { it('should remove an address from state', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
@ -291,7 +405,12 @@ describe('preferences controller', function () {
assert.equal(tokens.length, 1, 'one token removed'); assert.equal(tokens.length, 1, 'one token removed');
const [token1] = tokens; const [token1] = tokens;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 }); assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
}); });
it('should remove a token from its state on corresponding address', async function () { it('should remove a token from its state on corresponding address', async function () {
@ -310,7 +429,12 @@ describe('preferences controller', function () {
assert.equal(tokensFirst.length, 1, 'one token removed in account'); assert.equal(tokensFirst.length, 1, 'one token removed in account');
const [token1] = tokensFirst; const [token1] = tokensFirst;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 }); assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
await preferencesController.setSelectedAddress('0x7e57e3'); await preferencesController.setSelectedAddress('0x7e57e3');
const tokensSecond = preferencesController.getTokens(); const tokensSecond = preferencesController.getTokens();
@ -335,7 +459,12 @@ describe('preferences controller', function () {
assert.equal(tokensFirst.length, 1, 'one token removed in network'); assert.equal(tokensFirst.length, 1, 'one token removed in network');
const [token1] = tokensFirst; const [token1] = tokensFirst;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 }); assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
switchToRinkeby(); switchToRinkeby();
const tokensSecond = preferencesController.getTokens(); const tokensSecond = preferencesController.getTokens();

@ -19,7 +19,6 @@ import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils'
import { import {
fetchTradesInfo as defaultFetchTradesInfo, fetchTradesInfo as defaultFetchTradesInfo,
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime, fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime,
} from '../../../ui/pages/swaps/swaps.util'; } from '../../../ui/pages/swaps/swaps.util';
import { MINUTE, SECOND } from '../../../shared/constants/time'; import { MINUTE, SECOND } from '../../../shared/constants/time';
@ -73,6 +72,7 @@ const initialState = {
topAggId: null, topAggId: null,
routeState: '', routeState: '',
swapsFeatureIsLive: true, swapsFeatureIsLive: true,
useNewSwapsApi: false,
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
}, },
}; };
@ -85,7 +85,6 @@ export default class SwapsController {
getProviderConfig, getProviderConfig,
tokenRatesStore, tokenRatesStore,
fetchTradesInfo = defaultFetchTradesInfo, fetchTradesInfo = defaultFetchTradesInfo,
fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime,
getCurrentChainId, getCurrentChainId,
}) { }) {
@ -94,7 +93,6 @@ export default class SwapsController {
}); });
this._fetchTradesInfo = fetchTradesInfo; this._fetchTradesInfo = fetchTradesInfo;
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness;
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime; this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime;
this._getCurrentChainId = getCurrentChainId; this._getCurrentChainId = getCurrentChainId;
@ -119,15 +117,19 @@ export default class SwapsController {
// Sets the refresh rate for quote updates from the MetaSwap API // Sets the refresh rate for quote updates from the MetaSwap API
async _setSwapsQuoteRefreshTime() { async _setSwapsQuoteRefreshTime() {
const chainId = this._getCurrentChainId(); const chainId = this._getCurrentChainId();
const { swapsState } = this.store.getState();
// Default to fallback time unless API returns valid response // Default to fallback time unless API returns valid response
let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME; let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME;
try { try {
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(chainId); swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(
chainId,
swapsState.useNewSwapsApi,
);
} catch (e) { } catch (e) {
console.error('Request for swaps quote refresh time failed: ', e); console.error('Request for swaps quote refresh time failed: ', e);
} }
const { swapsState } = this.store.getState();
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, swapsQuoteRefreshTime }, swapsState: { ...swapsState, swapsQuoteRefreshTime },
}); });
@ -162,6 +164,9 @@ export default class SwapsController {
isPolledRequest, isPolledRequest,
) { ) {
const { chainId } = fetchParamsMetaData; const { chainId } = fetchParamsMetaData;
const {
swapsState: { useNewSwapsApi },
} = this.store.getState();
if (!fetchParams) { if (!fetchParams) {
return null; return null;
@ -182,7 +187,10 @@ export default class SwapsController {
this.indexOfNewestCallInFlight = indexOfCurrentCall; this.indexOfNewestCallInFlight = indexOfCurrentCall;
let [newQuotes] = await Promise.all([ let [newQuotes] = await Promise.all([
this._fetchTradesInfo(fetchParams, fetchParamsMetaData), this._fetchTradesInfo(fetchParams, {
...fetchParamsMetaData,
useNewSwapsApi,
}),
this._setSwapsQuoteRefreshTime(), this._setSwapsQuoteRefreshTime(),
]); ]);
@ -449,22 +457,23 @@ export default class SwapsController {
this.store.updateState({ swapsState: { ...swapsState, routeState } }); this.store.updateState({ swapsState: { ...swapsState, routeState } });
} }
setSwapsLiveness(swapsFeatureIsLive) { setSwapsLiveness(swapsLiveness) {
const { swapsState } = this.store.getState(); const { swapsState } = this.store.getState();
const { swapsFeatureIsLive, useNewSwapsApi } = swapsLiveness;
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, swapsFeatureIsLive }, swapsState: { ...swapsState, swapsFeatureIsLive, useNewSwapsApi },
}); });
} }
resetPostFetchState() { resetPostFetchState() {
const { swapsState } = this.store.getState(); const { swapsState } = this.store.getState();
this.store.updateState({ this.store.updateState({
swapsState: { swapsState: {
...initialState.swapsState, ...initialState.swapsState,
tokens: swapsState.tokens, tokens: swapsState.tokens,
fetchParams: swapsState.fetchParams, fetchParams: swapsState.fetchParams,
swapsFeatureIsLive: swapsState.swapsFeatureIsLive, swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
useNewSwapsApi: swapsState.useNewSwapsApi,
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
}, },
}); });
@ -473,7 +482,6 @@ export default class SwapsController {
resetSwapsState() { resetSwapsState() {
const { swapsState } = this.store.getState(); const { swapsState } = this.store.getState();
this.store.updateState({ this.store.updateState({
swapsState: { swapsState: {
...initialState.swapsState, ...initialState.swapsState,

@ -128,13 +128,13 @@ const EMPTY_INIT_STATE = {
topAggId: null, topAggId: null,
routeState: '', routeState: '',
swapsFeatureIsLive: true, swapsFeatureIsLive: true,
useNewSwapsApi: false,
swapsQuoteRefreshTime: 60000, swapsQuoteRefreshTime: 60000,
}, },
}; };
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
const fetchTradesInfoStub = sandbox.stub(); const fetchTradesInfoStub = sandbox.stub();
const fetchSwapsFeatureLivenessStub = sandbox.stub();
const fetchSwapsQuoteRefreshTimeStub = sandbox.stub(); const fetchSwapsQuoteRefreshTimeStub = sandbox.stub();
const getCurrentChainIdStub = sandbox.stub(); const getCurrentChainIdStub = sandbox.stub();
getCurrentChainIdStub.returns(MAINNET_CHAIN_ID); getCurrentChainIdStub.returns(MAINNET_CHAIN_ID);
@ -150,7 +150,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
@ -201,7 +200,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
const currentEthersInstance = swapsController.ethersProvider; const currentEthersInstance = swapsController.ethersProvider;
@ -226,7 +224,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
const currentEthersInstance = swapsController.ethersProvider; const currentEthersInstance = swapsController.ethersProvider;
@ -251,7 +248,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
const currentEthersInstance = swapsController.ethersProvider; const currentEthersInstance = swapsController.ethersProvider;
@ -658,6 +654,7 @@ describe('SwapsController', function () {
const quotes = await swapsController.fetchAndSetQuotes(undefined); const quotes = await swapsController.fetchAndSetQuotes(undefined);
assert.strictEqual(quotes, null); assert.strictEqual(quotes, null);
}); });
it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () { it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () {
fetchTradesInfoStub.resolves(getMockQuotes()); fetchTradesInfoStub.resolves(getMockQuotes());
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime());
@ -695,15 +692,15 @@ describe('SwapsController', function () {
metaMaskFeeInEth: '0.5050505050505050505', metaMaskFeeInEth: '0.5050505050505050505',
ethValueOfTokens: '50', ethValueOfTokens: '50',
}); });
assert.strictEqual( assert.strictEqual(
fetchTradesInfoStub.calledOnceWithExactly( fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS, {
MOCK_FETCH_PARAMS, ...MOCK_FETCH_METADATA,
MOCK_FETCH_METADATA, useNewSwapsApi: false,
), }),
true, true,
); );
}); });
it('performs the allowance check', async function () { it('performs the allowance check', async function () {
fetchTradesInfoStub.resolves(getMockQuotes()); fetchTradesInfoStub.resolves(getMockQuotes());
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime());
@ -878,12 +875,14 @@ describe('SwapsController', function () {
const tokens = 'test'; const tokens = 'test';
const fetchParams = 'test'; const fetchParams = 'test';
const swapsFeatureIsLive = false; const swapsFeatureIsLive = false;
const useNewSwapsApi = false;
const swapsQuoteRefreshTime = 0; const swapsQuoteRefreshTime = 0;
swapsController.store.updateState({ swapsController.store.updateState({
swapsState: { swapsState: {
tokens, tokens,
fetchParams, fetchParams,
swapsFeatureIsLive, swapsFeatureIsLive,
useNewSwapsApi,
swapsQuoteRefreshTime, swapsQuoteRefreshTime,
}, },
}); });

@ -25,6 +25,7 @@ export default class ThreeBoxController {
addressBookController, addressBookController,
version, version,
getKeyringControllerState, getKeyringControllerState,
trackMetaMetricsEvent,
} = opts; } = opts;
this.preferencesController = preferencesController; this.preferencesController = preferencesController;
@ -59,6 +60,7 @@ export default class ThreeBoxController {
); );
}, },
}); });
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
const initState = { const initState = {
threeBoxSyncingAllowed: false, threeBoxSyncingAllowed: false,
@ -83,6 +85,12 @@ export default class ThreeBoxController {
async init() { async init() {
const accounts = await this.keyringController.getAccounts(); const accounts = await this.keyringController.getAccounts();
this.address = accounts[0]; this.address = accounts[0];
this._trackMetaMetricsEvent({
event: '3Box Initiated',
category: '3Box',
});
if (this.address && !(this.box && this.store.getState().threeBoxSynced)) { if (this.address && !(this.box && this.store.getState().threeBoxSynced)) {
await this.new3Box(); await this.new3Box();
} }
@ -140,8 +148,18 @@ export default class ThreeBoxController {
backupExists = threeBoxConfig.spaces && threeBoxConfig.spaces.metamask; backupExists = threeBoxConfig.spaces && threeBoxConfig.spaces.metamask;
} catch (e) { } catch (e) {
if (e.message.match(/^Error: Invalid response \(404\)/u)) { if (e.message.match(/^Error: Invalid response \(404\)/u)) {
this._trackMetaMetricsEvent({
event: '3Box Backup does not exist',
category: '3Box',
});
backupExists = false; backupExists = false;
} else { } else {
this._trackMetaMetricsEvent({
event: '3Box Config Error',
category: '3Box',
});
throw e; throw e;
} }
} }
@ -175,9 +193,19 @@ export default class ThreeBoxController {
this.store.updateState(stateUpdate); this.store.updateState(stateUpdate);
log.debug('3Box space sync done'); log.debug('3Box space sync done');
this._trackMetaMetricsEvent({
event: '3Box Synced',
category: '3Box',
});
}, },
}); });
} catch (e) { } catch (e) {
this._trackMetaMetricsEvent({
event: '3Box Initiation Error',
category: '3Box',
});
console.error(e); console.error(e);
throw e; throw e;
} }
@ -216,13 +244,28 @@ export default class ThreeBoxController {
preferences && this.preferencesController.store.updateState(preferences); preferences && this.preferencesController.store.updateState(preferences);
addressBook && this.addressBookController.update(addressBook, true); addressBook && this.addressBookController.update(addressBook, true);
this.setShowRestorePromptToFalse(); this.setShowRestorePromptToFalse();
this._trackMetaMetricsEvent({
event: '3Box Restored Data',
category: '3Box',
});
} }
turnThreeBoxSyncingOn() { turnThreeBoxSyncingOn() {
this._trackMetaMetricsEvent({
event: '3Box Sync Turned On',
category: '3Box',
});
this._registerUpdates(); this._registerUpdates();
} }
turnThreeBoxSyncingOff() { turnThreeBoxSyncingOff() {
this._trackMetaMetricsEvent({
event: '3Box Sync Turned Off',
category: '3Box',
});
this.box.logout(); this.box.logout();
} }

@ -15,6 +15,7 @@ import {
bnToHex, bnToHex,
BnMultiplyByFraction, BnMultiplyByFraction,
addHexPrefix, addHexPrefix,
getChainType,
} from '../../lib/util'; } from '../../lib/util';
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/helpers/constants/error-keys'; import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/helpers/constants/error-keys';
import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/pages/swaps/swaps.util'; import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/pages/swaps/swaps.util';
@ -24,6 +25,7 @@ import {
} from '../../../../shared/constants/transaction'; } from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { GAS_LIMITS } from '../../../../shared/constants/gas';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../../shared/constants/network';
import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils';
import TransactionStateManager from './tx-state-manager'; import TransactionStateManager from './tx-state-manager';
import TxGasUtil from './tx-gas-utils'; import TxGasUtil from './tx-gas-utils';
@ -356,11 +358,16 @@ export default class TransactionController extends EventEmitter {
* @returns {Promise<Object>} Object containing the default gas limit, or the simulation failure object * @returns {Promise<Object>} Object containing the default gas limit, or the simulation failure object
*/ */
async _getDefaultGasLimit(txMeta, getCodeResponse) { async _getDefaultGasLimit(txMeta, getCodeResponse) {
const chainId = this._getCurrentChainId();
const customNetworkGasBuffer = CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId];
const chainType = getChainType(chainId);
if (txMeta.txParams.gas) { if (txMeta.txParams.gas) {
return {}; return {};
} else if ( } else if (
txMeta.txParams.to && txMeta.txParams.to &&
txMeta.type === TRANSACTION_TYPES.SENT_ETHER txMeta.type === TRANSACTION_TYPES.SENT_ETHER &&
chainType !== 'custom'
) { ) {
// if there's data in the params, but there's no contract code, it's not a valid transaction // if there's data in the params, but there's no contract code, it's not a valid transaction
if (txMeta.txParams.data) { if (txMeta.txParams.data) {
@ -389,6 +396,7 @@ export default class TransactionController extends EventEmitter {
const gasLimit = this.txGasUtil.addGasBuffer( const gasLimit = this.txGasUtil.addGasBuffer(
addHexPrefix(estimatedGasHex), addHexPrefix(estimatedGasHex),
blockGasLimit, blockGasLimit,
customNetworkGasBuffer,
); );
return { gasLimit, simulationFails }; return { gasLimit, simulationFails };
} }

@ -3,6 +3,10 @@ import extension from 'extensionizer';
import { stripHexPrefix } from 'ethereumjs-util'; import { stripHexPrefix } from 'ethereumjs-util';
import BN from 'bn.js'; import BN from 'bn.js';
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import {
MAINNET_CHAIN_ID,
TEST_CHAINS,
} from '../../../shared/constants/network';
import { import {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
@ -180,6 +184,15 @@ function bnToHex(inputBn) {
return addHexPrefix(inputBn.toString(16)); return addHexPrefix(inputBn.toString(16));
} }
function getChainType(chainId) {
if (chainId === MAINNET_CHAIN_ID) {
return 'mainnet';
} else if (TEST_CHAINS.includes(chainId)) {
return 'testnet';
}
return 'custom';
}
export { export {
getPlatform, getPlatform,
getEnvironmentType, getEnvironmentType,
@ -189,4 +202,5 @@ export {
checkForError, checkForError,
addHexPrefix, addHexPrefix,
bnToHex, bnToHex,
getChainType,
}; };

@ -132,11 +132,17 @@ export default class MetamaskController extends EventEmitter {
this.networkController = new NetworkController(initState.NetworkController); this.networkController = new NetworkController(initState.NetworkController);
this.networkController.setInfuraProjectId(opts.infuraProjectId); this.networkController.setInfuraProjectId(opts.infuraProjectId);
// now we can initialize the RPC provider, which other controllers require
this.initializeProvider();
this.provider = this.networkController.getProviderAndBlockTracker().provider;
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker;
this.preferencesController = new PreferencesController({ this.preferencesController = new PreferencesController({
initState: initState.PreferencesController, initState: initState.PreferencesController,
initLangCode: opts.initLangCode, initLangCode: opts.initLangCode,
openPopup: opts.openPopup, openPopup: opts.openPopup,
network: this.networkController, network: this.networkController,
provider: this.provider,
migrateAddressBookState: this.migrateAddressBookState.bind(this), migrateAddressBookState: this.migrateAddressBookState.bind(this),
}); });
@ -183,11 +189,6 @@ export default class MetamaskController extends EventEmitter {
initState.NotificationController, initState.NotificationController,
); );
// now we can initialize the RPC provider, which other controllers require
this.initializeProvider();
this.provider = this.networkController.getProviderAndBlockTracker().provider;
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker;
// token exchange rate tracker // token exchange rate tracker
this.tokenRatesController = new TokenRatesController({ this.tokenRatesController = new TokenRatesController({
preferences: this.preferencesController.store, preferences: this.preferencesController.store,
@ -314,6 +315,9 @@ export default class MetamaskController extends EventEmitter {
this.keyringController.memStore, this.keyringController.memStore,
), ),
version, version,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
}); });
this.txController = new TransactionController({ this.txController = new TransactionController({
@ -727,6 +731,10 @@ export default class MetamaskController extends EventEmitter {
preferencesController, preferencesController,
), ),
addToken: nodeify(preferencesController.addToken, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController),
updateTokenType: nodeify(
preferencesController.updateTokenType,
preferencesController,
),
removeToken: nodeify( removeToken: nodeify(
preferencesController.removeToken, preferencesController.removeToken,
preferencesController, preferencesController,

@ -6,9 +6,9 @@ module.exports = {
coverageThreshold: { coverageThreshold: {
global: { global: {
branches: 32.75, branches: 32.75,
functions: 42.9, functions: 40,
lines: 43.12, lines: 42.29,
statements: 43.67, statements: 42.83,
}, },
}, },
setupFiles: ['./test/setup.js', './test/env.js'], setupFiles: ['./test/setup.js', './test/env.js'],

@ -1,6 +1,6 @@
{ {
"name": "metamask-crx", "name": "metamask-crx",
"version": "9.7.1", "version": "9.8.0",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@ -97,7 +97,7 @@
"@lavamoat/preinstall-always-fail": "^1.0.0", "@lavamoat/preinstall-always-fail": "^1.0.0",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.26.0", "@metamask/contract-metadata": "^1.26.0",
"@metamask/controllers": "^9.0.0", "@metamask/controllers": "^10.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.5.0", "@metamask/eth-ledger-bridge-keyring": "^0.5.0",
"@metamask/eth-token-tracker": "^3.0.1", "@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0", "@metamask/etherscan-link": "^2.1.0",
@ -151,6 +151,7 @@
"fast-safe-stringify": "^2.0.7", "fast-safe-stringify": "^2.0.7",
"fuse.js": "^3.2.0", "fuse.js": "^3.2.0",
"globalthis": "^1.0.1", "globalthis": "^1.0.1",
"human-standard-collectible-abi": "^1.0.2",
"human-standard-token-abi": "^2.0.0", "human-standard-token-abi": "^2.0.0",
"immer": "^8.0.1", "immer": "^8.0.1",
"json-rpc-engine": "^6.1.0", "json-rpc-engine": "^6.1.0",

@ -19,6 +19,9 @@ export const GOERLI_CHAIN_ID = '0x5';
export const KOVAN_CHAIN_ID = '0x2a'; export const KOVAN_CHAIN_ID = '0x2a';
export const LOCALHOST_CHAIN_ID = '0x539'; export const LOCALHOST_CHAIN_ID = '0x539';
export const BSC_CHAIN_ID = '0x38'; export const BSC_CHAIN_ID = '0x38';
export const OPTIMISM_CHAIN_ID = '0xa';
export const OPTIMISM_TESTNET_CHAIN_ID = '0x45';
export const POLYGON_CHAIN_ID = '0x89';
/** /**
* The largest possible chain ID we can handle. * The largest possible chain ID we can handle.
@ -120,3 +123,8 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = {
}; };
export const INFURA_BLOCKED_KEY = 'countryBlocked'; export const INFURA_BLOCKED_KEY = 'countryBlocked';
export const CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP = {
[OPTIMISM_CHAIN_ID]: 1,
[OPTIMISM_TESTNET_CHAIN_ID]: 1,
};

@ -93,3 +93,7 @@ export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, [BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL,
[MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, [MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL,
}; };
export const ETHEREUM = 'ethereum';
export const POLYGON = 'polygon';
export const BSC = 'bsc';

@ -8,9 +8,21 @@
"mockMetaMetricsResponse": true "mockMetaMetricsResponse": true
}, },
"swaps": { "swaps": {
"featureFlag": { "featureFlags": {
"status": { "bsc": {
"active": true "mobile_active": false,
"extension_active": true,
"fallback_to_v1": true
},
"ethereum": {
"mobile_active": false,
"extension_active": true,
"fallback_to_v1": true
},
"polygon": {
"mobile_active": false,
"extension_active": true,
"fallback_to_v1": false
} }
} }
} }

@ -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 },
);
},
);
});
});

@ -48,9 +48,9 @@ async function setupFetchMocking(driver) {
return { json: async () => clone(mockResponses.gasPricesBasic) }; return { json: async () => clone(mockResponses.gasPricesBasic) };
} else if (url.match(/chromeextensionmm/u)) { } else if (url.match(/chromeextensionmm/u)) {
return { json: async () => clone(mockResponses.metametrics) }; return { json: async () => clone(mockResponses.metametrics) };
} else if (url.match(/^https:\/\/(api\.metaswap|.*airswap-dev)/u)) { } else if (url.match(/^https:\/\/(api2\.metaswap\.codefi\.network)/u)) {
if (url.match(/featureFlag$/u)) { if (url.match(/featureFlags$/u)) {
return { json: async () => clone(mockResponses.swaps.featureFlag) }; return { json: async () => clone(mockResponses.swaps.featureFlags) };
} }
} }
return window.origFetch(...args); return window.origFetch(...args);

@ -1 +1,2 @@
export const METASWAP_BASE_URL = 'https://api.metaswap.codefi.network'; export const METASWAP_BASE_URL = 'https://api.metaswap.codefi.network';
export const METASWAP_API_V2_BASE_URL = 'https://api2.metaswap.codefi.network';

@ -106,6 +106,7 @@ export const createSwapsMockStore = () => {
topAggId: null, topAggId: null,
routeState: '', routeState: '',
swapsFeatureIsLive: false, swapsFeatureIsLive: false,
useNewSwapsApi: false,
}, },
}, },
}; };

@ -59,3 +59,23 @@ export const TOKENS_GET_RESPONSE = [
address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF',
}, },
]; ];
export const createFeatureFlagsResponse = () => {
return {
bsc: {
mobile_active: false,
extension_active: true,
fallback_to_v1: true,
},
ethereum: {
mobile_active: false,
extension_active: true,
fallback_to_v1: true,
},
polygon: {
mobile_active: false,
extension_active: true,
fallback_to_v1: false,
},
};
};

@ -10,7 +10,7 @@ import InfoIcon from '../../ui/icon/info-icon.component';
import Button from '../../ui/button'; import Button from '../../ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { useMetricEvent } from '../../../hooks/useMetricEvent'; 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 { SEND_ROUTE } from '../../../helpers/constants/routes';
import { SEVERITIES } from '../../../helpers/constants/design-system'; import { SEVERITIES } from '../../../helpers/constants/design-system';
@ -27,6 +27,7 @@ const AssetListItem = ({
primary, primary,
secondary, secondary,
identiconBorder, identiconBorder,
isERC721,
}) => { }) => {
const t = useI18nContext(); const t = useI18nContext();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -68,13 +69,17 @@ const AssetListItem = ({
e.stopPropagation(); e.stopPropagation();
sendTokenEvent(); sendTokenEvent();
dispatch( dispatch(
updateSendToken({ updateSendAsset({
type: ASSET_TYPES.TOKEN,
details: {
address: tokenAddress, address: tokenAddress,
decimals: tokenDecimals, decimals: tokenDecimals,
symbol: tokenSymbol, symbol: tokenSymbol,
},
}), }),
); ).then(() => {
history.push(SEND_ROUTE); history.push(SEND_ROUTE);
});
}} }}
> >
{t('sendSpecifiedTokens', [tokenSymbol])} {t('sendSpecifiedTokens', [tokenSymbol])}
@ -107,7 +112,7 @@ const AssetListItem = ({
</button> </button>
} }
titleIcon={titleIcon} titleIcon={titleIcon}
subtitle={<h3 title={secondary}>{secondary}</h3>} subtitle={secondary ? <h3 title={secondary}>{secondary}</h3> : null}
onClick={onClick} onClick={onClick}
icon={ icon={
<Identicon <Identicon
@ -121,10 +126,12 @@ const AssetListItem = ({
} }
midContent={midContent} midContent={midContent}
rightContent={ rightContent={
!isERC721 && (
<> <>
<i className="fas fa-chevron-right asset-list-item__chevron-right" /> <i className="fas fa-chevron-right asset-list-item__chevron-right" />
{sendTokenButton} {sendTokenButton}
</> </>
)
} }
/> />
); );
@ -143,6 +150,7 @@ AssetListItem.propTypes = {
'primary': PropTypes.string, 'primary': PropTypes.string,
'secondary': PropTypes.string, 'secondary': PropTypes.string,
'identiconBorder': PropTypes.bool, 'identiconBorder': PropTypes.bool,
'isERC721': PropTypes.bool,
}; };
AssetListItem.defaultProps = { AssetListItem.defaultProps = {

@ -56,13 +56,13 @@ const AssetList = ({ onClickAsset }) => {
}, },
); );
const [secondaryCurrencyDisplay] = useCurrencyDisplay( const [
selectedAccountBalance, secondaryCurrencyDisplay,
{ secondaryCurrencyProperties,
] = useCurrencyDisplay(selectedAccountBalance, {
numberOfDecimals: secondaryNumberOfDecimals, numberOfDecimals: secondaryNumberOfDecimals,
currency: secondaryCurrency, currency: secondaryCurrency,
}, });
);
const primaryTokenImage = useSelector(getNativeCurrencyImage); const primaryTokenImage = useSelector(getNativeCurrencyImage);
@ -71,7 +71,9 @@ const AssetList = ({ onClickAsset }) => {
<AssetListItem <AssetListItem
onClick={() => onClickAsset(nativeCurrency)} onClick={() => onClickAsset(nativeCurrency)}
data-testid="wallet-balance" data-testid="wallet-balance"
primary={primaryCurrencyProperties.value} primary={
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
}
tokenSymbol={primaryCurrencyProperties.suffix} tokenSymbol={primaryCurrencyProperties.suffix}
secondary={showFiat ? secondaryCurrencyDisplay : undefined} secondary={showFiat ? secondaryCurrencyDisplay : undefined}
tokenImage={primaryTokenImage} tokenImage={primaryTokenImage}

@ -9,10 +9,10 @@ import {
} from '../../../../ducks/gas/gas.duck'; } from '../../../../ducks/gas/gas.duck';
import { import {
hideGasButtonGroup, useCustomGas,
setGasLimit, updateGasLimit,
setGasPrice, updateGasPrice,
} from '../../../../ducks/send/send.duck'; } from '../../../../ducks/send';
let mapDispatchToProps; let mapDispatchToProps;
let mergeProps; let mergeProps;
@ -32,8 +32,6 @@ jest.mock('../../../../selectors', () => ({
`mockRenderableBasicEstimateData:${Object.keys(s).length}`, `mockRenderableBasicEstimateData:${Object.keys(s).length}`,
getDefaultActiveButtonIndex: (a, b) => a + b, getDefaultActiveButtonIndex: (a, b) => a + b,
getCurrentEthBalance: (state) => state.metamask.balance || '0x0', getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
getSendToken: () => null,
getTokenBalance: (state) => state.send.tokenBalance || '0x0',
getCustomGasPrice: (state) => state.gas.customData.price || '0x0', getCustomGasPrice: (state) => state.gas.customData.price || '0x0',
getCustomGasLimit: (state) => state.gas.customData.limit || '0x0', getCustomGasLimit: (state) => state.gas.customData.limit || '0x0',
getCurrentCurrency: jest.fn().mockReturnValue('usd'), getCurrentCurrency: jest.fn().mockReturnValue('usd'),
@ -57,11 +55,15 @@ jest.mock('../../../../ducks/gas/gas.duck', () => ({
resetCustomData: jest.fn(), resetCustomData: jest.fn(),
})); }));
jest.mock('../../../../ducks/send/send.duck', () => ({ jest.mock('../../../../ducks/send', () => {
hideGasButtonGroup: jest.fn(), const { ASSET_TYPES } = jest.requireActual('../../../../ducks/send');
setGasLimit: jest.fn(), return {
setGasPrice: jest.fn(), useCustomGas: jest.fn(),
})); updateGasLimit: jest.fn(),
updateGasPrice: jest.fn(),
getSendAsset: jest.fn(() => ({ type: ASSET_TYPES.NATIVE })),
};
});
require('./gas-modal-page-container.container'); require('./gas-modal-page-container.container');
@ -79,11 +81,11 @@ describe('gas-modal-page-container container', () => {
dispatchSpy.resetHistory(); dispatchSpy.resetHistory();
}); });
describe('hideGasButtonGroup()', () => { describe('useCustomGas()', () => {
it('should dispatch a hideGasButtonGroup action', () => { it('should dispatch a useCustomGas action', () => {
mapDispatchToPropsObject.hideGasButtonGroup(); mapDispatchToPropsObject.useCustomGas();
expect(dispatchSpy.calledOnce).toStrictEqual(true); expect(dispatchSpy.calledOnce).toStrictEqual(true);
expect(hideGasButtonGroup).toHaveBeenCalled(); expect(useCustomGas).toHaveBeenCalled();
}); });
}); });
@ -126,13 +128,13 @@ describe('gas-modal-page-container container', () => {
}); });
describe('setGasData()', () => { 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'); mapDispatchToPropsObject.setGasData('ffff', 'aaaa');
expect(dispatchSpy.calledTwice).toStrictEqual(true); expect(dispatchSpy.calledTwice).toStrictEqual(true);
expect(setGasPrice).toHaveBeenCalled(); expect(updateGasPrice).toHaveBeenCalled();
expect(setGasLimit).toHaveBeenCalled(); expect(updateGasLimit).toHaveBeenCalled();
expect(setGasLimit).toHaveBeenCalledWith('ffff'); expect(updateGasLimit).toHaveBeenCalledWith('ffff');
expect(setGasPrice).toHaveBeenCalledWith('aaaa'); expect(updateGasPrice).toHaveBeenCalledWith('aaaa');
}); });
}); });
@ -165,7 +167,7 @@ describe('gas-modal-page-container container', () => {
}; };
dispatchProps = { dispatchProps = {
updateCustomGasPrice: sinon.spy(), updateCustomGasPrice: sinon.spy(),
hideGasButtonGroup: sinon.spy(), useCustomGas: sinon.spy(),
setGasData: sinon.spy(), setGasData: sinon.spy(),
updateConfirmTxGasAndCalculate: sinon.spy(), updateConfirmTxGasAndCalculate: sinon.spy(),
someOtherDispatchProp: sinon.spy(), someOtherDispatchProp: sinon.spy(),
@ -194,7 +196,7 @@ describe('gas-modal-page-container container', () => {
dispatchProps.updateConfirmTxGasAndCalculate.callCount, dispatchProps.updateConfirmTxGasAndCalculate.callCount,
).toStrictEqual(0); ).toStrictEqual(0);
expect(dispatchProps.setGasData.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); expect(dispatchProps.hideModal.callCount).toStrictEqual(0);
result.onSubmit(); result.onSubmit();
@ -203,7 +205,7 @@ describe('gas-modal-page-container container', () => {
dispatchProps.updateConfirmTxGasAndCalculate.callCount, dispatchProps.updateConfirmTxGasAndCalculate.callCount,
).toStrictEqual(1); ).toStrictEqual(1);
expect(dispatchProps.setGasData.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(1); expect(dispatchProps.hideModal.callCount).toStrictEqual(1);
expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0); expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0);
@ -238,7 +240,7 @@ describe('gas-modal-page-container container', () => {
dispatchProps.updateConfirmTxGasAndCalculate.callCount, dispatchProps.updateConfirmTxGasAndCalculate.callCount,
).toStrictEqual(0); ).toStrictEqual(0);
expect(dispatchProps.setGasData.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); expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(0);
result.onSubmit('mockNewLimit', 'mockNewPrice'); result.onSubmit('mockNewLimit', 'mockNewPrice');
@ -251,7 +253,7 @@ describe('gas-modal-page-container container', () => {
'mockNewLimit', 'mockNewLimit',
'mockNewPrice', 'mockNewPrice',
]); ]);
expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(1); expect(dispatchProps.useCustomGas.callCount).toStrictEqual(1);
expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1); expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1);
expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0); expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0);
@ -278,7 +280,7 @@ describe('gas-modal-page-container container', () => {
dispatchProps.updateConfirmTxGasAndCalculate.callCount, dispatchProps.updateConfirmTxGasAndCalculate.callCount,
).toStrictEqual(0); ).toStrictEqual(0);
expect(dispatchProps.setGasData.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.cancelAndClose.callCount).toStrictEqual(1);
expect(dispatchProps.createSpeedUpTransaction.callCount).toStrictEqual(1); expect(dispatchProps.createSpeedUpTransaction.callCount).toStrictEqual(1);

@ -14,20 +14,21 @@ import {
fetchBasicGasEstimates, fetchBasicGasEstimates,
} from '../../../../ducks/gas/gas.duck'; } from '../../../../ducks/gas/gas.duck';
import { import {
hideGasButtonGroup, getSendMaxModeState,
setGasLimit, getGasLimit,
setGasPrice, getGasPrice,
setGasTotal, getSendAmount,
updateSendAmount, updateGasLimit,
updateSendErrors, updateGasPrice,
} from '../../../../ducks/send/send.duck'; useCustomGas,
getSendAsset,
ASSET_TYPES,
} from '../../../../ducks/send';
import { import {
conversionRateSelector as getConversionRate, conversionRateSelector as getConversionRate,
getCurrentCurrency, getCurrentCurrency,
getCurrentEthBalance, getCurrentEthBalance,
getIsMainnet, getIsMainnet,
getSendToken,
getPreferences,
getIsTestnet, getIsTestnet,
getBasicGasEstimateLoadingStatus, getBasicGasEstimateLoadingStatus,
getCustomGasLimit, getCustomGasLimit,
@ -35,13 +36,12 @@ import {
getDefaultActiveButtonIndex, getDefaultActiveButtonIndex,
getRenderableBasicEstimateData, getRenderableBasicEstimateData,
isCustomPriceSafe, isCustomPriceSafe,
getTokenBalance,
getSendMaxModeState,
isCustomPriceSafeForCustomNetwork, isCustomPriceSafeForCustomNetwork,
getAveragePriceEstimateInHexWEI, getAveragePriceEstimateInHexWEI,
isCustomPriceExcessive, isCustomPriceExcessive,
getIsGasEstimatesFetched, getIsGasEstimatesFetched,
getIsCustomNetworkGasPriceFetched, getIsCustomNetworkGasPriceFetched,
getShouldShowFiat,
} from '../../../../selectors'; } from '../../../../selectors';
import { import {
@ -57,16 +57,15 @@ import {
isBalanceSufficient, isBalanceSufficient,
} from '../../../../pages/send/send.utils'; } from '../../../../pages/send/send.utils';
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'; 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 { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction';
import { GAS_LIMITS } from '../../../../../shared/constants/gas'; import { GAS_LIMITS } from '../../../../../shared/constants/gas';
import GasModalPageContainer from './gas-modal-page-container.component'; import GasModalPageContainer from './gas-modal-page-container.component';
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const { const gasLimit = getGasLimit(state);
metamask: { currentNetworkTxList }, const gasPrice = getGasPrice(state);
send, const amount = getSendAmount(state);
} = state; const { currentNetworkTxList } = state.metamask;
const { modalState: { props: modalProps } = {} } = state.appState.modal || {}; const { modalState: { props: modalProps } = {} } = state.appState.modal || {};
const { txData = {} } = modalProps || {}; const { txData = {} } = modalProps || {};
const { transaction = {}, onSubmit } = ownProps; const { transaction = {}, onSubmit } = ownProps;
@ -74,15 +73,15 @@ const mapStateToProps = (state, ownProps) => {
({ id }) => id === (transaction.id || txData.id), ({ id }) => id === (transaction.id || txData.id),
); );
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state); 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 // a "default" txParams is used during the send flow, since the transaction doesn't exist yet in that case
const txParams = selectedTransaction?.txParams const txParams = selectedTransaction?.txParams
? selectedTransaction.txParams ? selectedTransaction.txParams
: { : {
gas: send.gasLimit || GAS_LIMITS.SIMPLE, gas: gasLimit || GAS_LIMITS.SIMPLE,
gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state, true), gasPrice: gasPrice || getAveragePriceEstimateInHexWEI(state, true),
value: sendToken ? '0x0' : send.amount, value: asset.type === ASSET_TYPES.TOKEN ? '0x0' : amount,
}; };
const { gasPrice: currentGasPrice, gas: currentGasLimit } = txParams; const { gasPrice: currentGasPrice, gas: currentGasLimit } = txParams;
@ -116,20 +115,18 @@ const mapStateToProps = (state, ownProps) => {
const balance = getCurrentEthBalance(state); const balance = getCurrentEthBalance(state);
const { showFiatInTestnets } = getPreferences(state);
const isMainnet = getIsMainnet(state); const isMainnet = getIsMainnet(state);
const showFiat = Boolean(isMainnet || showFiatInTestnets); const showFiat = getShouldShowFiat(state);
const isSendTokenSet = Boolean(sendToken);
const isTestnet = getIsTestnet(state); const isTestnet = getIsTestnet(state);
const newTotalEth = const newTotalEth =
maxModeOn && !isSendTokenSet maxModeOn && asset.type === ASSET_TYPES.NATIVE
? sumHexWEIsToRenderableEth([balance, '0x0']) ? sumHexWEIsToRenderableEth([balance, '0x0'])
: sumHexWEIsToRenderableEth([value, customGasTotal]); : sumHexWEIsToRenderableEth([value, customGasTotal]);
const sendAmount = const sendAmount =
maxModeOn && !isSendTokenSet maxModeOn && asset.type === ASSET_TYPES.NATIVE
? subtractHexWEIsFromRenderableEth(balance, customGasTotal) ? subtractHexWEIsFromRenderableEth(balance, customGasTotal)
: sumHexWEIsToRenderableEth([value, '0x0']); : sumHexWEIsToRenderableEth([value, '0x0']);
@ -194,9 +191,7 @@ const mapStateToProps = (state, ownProps) => {
txId: transaction.id, txId: transaction.id,
insufficientBalance, insufficientBalance,
isMainnet, isMainnet,
sendToken,
balance, balance,
tokenBalance: getTokenBalance(state),
conversionRate, conversionRate,
value, value,
onSubmit, onSubmit,
@ -213,12 +208,13 @@ const mapDispatchToProps = (dispatch) => {
dispatch(hideModal()); dispatch(hideModal());
}, },
hideModal: () => dispatch(hideModal()), hideModal: () => dispatch(hideModal()),
useCustomGas: () => dispatch(useCustomGas()),
updateCustomGasPrice, updateCustomGasPrice,
updateCustomGasLimit: (newLimit) => updateCustomGasLimit: (newLimit) =>
dispatch(setCustomGasLimit(addHexPrefix(newLimit))), dispatch(setCustomGasLimit(addHexPrefix(newLimit))),
setGasData: (newLimit, newPrice) => { setGasData: (newLimit, newPrice) => {
dispatch(setGasLimit(newLimit)); dispatch(updateGasLimit(newLimit));
dispatch(setGasPrice(newPrice)); dispatch(updateGasPrice(newPrice));
}, },
updateConfirmTxGasAndCalculate: (gasLimit, gasPrice, updatedTx) => { updateConfirmTxGasAndCalculate: (gasLimit, gasPrice, updatedTx) => {
updateCustomGasPrice(gasPrice); updateCustomGasPrice(gasPrice);
@ -231,14 +227,8 @@ const mapDispatchToProps = (dispatch) => {
createSpeedUpTransaction: (txId, gasPrice, gasLimit) => { createSpeedUpTransaction: (txId, gasPrice, gasLimit) => {
return dispatch(createSpeedUpTransaction(txId, gasPrice, gasLimit)); return dispatch(createSpeedUpTransaction(txId, gasPrice, gasLimit));
}, },
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
hideSidebar: () => dispatch(hideSidebar()), hideSidebar: () => dispatch(hideSidebar()),
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
setGasTotal: (total) => dispatch(setGasTotal(total)),
setAmountToMax: (maxAmountDataObject) => {
dispatch(updateSendErrors({ amount: null }));
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)));
},
}; };
}; };
@ -251,17 +241,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
isSpeedUp, isSpeedUp,
isRetry, isRetry,
insufficientBalance, insufficientBalance,
maxModeOn,
customGasPrice, customGasPrice,
customGasTotal,
balance,
sendToken,
tokenBalance,
customGasLimit, customGasLimit,
transaction, transaction,
} = stateProps; } = stateProps;
const { const {
hideGasButtonGroup: dispatchHideGasButtonGroup, useCustomGas: dispatchUseCustomGas,
setGasData: dispatchSetGasData, setGasData: dispatchSetGasData,
updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
@ -269,7 +254,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
hideSidebar: dispatchHideSidebar, hideSidebar: dispatchHideSidebar,
cancelAndClose: dispatchCancelAndClose, cancelAndClose: dispatchCancelAndClose,
hideModal: dispatchHideModal, hideModal: dispatchHideModal,
setAmountToMax: dispatchSetAmountToMax,
...otherDispatchProps ...otherDispatchProps
} = dispatchProps; } = dispatchProps;
@ -305,17 +289,9 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchCancelAndClose(); dispatchCancelAndClose();
} else { } else {
dispatchSetGasData(gasLimit, gasPrice); dispatchSetGasData(gasLimit, gasPrice);
dispatchHideGasButtonGroup(); dispatchUseCustomGas();
dispatchCancelAndClose(); dispatchCancelAndClose();
} }
if (maxModeOn) {
dispatchSetAmountToMax({
balance,
gasTotal: customGasTotal,
sendToken,
tokenBalance,
});
}
}, },
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps, ...gasPriceButtonGroupProps,

@ -15,6 +15,7 @@ export default function TokenCell({
string, string,
image, image,
onClick, onClick,
isERC721,
}) { }) {
const userAddress = useSelector(getSelectedAddress); const userAddress = useSelector(getSelectedAddress);
const t = useI18nContext(); const t = useI18nContext();
@ -50,6 +51,7 @@ export default function TokenCell({
warning={warning} warning={warning}
primary={`${string || 0}`} primary={`${string || 0}`}
secondary={formattedFiat} secondary={formattedFiat}
isERC721={isERC721}
/> />
); );
} }
@ -62,6 +64,7 @@ TokenCell.propTypes = {
string: PropTypes.string, string: PropTypes.string,
image: PropTypes.string, image: PropTypes.string,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
isERC721: PropTypes.bool,
}; };
TokenCell.defaultProps = { TokenCell.defaultProps = {

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getIsMainnet, getPreferences } from '../../../selectors'; import { getShouldShowFiat } from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'; import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util';
import { sumHexes } from '../../../helpers/utils/transactions.util'; import { sumHexes } from '../../../helpers/utils/transactions.util';
@ -11,8 +11,6 @@ const mapStateToProps = (state, ownProps) => {
txParams: { gas, gasPrice, value } = {}, txParams: { gas, gasPrice, value } = {},
txReceipt: { gasUsed } = {}, txReceipt: { gasUsed } = {},
} = transaction; } = transaction;
const { showFiatInTestnets } = getPreferences(state);
const isMainnet = getIsMainnet(state);
const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas; const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas;
@ -22,7 +20,7 @@ const mapStateToProps = (state, ownProps) => {
return { return {
nativeCurrency: getNativeCurrency(state), nativeCurrency: getNativeCurrency(state),
showFiat: isMainnet || Boolean(showFiatInTestnets), showFiat: getShouldShowFiat(state),
totalInHex, totalInHex,
gas, gas,
gasPrice, gasPrice,

@ -17,7 +17,7 @@ import {
} from '../../../hooks/useMetricEvent'; } from '../../../hooks/useMetricEvent';
import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; 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 { setSwapsFromToken } from '../../../ducks/swaps/swaps';
import { import {
getAssetImages, getAssetImages,
@ -85,12 +85,19 @@ const TokenOverview = ({ className, token }) => {
className="token-overview__button" className="token-overview__button"
onClick={() => { onClick={() => {
sendTokenEvent(); sendTokenEvent();
dispatch(updateSendToken(token)); dispatch(
updateSendAsset({
type: ASSET_TYPES.TOKEN,
details: token,
}),
).then(() => {
history.push(SEND_ROUTE); history.push(SEND_ROUTE);
});
}} }}
Icon={SendIcon} Icon={SendIcon}
label={t('send')} label={t('send')}
data-testid="eth-overview-send" data-testid="eth-overview-send"
disabled={token.isERC721}
/> />
<IconButton <IconButton
className="token-overview__button" className="token-overview__button"
@ -145,6 +152,7 @@ TokenOverview.propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
decimals: PropTypes.number, decimals: PropTypes.number,
symbol: PropTypes.string, symbol: PropTypes.string,
isERC721: PropTypes.bool,
}).isRequired, }).isRequired,
}; };

@ -1,20 +1,19 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ETH } from '../../../helpers/constants/common'; import { ETH } from '../../../helpers/constants/common';
import { getIsMainnet, getPreferences } from '../../../selectors'; import { getShouldShowFiat } from '../../../selectors';
import CurrencyInput from './currency-input.component'; import CurrencyInput from './currency-input.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
metamask: { nativeCurrency, currentCurrency, conversionRate }, metamask: { nativeCurrency, currentCurrency, conversionRate },
} = state; } = state;
const { showFiatInTestnets } = getPreferences(state); const showFiat = getShouldShowFiat(state);
const isMainnet = getIsMainnet(state);
return { return {
nativeCurrency, nativeCurrency,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
hideFiat: !isMainnet && !showFiatInTestnets, hideFiat: !showFiat,
}; };
}; };

@ -110,3 +110,12 @@
'. actions actions actions actions mid mid mid mid right right right'; '. actions actions actions actions mid mid mid mid right right right';
} }
} }
.list-item--single-content-row {
grid-template-areas: 'icon head head head head head head head right right right right';
align-items: center;
@media (min-width: 576px) {
grid-template-areas: 'icon head head head head mid mid mid mid right right right';
}
}

@ -14,7 +14,11 @@ export default function ListItem({
className, className,
'data-testid': dataTestId, 'data-testid': dataTestId,
}) { }) {
const primaryClassName = classnames('list-item', className); const primaryClassName = classnames(
'list-item',
className,
subtitle || children ? '' : 'list-item--single-content-row',
);
return ( return (
<div <div

@ -141,6 +141,7 @@ describe('TokenInput Component', () => {
}} }}
tokenExchangeRates={{ '0x1': 2 }} tokenExchangeRates={{ '0x1': 2 }}
showFiat showFiat
currentCurrency="usd"
/> />
</Provider>, </Provider>,
); );
@ -278,6 +279,7 @@ describe('TokenInput Component', () => {
}} }}
tokenExchangeRates={{ '0x1': 2 }} tokenExchangeRates={{ '0x1': 2 }}
showFiat showFiat
currentCurrency="usd"
/> />
</Provider>, </Provider>,
); );

@ -1,23 +1,17 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { getTokenExchangeRates, getShouldShowFiat } from '../../../selectors';
getIsMainnet,
getTokenExchangeRates,
getPreferences,
} from '../../../selectors';
import TokenInput from './token-input.component'; import TokenInput from './token-input.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
metamask: { currentCurrency }, metamask: { currentCurrency },
} = state; } = state;
const { showFiatInTestnets } = getPreferences(state);
const isMainnet = getIsMainnet(state);
return { return {
currentCurrency, currentCurrency,
tokenExchangeRates: getTokenExchangeRates(state), tokenExchangeRates: getTokenExchangeRates(state),
hideConversion: !isMainnet && !showFiatInTestnets, hideConversion: !getShouldShowFiat(state),
}; };
}; };

@ -1,7 +1,10 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; 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 * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also

@ -15,10 +15,11 @@ import {
getNumberOfAccounts, getNumberOfAccounts,
getNumberOfTokens, getNumberOfTokens,
} from '../selectors/selectors'; } from '../selectors/selectors';
import { getSendToken } from '../selectors/send'; import { getSendAsset, ASSET_TYPES } from '../ducks/send';
import { txDataSelector } from '../selectors/confirm-transaction'; import { txDataSelector } from '../selectors/confirm-transaction';
import { getEnvironmentType } from '../../app/scripts/lib/util'; import { getEnvironmentType } from '../../app/scripts/lib/util';
import { trackMetaMetricsEvent } from '../store/actions'; import { trackMetaMetricsEvent } from '../store/actions';
import { getNativeCurrency } from '../ducks/metamask/metamask';
export const MetaMetricsContext = createContext(() => { export const MetaMetricsContext = createContext(() => {
captureException( captureException(
@ -31,7 +32,8 @@ export const MetaMetricsContext = createContext(() => {
export function MetaMetricsProvider({ children }) { export function MetaMetricsProvider({ children }) {
const txData = useSelector(txDataSelector) || {}; const txData = useSelector(txDataSelector) || {};
const environmentType = getEnvironmentType(); const environmentType = getEnvironmentType();
const activeCurrency = useSelector(getSendToken)?.symbol; const activeAsset = useSelector(getSendAsset);
const nativeAssetSymbol = useSelector(getNativeCurrency);
const accountType = useSelector(getAccountType); const accountType = useSelector(getAccountType);
const confirmTransactionOrigin = txData.origin; const confirmTransactionOrigin = txData.origin;
const numberOfTokens = useSelector(getNumberOfTokens); const numberOfTokens = useSelector(getNumberOfTokens);
@ -72,7 +74,10 @@ export function MetaMetricsProvider({ children }) {
action: eventOpts.action, action: eventOpts.action,
number_of_tokens: numberOfTokens, number_of_tokens: numberOfTokens,
number_of_accounts: numberOfAccounts, number_of_accounts: numberOfAccounts,
active_currency: activeCurrency, active_currency:
activeAsset.type === ASSET_TYPES.NATIVE
? nativeAssetSymbol
: activeAsset?.details?.symbol,
account_type: accountType, account_type: accountType,
is_new_visit: config.is_new_visit, is_new_visit: config.is_new_visit,
// the properties coming from this key will not match our standards for // the properties coming from this key will not match our standards for
@ -102,7 +107,8 @@ export function MetaMetricsProvider({ children }) {
accountType, accountType,
currentPath, currentPath,
confirmTransactionOrigin, confirmTransactionOrigin,
activeCurrency, activeAsset,
nativeAssetSymbol,
numberOfTokens, numberOfTokens,
numberOfAccounts, numberOfAccounts,
environmentType, environmentType,

@ -0,0 +1,200 @@
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 = null;
state.warning = null;
state.resolution = null;
state.network = null;
},
ensNotSupported: (state) => {
state.resolution = null;
state.warning = null;
state.error = ENS_NOT_SUPPORTED_ON_NETWORK;
},
resetEnsResolution: (state) => {
state.resolution = null;
state.warning = null;
state.error = 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,
ensNotSupported,
resetEnsResolution,
} = actions;
export { resetEnsResolution };
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(ensNotSupported());
} 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;
}

@ -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, fetchBasicGasEstimates,
} from './gas.duck'; } 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', () => ({ jest.mock('../../helpers/utils/storage-helpers.js', () => ({
getStorageItem: jest.fn(), getStorageItem: jest.fn(),
setStorageItem: jest.fn(), setStorageItem: jest.fn(),
@ -61,13 +69,6 @@ describe('Gas Duck', () => {
type: 'mainnet', 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()', () => { describe('GasReducer()', () => {
it('should initialize state', () => { it('should initialize state', () => {
expect(GasReducer(undefined, {})).toStrictEqual(initState); expect(GasReducer(undefined, {})).toStrictEqual(initState);

@ -10,6 +10,14 @@ import {
} from '../../helpers/utils/conversions.util'; } from '../../helpers/utils/conversions.util';
import { getIsMainnet, getCurrentChainId } from '../../selectors'; import { getIsMainnet, getCurrentChainId } from '../../selectors';
import fetchWithCache from '../../helpers/utils/fetch-with-cache'; 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 = { export const BASIC_ESTIMATE_STATES = {
LOADING: 'LOADING', LOADING: 'LOADING',
@ -22,14 +30,6 @@ export const GAS_SOURCE = {
ETHGASPRICE: 'eth_gasprice', 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 = { const initState = {
customData: { customData: {
price: null, price: null,

@ -2,7 +2,8 @@ import { combineReducers } from 'redux';
import { ALERT_TYPES } from '../../shared/constants/alerts'; import { ALERT_TYPES } from '../../shared/constants/alerts';
import metamaskReducer from './metamask/metamask'; import metamaskReducer from './metamask/metamask';
import localeMessagesReducer from './locale/locale'; 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 appStateReducer from './app/app';
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck'; import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck';
import gasReducer from './gas/gas.duck'; import gasReducer from './gas/gas.duck';
@ -16,6 +17,7 @@ export default combineReducers({
activeTab: (s) => (s === undefined ? null : s), activeTab: (s) => (s === undefined ? null : s),
metamask: metamaskReducer, metamask: metamaskReducer,
appState: appStateReducer, appState: appStateReducer,
ENS: ensReducer,
history: historyReducer, history: historyReducer,
send: sendReducer, send: sendReducer,
confirmTransaction: confirmTransactionReducer, confirmTransaction: confirmTransactionReducer,

@ -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,
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -34,9 +34,10 @@ import {
SWAPS_MAINTENANCE_ROUTE, SWAPS_MAINTENANCE_ROUTE,
} from '../../helpers/constants/routes'; } from '../../helpers/constants/routes';
import { import {
fetchSwapsFeatureLiveness, fetchSwapsFeatureFlags,
fetchSwapsGasPrices, fetchSwapsGasPrices,
isContractAddressValid, isContractAddressValid,
getSwapsLivenessForNetwork,
} from '../../pages/swaps/swaps.util'; } from '../../pages/swaps/swaps.util';
import { calcGasTotal } from '../../pages/send/send.utils'; import { calcGasTotal } from '../../pages/send/send.utils';
import { import {
@ -223,9 +224,12 @@ export function shouldShowCustomPriceTooLowWarning(state) {
const getSwapsState = (state) => state.metamask.swapsState; const getSwapsState = (state) => state.metamask.swapsState;
export const getSwapsFeatureLiveness = (state) => export const getSwapsFeatureIsLive = (state) =>
state.metamask.swapsState.swapsFeatureIsLive; state.metamask.swapsState.swapsFeatureIsLive;
export const getUseNewSwapsApi = (state) =>
state.metamask.swapsState.useNewSwapsApi;
export const getSwapsQuoteRefreshTime = (state) => export const getSwapsQuoteRefreshTime = (state) =>
state.metamask.swapsState.swapsQuoteRefreshTime; state.metamask.swapsState.swapsQuoteRefreshTime;
@ -373,16 +377,21 @@ export const fetchAndSetSwapsGasPriceInfo = () => {
export const fetchSwapsLiveness = () => { export const fetchSwapsLiveness = () => {
return async (dispatch, getState) => { return async (dispatch, getState) => {
let swapsFeatureIsLive = false; let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
try { try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness( const swapsFeatureFlags = await fetchSwapsFeatureFlags();
swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags,
getCurrentChainId(getState()), getCurrentChainId(getState()),
); );
} catch (error) { } catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsFeatureIsLive)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
return swapsFeatureIsLive; return swapsLivenessForNetwork;
}; };
}; };
@ -395,15 +404,22 @@ export const fetchQuotesAndSetQuoteState = (
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
let swapsFeatureIsLive = false; let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
try { try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); const swapsFeatureFlags = await fetchSwapsFeatureFlags();
swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags,
chainId,
);
} catch (error) { } catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsFeatureIsLive)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
if (!swapsFeatureIsLive) { if (!swapsLivenessForNetwork.swapsFeatureIsLive) {
await history.push(SWAPS_MAINTENANCE_ROUTE); await history.push(SWAPS_MAINTENANCE_ROUTE);
return; return;
} }
@ -600,15 +616,22 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
const hardwareWalletUsed = isHardwareWallet(state); const hardwareWalletUsed = isHardwareWallet(state);
let swapsFeatureIsLive = false; let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
try { try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); const swapsFeatureFlags = await fetchSwapsFeatureFlags();
swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags,
chainId,
);
} catch (error) { } catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsFeatureIsLive)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
if (!swapsFeatureIsLive) { if (!swapsLivenessForNetwork.swapsFeatureIsLive) {
await history.push(SWAPS_MAINTENANCE_ROUTE); await history.push(SWAPS_MAINTENANCE_ROUTE);
return; return;
} }
@ -808,12 +831,13 @@ export function fetchMetaSwapsGasPriceEstimates() {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
const useNewSwapsApi = getUseNewSwapsApi(state);
dispatch(swapGasPriceEstimatesFetchStarted()); dispatch(swapGasPriceEstimatesFetchStarted());
let priceEstimates; let priceEstimates;
try { try {
priceEstimates = await fetchSwapsGasPrices(chainId); priceEstimates = await fetchSwapsGasPrices(chainId, useNewSwapsApi);
} catch (e) { } catch (e) {
log.warn('Fetching swaps gas prices failed:', e); log.warn('Fetching swaps gas prices failed:', e);

@ -1,5 +1,6 @@
import nock from 'nock'; import nock from 'nock';
import { MOCKS } from '../../../test/jest';
import { setSwapsLiveness } from '../../store/actions'; import { setSwapsLiveness } from '../../store/actions';
import { setStorageItem } from '../../helpers/utils/storage-helpers'; import { setStorageItem } from '../../helpers/utils/storage-helpers';
import * as swaps from './swaps'; import * as swaps from './swaps';
@ -25,7 +26,7 @@ describe('Ducks - Swaps', () => {
describe('fetchSwapsLiveness', () => { describe('fetchSwapsLiveness', () => {
const cleanFeatureFlagApiCache = () => { const cleanFeatureFlagApiCache = () => {
setStorageItem( setStorageItem(
'cachedFetch:https://api.metaswap.codefi.network/featureFlag', 'cachedFetch:https://api2.metaswap.codefi.network/featureFlags',
null, null,
); );
}; };
@ -34,12 +35,12 @@ describe('Ducks - Swaps', () => {
cleanFeatureFlagApiCache(); cleanFeatureFlagApiCache();
}); });
const mockFeatureFlagApiResponse = ({ const mockFeatureFlagsApiResponse = ({
active = false, featureFlagsResponse,
replyWithError = false, replyWithError = false,
} = {}) => { } = {}) => {
const apiNock = nock('https://api.metaswap.codefi.network').get( const apiNock = nock('https://api2.metaswap.codefi.network').get(
'/featureFlag', '/featureFlags',
); );
if (replyWithError) { if (replyWithError) {
return apiNock.replyWithError({ return apiNock.replyWithError({
@ -47,9 +48,7 @@ describe('Ducks - Swaps', () => {
code: 'serverSideError', code: 'serverSideError',
}); });
} }
return apiNock.reply(200, { return apiNock.reply(200, featureFlagsResponse);
active,
});
}; };
const createGetState = () => { const createGetState = () => {
@ -58,61 +57,111 @@ describe('Ducks - Swaps', () => {
}); });
}; };
it('returns true if the Swaps feature is enabled', async () => { it('checks that Swaps for ETH are enabled and can use new API', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true }); const expectedSwapsLiveness = {
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( swapsFeatureIsLive: true,
useNewSwapsApi: true,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(true); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(true); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
it('returns false if the Swaps feature is disabled', async () => { it('checks that Swaps for ETH are disabled for API v2 and enabled for API v1', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: false }); const expectedSwapsLiveness = {
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( swapsFeatureIsLive: true,
useNewSwapsApi: false,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
featureFlagsResponse.ethereum.extension_active = false;
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(false); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(false); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
it('returns false if the /featureFlag API call throws an error', async () => { it('checks that Swaps for ETH are disabled for API v1 and v2', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ const expectedSwapsLiveness = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
featureFlagsResponse.ethereum.extension_active = false;
featureFlagsResponse.ethereum.fallback_to_v1 = false;
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch,
createGetState(),
);
expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
});
it('checks that Swaps for ETH are disabled if the /featureFlags API call throws an error', async () => {
const mockDispatch = jest.fn();
const expectedSwapsLiveness = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
const featureFlagApiNock = mockFeatureFlagsApiResponse({
replyWithError: true, replyWithError: true,
}); });
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(false); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(false); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
it('only calls the API once and returns true from cache for the second call', async () => { it('only calls the API once and returns response from cache for the second call', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true }); const expectedSwapsLiveness = {
swapsFeatureIsLive: true,
useNewSwapsApi: true,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
await swaps.fetchSwapsLiveness()(mockDispatch, createGetState()); await swaps.fetchSwapsLiveness()(mockDispatch, createGetState());
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
const featureFlagApiNock2 = mockFeatureFlagApiResponse({ active: true }); const featureFlagApiNock2 = mockFeatureFlagsApiResponse({
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead. expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead.
expect(mockDispatch).toHaveBeenCalledTimes(2); expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(setSwapsLiveness).toHaveBeenCalledWith(true); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(true); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
}); });
}); });

@ -5,3 +5,4 @@ export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract';
export const ETH_GAS_PRICE_FETCH_WARNING_KEY = 'ethGasPriceFetchWarning'; export const ETH_GAS_PRICE_FETCH_WARNING_KEY = 'ethGasPriceFetchWarning';
export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed'; export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed';
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive'; export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';
export const UNSENDABLE_ASSET_ERROR_KEY = 'unsendableAsset';

@ -150,8 +150,11 @@ const conversionUtil = (
conversionRate, conversionRate,
invertConversionRate, invertConversionRate,
}, },
) => ) => {
converter({ if (fromCurrency !== toCurrency && !conversionRate) {
return 0;
}
return converter({
fromCurrency, fromCurrency,
toCurrency, toCurrency,
fromNumericBase, fromNumericBase,
@ -163,6 +166,7 @@ const conversionUtil = (
invertConversionRate, invertConversionRate,
value: value || '0', value: value || '0',
}); });
};
const getBigNumber = (value, base) => { const getBigNumber = (value, base) => {
if (!isValidBase(base)) { if (!isValidBase(base)) {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save