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. 32
      ui/components/app/asset-list-item/asset-list-item.js
  74. 18
      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. 14
      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]
## [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]
### 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
@ -2321,7 +2329,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- 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.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

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

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

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

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

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

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

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

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

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

@ -650,12 +650,21 @@
"message": "The endpoint returned a different chain ID: $1",
"description": "$1 is the return value of eth_chainId from an RPC endpoint"
},
"ensIllegalCharacter": {
"message": "Illegal Character for ENS."
},
"ensNotFoundOnCurrentNetwork": {
"message": "ENS name not found on the current network. Try switching to Ethereum Mainnet."
},
"ensNotSupportedOnNetwork": {
"message": "Network does not support ENS"
},
"ensRegistrationError": {
"message": "Error in ENS name registration"
},
"ensUnknownError": {
"message": "ENS Lookup failed."
},
"enterAnAlias": {
"message": "Enter an alias"
},
@ -1174,6 +1183,9 @@
"networkNameEthereum": {
"message": "Ethereum"
},
"networkNamePolygon": {
"message": "Polygon"
},
"networkNameTestnet": {
"message": "Testnet"
},
@ -1451,9 +1463,6 @@
"recents": {
"message": "Recents"
},
"recipientAddress": {
"message": "Recipient Address"
},
"recipientAddressPlaceholder": {
"message": "Search, public address (0x), or ENS"
},
@ -2369,6 +2378,10 @@
"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."
},
"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": {
"message": "Updated $1"
},

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

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

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

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

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

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

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

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

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

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

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

@ -450,9 +450,6 @@
"readdToken": {
"message": "Ou ka ajoute token sa aprè sa ankò ou prale nan \"Ajoute token\" nan opsyon meni kont ou an."
},
"recipientAddress": {
"message": "Adrès pou resevwa"
},
"reject": {
"message": "Rejte"
},

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

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

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

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

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

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

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

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

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

@ -272,9 +272,6 @@
"readdToken": {
"message": "U kunt dit token in de toekomst weer toevoegen door naar \"Token toevoegen\" te gaan in het menu met accountopties."
},
"recipientAddress": {
"message": "Geadresseerde adres"
},
"reject": {
"message": "Afwijzen"
},

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

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

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

@ -282,9 +282,6 @@
"readdToken": {
"message": "Pode adicionar este token de novo clicando na opção “Adicionar token” no menu de opções da sua conta."
},
"recipientAddress": {
"message": "Endereço do Destinatário"
},
"reject": {
"message": "Rejeitar"
},

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -2,14 +2,21 @@ import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors';
import { normalize as normalizeAddress } from 'eth-sig-util';
import ethers from 'ethers';
import { ethers } from 'ethers';
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 { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
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';
const ERC721METADATA_INTERFACE_ID = '0x5b5e139f';
export default class PreferencesController {
/**
*
@ -73,11 +80,18 @@ export default class PreferencesController {
};
this.network = opts.network;
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
this.store = new ObservableStore(initState);
this.store.setMaxListeners(12);
this.openPopup = opts.openPopup;
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();
global.setPreference = (key, value) => {
@ -393,6 +407,8 @@ export default class PreferencesController {
});
const previousIndex = tokens.indexOf(previousEntry);
newEntry.isERC721 = await this._detectIsERC721(newEntry.address);
if (previousEntry) {
tokens[previousIndex] = newEntry;
} else {
@ -403,6 +419,24 @@ export default class PreferencesController {
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
*
@ -480,11 +514,8 @@ export default class PreferencesController {
let addressBookKey = rpcDetail.chainId;
if (!addressBookKey) {
// We need to find the networkId to determine what these addresses were keyed by
const provider = new ethers.providers.JsonRpcProvider(
rpcDetail.rpcUrl,
);
try {
addressBookKey = await provider.send('net_version');
addressBookKey = await this.ethersProvider.send('net_version');
assert(typeof addressBookKey === 'string');
} catch (error) {
log.debug(error);
@ -701,17 +732,6 @@ export default class PreferencesController {
// 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() {
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
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.
*

@ -1,10 +1,13 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import contractMaps from '@metamask/contract-metadata';
import abiERC721 from 'human-standard-collectible-abi';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import PreferencesController from './preferences';
import NetworkController from './network';
describe('preferences controller', function () {
let preferencesController;
@ -13,19 +16,32 @@ describe('preferences controller', function () {
let triggerNetworkChange;
let switchToMainnet;
let switchToRinkeby;
let provider;
const migrateAddressBookState = sinon.stub();
beforeEach(function () {
const sandbox = sinon.createSandbox();
currentChainId = MAINNET_CHAIN_ID;
network = {
getCurrentChainId: () => currentChainId,
on: sinon.spy(),
const networkControllerProviderConfig = {
getAccounts: () => undefined,
};
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({
migrateAddressBookState,
network,
provider,
});
triggerNetworkChange = network.on.firstCall.args[1];
triggerNetworkChange = spy.firstCall.args[1];
switchToMainnet = () => {
currentChainId = MAINNET_CHAIN_ID;
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 () {
it('should remove an address from state', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
@ -291,7 +405,12 @@ describe('preferences controller', function () {
assert.equal(tokens.length, 1, 'one token removed');
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 () {
@ -310,7 +429,12 @@ describe('preferences controller', function () {
assert.equal(tokensFirst.length, 1, 'one token removed in account');
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');
const tokensSecond = preferencesController.getTokens();
@ -335,7 +459,12 @@ describe('preferences controller', function () {
assert.equal(tokensFirst.length, 1, 'one token removed in network');
const [token1] = tokensFirst;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
switchToRinkeby();
const tokensSecond = preferencesController.getTokens();

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

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

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

@ -15,6 +15,7 @@ import {
bnToHex,
BnMultiplyByFraction,
addHexPrefix,
getChainType,
} from '../../lib/util';
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/helpers/constants/error-keys';
import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/pages/swaps/swaps.util';
@ -24,6 +25,7 @@ import {
} from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
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 TransactionStateManager from './tx-state-manager';
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
*/
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) {
return {};
} else if (
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 (txMeta.txParams.data) {
@ -389,6 +396,7 @@ export default class TransactionController extends EventEmitter {
const gasLimit = this.txGasUtil.addGasBuffer(
addHexPrefix(estimatedGasHex),
blockGasLimit,
customNetworkGasBuffer,
);
return { gasLimit, simulationFails };
}

@ -3,6 +3,10 @@ import extension from 'extensionizer';
import { stripHexPrefix } from 'ethereumjs-util';
import BN from 'bn.js';
import { memoize } from 'lodash';
import {
MAINNET_CHAIN_ID,
TEST_CHAINS,
} from '../../../shared/constants/network';
import {
ENVIRONMENT_TYPE_POPUP,
@ -180,6 +184,15 @@ function bnToHex(inputBn) {
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 {
getPlatform,
getEnvironmentType,
@ -189,4 +202,5 @@ export {
checkForError,
addHexPrefix,
bnToHex,
getChainType,
};

@ -132,11 +132,17 @@ export default class MetamaskController extends EventEmitter {
this.networkController = new NetworkController(initState.NetworkController);
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({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
openPopup: opts.openPopup,
network: this.networkController,
provider: this.provider,
migrateAddressBookState: this.migrateAddressBookState.bind(this),
});
@ -183,11 +189,6 @@ export default class MetamaskController extends EventEmitter {
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
this.tokenRatesController = new TokenRatesController({
preferences: this.preferencesController.store,
@ -314,6 +315,9 @@ export default class MetamaskController extends EventEmitter {
this.keyringController.memStore,
),
version,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.txController = new TransactionController({
@ -727,6 +731,10 @@ export default class MetamaskController extends EventEmitter {
preferencesController,
),
addToken: nodeify(preferencesController.addToken, preferencesController),
updateTokenType: nodeify(
preferencesController.updateTokenType,
preferencesController,
),
removeToken: nodeify(
preferencesController.removeToken,
preferencesController,

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

@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "9.7.1",
"version": "9.8.0",
"private": true,
"repository": {
"type": "git",
@ -97,7 +97,7 @@
"@lavamoat/preinstall-always-fail": "^1.0.0",
"@material-ui/core": "^4.11.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-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0",
@ -151,6 +151,7 @@
"fast-safe-stringify": "^2.0.7",
"fuse.js": "^3.2.0",
"globalthis": "^1.0.1",
"human-standard-collectible-abi": "^1.0.2",
"human-standard-token-abi": "^2.0.0",
"immer": "^8.0.1",
"json-rpc-engine": "^6.1.0",

@ -19,6 +19,9 @@ export const GOERLI_CHAIN_ID = '0x5';
export const KOVAN_CHAIN_ID = '0x2a';
export const LOCALHOST_CHAIN_ID = '0x539';
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.
@ -120,3 +123,8 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = {
};
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,
[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
},
"swaps": {
"featureFlag": {
"status": {
"active": true
"featureFlags": {
"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
}
}
}

@ -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) };
} else if (url.match(/chromeextensionmm/u)) {
return { json: async () => clone(mockResponses.metametrics) };
} else if (url.match(/^https:\/\/(api\.metaswap|.*airswap-dev)/u)) {
if (url.match(/featureFlag$/u)) {
return { json: async () => clone(mockResponses.swaps.featureFlag) };
} else if (url.match(/^https:\/\/(api2\.metaswap\.codefi\.network)/u)) {
if (url.match(/featureFlags$/u)) {
return { json: async () => clone(mockResponses.swaps.featureFlags) };
}
}
return window.origFetch(...args);

@ -1 +1,2 @@
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,
routeState: '',
swapsFeatureIsLive: false,
useNewSwapsApi: false,
},
},
};

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

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

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

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

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

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

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

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

@ -110,3 +110,12 @@
'. 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,
'data-testid': dataTestId,
}) {
const primaryClassName = classnames('list-item', className);
const primaryClassName = classnames(
'list-item',
className,
subtitle || children ? '' : 'list-item--single-content-row',
);
return (
<div

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

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

@ -1,7 +1,10 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { removeLeadingZeroes } from '../../../pages/send/send.utils';
function removeLeadingZeroes(str) {
return str.replace(/^0*(?=\d)/u, '');
}
/**
* Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also

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

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

@ -10,6 +10,14 @@ import {
} from '../../helpers/utils/conversions.util';
import { getIsMainnet, getCurrentChainId } from '../../selectors';
import fetchWithCache from '../../helpers/utils/fetch-with-cache';
import {
BASIC_GAS_ESTIMATE_STATUS,
RESET_CUSTOM_DATA,
SET_BASIC_GAS_ESTIMATE_DATA,
SET_CUSTOM_GAS_LIMIT,
SET_CUSTOM_GAS_PRICE,
SET_ESTIMATE_SOURCE,
} from './gas-action-constants';
export const BASIC_ESTIMATE_STATES = {
LOADING: 'LOADING',
@ -22,14 +30,6 @@ export const GAS_SOURCE = {
ETHGASPRICE: 'eth_gasprice',
};
// Actions
const BASIC_GAS_ESTIMATE_STATUS = 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA';
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';
const initState = {
customData: {
price: null,

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

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

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

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

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

Loading…
Cancel
Save