From 14b5c389ed5bf3c551e3c8b8f82d38b4dd23e217 Mon Sep 17 00:00:00 2001 From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Date: Fri, 2 Apr 2021 12:30:57 -0700 Subject: [PATCH] Version v9.3.0 RC (#10739) * Replace logic for eth swap token in fetchQuotesAndSetQuoteState with getSwapsEthToken call (#10624) * Move swaps constants to the shared constants directory (#10614) * Fix: ETH 'token' now only appears once in the swaps to and from dropdowns. (#10650) * Swaps support for local testnet (#10658) * Swaps support for local testnet * Create util method for comparison of token addresses/symbols to default swaps token * Get chainId from txMeta in _trackSwapsMetrics of transaction controller * Add comment to document purpose of getTransactionGroupRecipientAddressFilter * Use isSwapsDefaultTokenSymbol in place of repeated defaultTokenSymbol comparisons in build-quote.js * Additional swaps network support (#10721) * Add swaps support for bnc chain * Use single default token address in shared/constants/swaps * Ensure swaps gas prices are fetched from the correct chain specific endpoint (#10744) * Ensure swaps gas prices are fetched from the correct chain specific endpoint * Just rely on fetchWithCache to cache swaps gas prices, instead of directly using storage in getSwapsPriceEstimatesLastRetrieved * Empty commit * update @metamask/etherscan-link to v2.0.0 (#10747) * Use correct block explorer name and link in swaps when on custom network (#10743) * Use correct block explorer name and link in swaps when on custom network. * Fix up custom etherscan link code in build-quote.js * Use blockExplorerUrl hostname instead of 'blockExplorerBaseUrl' * Use correct etherscan-link method for token links in build-quote * Create correct token link in build-quote for mainnet AND custom networks * Block explorer url improvements in awaiting-swap.js and build-quote.js * Use swapVerifyTokenExplanation message with substitutable block explorer for all applicable locales * Ensure that block explorer links are not shown in awaiting-swap if no url is available * Ensure that the correct default currency symbols are used for fees on the view quote screen (#10753) * Updating y18n and netmask to resolve dependency issues (#10765) netmask@1.0.6 -> 2.0.1, y18n@3.2.1 -> 3.2.2, y18n@4.0.0 -> 4.0.1 * Ensure that priceSlippage fiat amounts are always shown in view-quote.js (#10762) * Ensure that the approval fee in the swaps custom gas modal is in network specific currency (#10763) * Use network specific swaps contract address when checking swap contract token approval (#10774) * Set the BSC_CONTRACT_ADDRESS to lowercase (#10800) * Ensure correct primary currency image is displayed on home screen and token list (#10777) * [skip e2e] Update changelog for v9.3.0 (#10740) * Version v9.3.0 * [skip e2e] Update changelog for v9.3.0 (#10803) Co-authored-by: Dan J Miller Co-authored-by: ryanml Co-authored-by: David Walsh Co-authored-by: MetaMask Bot --- CHANGELOG.md | 6 + app/_locales/en/messages.json | 7 +- app/_locales/es/messages.json | 7 +- app/_locales/es_419/messages.json | 7 +- app/_locales/hi/messages.json | 7 +- app/_locales/id/messages.json | 7 +- app/_locales/it/messages.json | 7 +- app/_locales/ja/messages.json | 7 +- app/_locales/ko/messages.json | 7 +- app/_locales/ru/messages.json | 7 +- app/_locales/tl/messages.json | 7 +- app/_locales/vi/messages.json | 7 +- app/_locales/zh_CN/messages.json | 7 +- app/images/bnb.png | Bin 0 -> 2210 bytes app/manifest/_base.json | 2 +- app/scripts/controllers/swaps.js | 84 ++++---- app/scripts/controllers/transactions/index.js | 1 + app/scripts/metamask-controller.js | 3 + package.json | 3 +- shared/constants/network.js | 14 ++ shared/constants/swaps.js | 89 +++++++++ shared/modules/swaps.utils.js | 33 ++++ test/unit/app/controllers/swaps.test.js | 16 +- .../app/asset-list-item/asset-list-item.js | 3 + .../components/app/asset-list/asset-list.js | 5 + .../transaction-list.component.js | 37 +++- .../app/wallet-overview/eth-overview.js | 19 +- .../app/wallet-overview/token-overview.js | 11 +- .../ui/identicon/identicon.component.js | 24 +-- ui/app/components/ui/identicon/index.scss | 2 +- .../tests/identicon.component.test.js | 7 +- ui/app/ducks/swaps/swaps.js | 75 +++---- ui/app/helpers/constants/swaps.js | 25 --- ui/app/helpers/utils/formatters.js | 5 +- .../tests/useTransactionDisplayData.test.js | 4 + ui/app/hooks/useCurrentAsset.js | 17 +- ui/app/hooks/useSwappedTokenValue.js | 26 ++- ui/app/hooks/useTokensToSearch.js | 63 +++--- .../send-asset-row.component.js | 13 +- .../send-asset-row.container.js | 2 + .../swaps/awaiting-swap/awaiting-swap.js | 58 ++++-- .../view-on-ether-scan-link.js | 2 +- ui/app/pages/swaps/build-quote/build-quote.js | 183 ++++++++++++------ ui/app/pages/swaps/index.js | 20 +- ui/app/pages/swaps/intro-popup/intro-popup.js | 12 +- ...swaps-gas-customization-modal.container.js | 41 ++-- .../pages/swaps/swaps-util-test-constants.js | 9 +- ui/app/pages/swaps/swaps.util.js | 154 +++++++++------ ui/app/pages/swaps/swaps.util.test.js | 45 +++-- ui/app/pages/swaps/view-quote/view-quote.js | 52 +++-- ui/app/selectors/custom-gas.js | 34 +++- ui/app/selectors/selectors.js | 37 +++- yarn.lock | 28 +-- 53 files changed, 845 insertions(+), 503 deletions(-) create mode 100644 app/images/bnb.png create mode 100644 shared/constants/swaps.js create mode 100644 shared/modules/swaps.utils.js delete mode 100644 ui/app/helpers/constants/swaps.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 637c22b6e..2e96f45c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Current Develop Branch +## 9.3.0 Fri Mar 26 2021 +- [#10777](https://github.com/MetaMask/metamask-extension/pull/10777): Display BNB token image for default currency on BSC network home screen +- [#10721](https://github.com/MetaMask/metamask-extension/pull/10721): Swaps support for the Binance network +- [#10658](https://github.com/MetaMask/metamask-extension/pull/10658): Swaps support for forked Mainnet on localhost +- [#10650](https://github.com/MetaMask/metamask-extension/pull/10650): Fix: ETH now only appears once in the swaps "to" and "from" dropdowns. + ## 9.2.1 Thu Mar 25 2021 - [#10692](https://github.com/MetaMask/metamask-extension/pull/10692): Prevent UI crash when a 'wallet_requestPermissions" confirmation is queued behind a "wallet_addEthereumChain" confirmation - [#10712](https://github.com/MetaMask/metamask-extension/pull/10712): Fix infinite spinner when request for token symbol fails while attempting an approve transaction diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b79c5b9b0..70e25f644 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -143,10 +143,6 @@ "amount": { "message": "Amount" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Amount:" }, @@ -1940,7 +1936,8 @@ "message": "Using the best quote" }, "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check Etherscan to verify this is the token you're looking for." + "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "View $1" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index f7cb42082..4a160692e 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -126,10 +126,6 @@ "amount": { "message": "Cantidad" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Cantidad:" }, @@ -1814,7 +1810,8 @@ "message": "Utilizando la mejor cotización" }, "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique Etherscan para verificar que este es el token que está buscando." + "message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique $1 para verificar que este es el token que está buscando.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Ver $1" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 9428674bd..f7b9d815f 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -126,10 +126,6 @@ "amount": { "message": "Monto" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Monto:" }, @@ -1814,7 +1810,8 @@ "message": "Utilizando la mejor cotización" }, "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique Etherscan para verificar que este es el token que está buscando." + "message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique $1 para verificar que este es el token que está buscando.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Ver $1" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 8feb56e55..9e8d76544 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -120,10 +120,6 @@ "amount": { "message": "राशि" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "राशि:" }, @@ -1781,7 +1777,8 @@ "message": "अज्ञात" }, "swapVerifyTokenExplanation": { - "message": "एकाधिक टोकन एक ही नाम और प्रतीक का उपयोग कर सकते हैं। यह सत्यापित करने के लिए Etherscan की जाँच करें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।" + "message": "एकाधिक टोकन एक ही नाम और प्रतीक का उपयोग कर सकते हैं। यह सत्यापित करने के लिए $1 की जाँच करें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "$1 देखें" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 67f7d4470..910bfcaff 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -120,10 +120,6 @@ "amount": { "message": "Jumlah" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Jumlah:" }, @@ -1781,7 +1777,8 @@ "message": "Tidak diketahui" }, "swapVerifyTokenExplanation": { - "message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa Etherscan untuk memverifikasi inilah token yang Anda cari." + "message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Lihat $1" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index c497ee342..6f591ddd9 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -126,10 +126,6 @@ "amount": { "message": "Importo" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Importo:" }, @@ -1837,7 +1833,8 @@ "message": "Quotazione migliore" }, "swapVerifyTokenExplanation": { - "message": "Più token possono usare lo stesso nome e simbolo. Verifica su Etherscan che questo sia il token che stai cercando." + "message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Vedi $1" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 69b2f208e..a29480b24 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -126,10 +126,6 @@ "amount": { "message": "金額" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "金額:" }, @@ -1814,7 +1810,8 @@ "message": "最適な見積を使用する" }, "swapVerifyTokenExplanation": { - "message": "複数のトークンが同じ名前とシンボルであることがあります。Etherscanで実際のトークンでを確認してください。" + "message": "複数のトークンが同じ名前とシンボルであることがあります。$1で実際のトークンでを確認してください。", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "$1 を表示" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 3f1df0f86..59d1fcc42 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -120,10 +120,6 @@ "amount": { "message": "금액" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "금액:" }, @@ -1778,7 +1774,8 @@ "message": "알 수 없음" }, "swapVerifyTokenExplanation": { - "message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. Etherscan을 확인하여 이것이 원하는 토큰인지 확인하세요." + "message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1을 확인하여 이것이 원하는 토큰인지 확인하세요.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "$1 보기" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 697abea15..6fb75c74a 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -120,10 +120,6 @@ "amount": { "message": "Сумма" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Сумма:" }, @@ -1781,7 +1777,8 @@ "message": "Неизвестный" }, "swapVerifyTokenExplanation": { - "message": "Несколько токенов могут использовать одно и то же имя и символ. Проверьте Etherscan, чтобы убедиться, что это именно тот токен, который вы ищете." + "message": "Несколько токенов могут использовать одно и то же имя и символ. Проверьте $1, чтобы убедиться, что это именно тот токен, который вы ищете.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Просмотреть $1" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index f35d6ae6a..22af3556c 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -120,10 +120,6 @@ "amount": { "message": "Halaga" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Halaga:" }, @@ -1778,7 +1774,8 @@ "message": "Hindi Alam" }, "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang Etherscan para ma-verify na ito ang token na hinahanap mo." + "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Tingnan ang $1" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index da2e8d5b5..e35becf7f 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -120,10 +120,6 @@ "amount": { "message": "Số tiền" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "Số tiền:" }, @@ -1781,7 +1777,8 @@ "message": "Không xác định" }, "swapVerifyTokenExplanation": { - "message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra trên Etherscan để xác minh xem đây có phải là token bạn đang tìm kiếm không." + "message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra trên $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "Xem $1" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 09c370050..9ca2dbe32 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -126,10 +126,6 @@ "amount": { "message": "数额" }, - "amountInEth": { - "message": "$1 ETH", - "description": "Displays an eth amount to the user. $1 is a decimal number" - }, "amountWithColon": { "message": "数额:" }, @@ -1814,7 +1810,8 @@ "message": "使用最好的报价" }, "swapVerifyTokenExplanation": { - "message": "多个代币可以使用相同的名称和符号。检查 Etherscan(以太坊浏览器)以确认这是您正在寻找的代币。" + "message": "多个代币可以使用相同的名称和符号。检查 $1(以太坊浏览器)以确认这是您正在寻找的代币。", + "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, "swapViewToken": { "message": "查看 $1" diff --git a/app/images/bnb.png b/app/images/bnb.png new file mode 100644 index 0000000000000000000000000000000000000000..64ef1c940b82852eda336c9af5fef1501b1e9a01 GIT binary patch literal 2210 zcmYM0dpOhWAIF^*OVnnFlE)l2%b{pFl;ggb)B1jEi;F9YClAv)J-?x%QhIDtN~Ut& zMtpw?`DG>P@D%+jEn!kr;%8yf35%WlzWw$5albzI{kg9Dy59Ha^Sw%vW9!(RP!N;CUjqx`PuaGYyjzj)71 z+s{;YI%yO#wS=L3k*t0Zrk)*9$^WbE4$?mL(R>?FKeAQ!W~g68s%KuRCOXuT?HWm} zrY}Jy*{$yTQ#E=`+rLLW`A{VjE2TNg2g|iRTa^Qus^PO*mEz9zW54U@7|OzdAbM7X z?EO^mus06%(Pb0*TKP~vb{P>j*|D_7be!1vz3xx(o0u`Ro778r5IYrDCV3LQn6dfC zlJ{XAi!H8D;*;vy;v|+6R+nS<$ILEfw{%-DTig%&C(d}-bgdLo>%i_A;ln7Qg;?)q z-cg4#XX)W%al13Xf}OdgL9SYs*-FRrFrL7qJ4pb{+Eu}+^s z!-X$vFh(+IfoywbM3ipBddNW+{sKsd%zQaT%7@o9*g^oAo+wJe%?HmDm=V(DWCI9r zmTmwrB_IVuc9fsCE+mRfnN$(S1Kz_e!;*tcqH&PHyoWagm`GOx(Oh3aLg;!BEsX^* zDDZ#C9-0Z+sh7enE)=QH9TyjAJ8MB1;C15VI*UGR{b>@Ry-mA0{=>7#id38|vsW@N zQ}tB7XVp5=^apY$14a}9ceYwy_D0dYL{u!ei;G7GvI#u+AI0NQ31eAc4j0c8K7Y{b zg@|hA2b%=pe2Oo7FGHjk$?iPrfwh^v#n|~ElQ0})NOph#E9nhHY?Eu_5qpNW>bM=LmnP*C*L2NyHOHb}ru1CIT8TK5o^s`wb3*RdnzpNv z;~cymJYCMQU%*(CB5^`ts3Y$Jctm7`CVN16r$}}@5Ueo^F@ilrM!IYR5y!lNPfW_0 zwmm~k$|Vtav4TPXXO$-pM&N4={jSrn27#Od6eYk-zfgk>IVxy3Lr@rno63_@5cv8+ zwfab}YW3v?K#_&?9096Dv9ThmJ}HEsWnv8`V>I)DQRWqaS2_Oow+=7$qUpTJ((K~a zvATAGTidb{{L%PEH;yw$cF1cDZ}LY|yBTd;PHWM=b(~0TUIgZzrlqR+Ho&b0okh0E zZWUo&9h0A`+EVpP?Bg1LT1qG~&X>eONu`c_4?B%QapX2h+Cb3K1@io0yiVTI&pi`i zMxLX>o1nbE;;P#x1ou7MJCoTRUk`&{6VpU>fT?^+k7!tuHM9Sk@)!Xn-o0@!!l9}5 z?b)y(@x-l-iL@Yb{UYZ4$)SW@BEis)SaQJQCinfC6D(D7yE${vT5~Gbw_#tQ0B`&$b8KRQZ4Ds2O@aX(Lpa`SB+=@oqVu zG=&wLu>_ccIL8f~>GpPoqtx9;P z66?wEyB)4ETak<*W60)y+xnCPC6i&FW-mW@5vjo+hZ~jW7w(N+Yt+7cUj1m}_|T`M zbHW>v10xMb7USnKpEkKoEpEwnhc@s|p|3v82H5gH33(QQ$@gsg!1VW>F7#17 zxTDlfPO~80>sOXO-mQh1Y{f_I4eY?I2v6}N0{D5OAMH=7AB*~Gl?adkA zT7+M=G-THI!_yX*P`BLR`;x4iS?Swsz{YY#>sMI&YdM|o3U?N>BwLuAl#bFm@<%7c zy8P({0{ATFsyvg>C8l ztd)#~>~DtjOYleco!btK8A^FhM~R0XY5OW4+jYp|Bm5r8j-)MxnTFr(DW_hG33y0& zQ(Bjx`CNVbskXGVYN9IJmef1@Zik|$Y44JHbjO#T3;r_g+HW^mWc>Uecd9>kyQT%4 zKVZiU-8kxJo-|YNcjMNhoG=9sQm zMra$1aZY``(b4HvV*HKD$Z1GC@xk(qSFa2{*{Qg{!h~T3IjHu9#A&-I>R2&+`aD*II(60hTtH1$lh+0T;;w=O-qH*6(H&hVE^cj9o zrwvmNz{J4C+kC$tI$dT2b)uLaKvB~ADiST9xP5tK6px5MUA7tm^w{Fa8-Zx40T=Hg zb>QOHO2@y5<~M+rcL6Rw14f&L?5l>%?iIZDTtPl0db&zF&c$m$&);}+L6TF%J`=K; zh&mX5qVK@mOsg;jz@-ICc*MiY$>m6a+3;ITNFnIyz*{Rw3CHP5qqz940aBuf+S2&L hk;ng0!}XgD;JVZlSlGYvLp~ik;UP4jDR}p({{yi6D;NL( literal 0 HcmV?d00001 diff --git a/app/manifest/_base.json b/app/manifest/_base.json index 0b7caade1..ab4d0d4ec 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -78,6 +78,6 @@ "notifications" ], "short_name": "__MSG_appName__", - "version": "9.2.1", + "version": "9.3.0", "web_accessible_resources": ["inpage.js", "phishing.html"] } diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 3aef182fb..81a55ead3 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -8,12 +8,14 @@ import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'; import { calcGasTotal } from '../../../ui/app/pages/send/send.utils'; import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util'; import { - ETH_SWAPS_TOKEN_OBJECT, DEFAULT_ERC20_APPROVE_GAS, QUOTES_EXPIRED_ERROR, QUOTES_NOT_AVAILABLE_ERROR, SWAPS_FETCH_ORDER_CONFLICT, -} from '../../../ui/app/helpers/constants/swaps'; + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, +} from '../../../shared/constants/swaps'; +import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils'; + import { fetchTradesInfo as defaultFetchTradesInfo, fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness, @@ -21,8 +23,6 @@ import { } from '../../../ui/app/pages/swaps/swaps.util'; import { NETWORK_EVENTS } from './network'; -const METASWAP_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'; - // The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator const MAX_GAS_LIMIT = 2500000; @@ -85,6 +85,7 @@ export default class SwapsController { fetchTradesInfo = defaultFetchTradesInfo, fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness, fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, + getCurrentChainId, }) { this.store = new ObservableStore({ swapsState: { ...initialState.swapsState }, @@ -93,6 +94,7 @@ export default class SwapsController { this._fetchTradesInfo = fetchTradesInfo; this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness; this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime; + this._getCurrentChainId = getCurrentChainId; this.getBufferedGasLimit = getBufferedGasLimit; this.tokenRatesStore = tokenRatesStore; @@ -116,10 +118,11 @@ export default class SwapsController { // Sets the refresh rate for quote updates from the MetaSwap API async _setSwapsQuoteRefreshTime() { + const chainId = this._getCurrentChainId(); // Default to fallback time unless API returns valid response let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME; try { - swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(); + swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(chainId); } catch (e) { console.error('Request for swaps quote refresh time failed: ', e); } @@ -158,6 +161,8 @@ export default class SwapsController { fetchParamsMetaData = {}, isPolledRequest, ) { + const { chainId } = fetchParamsMetaData; + if (!fetchParams) { return null; } @@ -177,7 +182,7 @@ export default class SwapsController { this.indexOfNewestCallInFlight = indexOfCurrentCall; let [newQuotes] = await Promise.all([ - this._fetchTradesInfo(fetchParams), + this._fetchTradesInfo(fetchParams, fetchParamsMetaData), this._setSwapsQuoteRefreshTime(), ]); @@ -191,12 +196,13 @@ export default class SwapsController { let approvalRequired = false; if ( - fetchParams.sourceToken !== ETH_SWAPS_TOKEN_OBJECT.address && + !isSwapsDefaultTokenAddress(fetchParams.sourceToken, chainId) && Object.values(newQuotes).length ) { const allowance = await this._getERC20Allowance( fetchParams.sourceToken, fetchParams.fromAddress, + chainId, ); // For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token. @@ -490,6 +496,7 @@ export default class SwapsController { const { swapsState: { customGasPrice }, } = this.store.getState(); + const chainId = this._getCurrentChainId(); const numQuotes = Object.keys(quotes).length; if (!numQuotes) { @@ -533,8 +540,8 @@ export default class SwapsController { // trade.value is a sum of different values depending on the transaction. // It always includes any external fees charged by the quote source. In - // addition, if the source asset is ETH, trade.value includes the amount - // of swapped ETH. + // addition, if the source asset is the selected chain's default token, trade.value + // includes the amount of that token. const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16).plus( trade.value, 16, @@ -549,21 +556,21 @@ export default class SwapsController { }); // The total fee is aggregator/exchange fees plus gas fees. - // If the swap is from ETH, subtract the sourceAmount from the total cost. - // Otherwise, the total fee is simply trade.value plus gas fees. - const ethFee = - sourceToken === ETH_SWAPS_TOKEN_OBJECT.address - ? conversionUtil( - totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei - { - fromCurrency: 'ETH', - fromDenomination: 'WEI', - toDenomination: 'ETH', - fromNumericBase: 'BN', - numberOfDecimals: 6, - }, - ) - : totalEthCost; + // If the swap is from the selected chain's default token, subtract + // the sourceAmount from the total cost. Otherwise, the total fee + // is simply trade.value plus gas fees. + const ethFee = isSwapsDefaultTokenAddress(sourceToken, chainId) + ? conversionUtil( + totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei + { + fromCurrency: 'ETH', + fromDenomination: 'WEI', + toDenomination: 'ETH', + fromNumericBase: 'BN', + numberOfDecimals: 6, + }, + ) + : totalEthCost; const decimalAdjustedDestinationAmount = calcTokenAmount( destinationAmount, @@ -588,10 +595,12 @@ export default class SwapsController { 10, ); - const conversionRateForCalculations = - destinationToken === ETH_SWAPS_TOKEN_OBJECT.address - ? 1 - : tokenConversionRate; + const conversionRateForCalculations = isSwapsDefaultTokenAddress( + destinationToken, + chainId, + ) + ? 1 + : tokenConversionRate; const overallValueOfQuoteForSorting = conversionRateForCalculations === undefined @@ -618,8 +627,10 @@ export default class SwapsController { }); const isBest = - newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_OBJECT.address || - Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]); + isSwapsDefaultTokenAddress( + newQuotes[topAggId].destinationToken, + chainId, + ) || Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]); let savings = null; @@ -664,13 +675,16 @@ export default class SwapsController { return [topAggId, newQuotes]; } - async _getERC20Allowance(contractAddress, walletAddress) { + async _getERC20Allowance(contractAddress, walletAddress, chainId) { const contract = new ethers.Contract( contractAddress, abi, this.ethersProvider, ); - return await contract.allowance(walletAddress, METASWAP_ADDRESS); + return await contract.allowance( + walletAddress, + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId], + ); } /** @@ -726,13 +740,17 @@ export default class SwapsController { async _fetchAndSetSwapsLiveness() { const { swapsState } = this.store.getState(); const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState; + const chainId = this._getCurrentChainId(); + let swapsFeatureIsLive = false; let successfullyFetched = false; let numAttempts = 0; const fetchAndIncrementNumAttempts = async () => { try { - swapsFeatureIsLive = Boolean(await this._fetchSwapsFeatureLiveness()); + swapsFeatureIsLive = Boolean( + await this._fetchSwapsFeatureLiveness(chainId), + ); successfullyFetched = true; } catch (err) { log.error(err); diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index f153558d3..031ed7863 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -944,6 +944,7 @@ export default class TransactionController extends EventEmitter { txMeta.txParams.from, txMeta.destinationTokenDecimals, approvalTxMeta, + txMeta.chainId, ); const quoteVsExecutionRatio = `${new BigNumber(tokensReceived, 10) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d0cf9c1a0..3b9dffdff 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -373,6 +373,9 @@ export default class MetamaskController extends EventEmitter { this.networkController, ), tokenRatesStore: this.tokenRatesController.store, + getCurrentChainId: this.networkController.getCurrentChainId.bind( + this.networkController, + ), }); // ensure accountTracker updates balances after network change diff --git a/package.json b/package.json index 23f92a21e..50d265a9e 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "3box/**/libp2p-crypto/node-forge": "^0.10.0", "3box/**/libp2p-keychain/node-forge": "^0.10.0", "analytics-node/axios": "^0.21.1", + "netmask": "^2.0.1", "pull-ws": "^3.3.2" }, "dependencies": { @@ -86,7 +87,7 @@ "@metamask/controllers": "^5.1.0", "@metamask/eth-ledger-bridge-keyring": "^0.3.0", "@metamask/eth-token-tracker": "^3.0.1", - "@metamask/etherscan-link": "^1.5.0", + "@metamask/etherscan-link": "^2.0.0", "@metamask/inpage-provider": "^8.0.4", "@metamask/jazzicon": "^2.0.0", "@metamask/logo": "^2.5.0", diff --git a/shared/constants/network.js b/shared/constants/network.js index a3cb2e7c9..7b1e1b2ad 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -29,6 +29,14 @@ export const KOVAN_DISPLAY_NAME = 'Kovan'; export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; export const GOERLI_DISPLAY_NAME = 'Goerli'; +export const ETH_SYMBOL = 'ETH'; +export const TEST_ETH_SYMBOL = 'TESTETH'; +export const BNB_SYMBOL = 'BNB'; + +export const ETH_TOKEN_IMAGE_URL = './images/eth_logo.svg'; +export const TEST_ETH_TOKEN_IMAGE_URL = './images/black-eth-logo.svg'; +export const BNB_TOKEN_IMAGE_URL = './images/bnb.png'; + export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI]; export const TEST_CHAINS = [ @@ -79,3 +87,9 @@ export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values( chainIdToNetworkIdMap[chainId] = networkId; return chainIdToNetworkIdMap; }, {}); + +export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = { + [ETH_SYMBOL]: ETH_TOKEN_IMAGE_URL, + [TEST_ETH_SYMBOL]: TEST_ETH_TOKEN_IMAGE_URL, + [BNB_SYMBOL]: BNB_TOKEN_IMAGE_URL, +}; diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js new file mode 100644 index 000000000..186df2774 --- /dev/null +++ b/shared/constants/swaps.js @@ -0,0 +1,89 @@ +import { + MAINNET_CHAIN_ID, + ETH_SYMBOL, + TEST_ETH_SYMBOL, + BNB_SYMBOL, + TEST_ETH_TOKEN_IMAGE_URL, + BNB_TOKEN_IMAGE_URL, +} from './network'; + +export const QUOTES_EXPIRED_ERROR = 'quotes-expired'; +export const SWAP_FAILED_ERROR = 'swap-failed-error'; +export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes'; +export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable'; +export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance'; +export const SWAPS_FETCH_ORDER_CONFLICT = 'swaps-fetch-order-conflict'; + +// An address that the metaswap-api recognizes as the default token for the current network, in place of the token address that ERC-20 tokens have +const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; + +export const ETH_SWAPS_TOKEN_OBJECT = { + symbol: ETH_SYMBOL, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: './images/black-eth-logo.svg', +}; + +export const BNB_SWAPS_TOKEN_OBJECT = { + symbol: BNB_SYMBOL, + name: 'Binance Coin', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: BNB_TOKEN_IMAGE_URL, +}; + +export const TEST_ETH_SWAPS_TOKEN_OBJECT = { + symbol: TEST_ETH_SYMBOL, + name: 'Test Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: TEST_ETH_TOKEN_IMAGE_URL, +}; + +// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations +export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0'; + +const MAINNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'; + +const TESTNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'; + +const BSC_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31'; + +const METASWAP_ETH_API_HOST = 'https://api.metaswap.codefi.network'; + +const METASWAP_BNB_API_HOST = 'https://bsc-api.metaswap.codefi.network'; +export const BNB_CHAIN_ID = '0x38'; + +const SWAPS_TESTNET_CHAIN_ID = '0x539'; +const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network'; + +const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; + +export const ALLOWED_SWAPS_CHAIN_IDS = { + [MAINNET_CHAIN_ID]: true, + [SWAPS_TESTNET_CHAIN_ID]: true, + [BNB_CHAIN_ID]: true, +}; + +export const METASWAP_CHAINID_API_HOST_MAP = { + [MAINNET_CHAIN_ID]: METASWAP_ETH_API_HOST, + [SWAPS_TESTNET_CHAIN_ID]: SWAPS_TESTNET_HOST, + [BNB_CHAIN_ID]: METASWAP_BNB_API_HOST, +}; + +export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = { + [MAINNET_CHAIN_ID]: MAINNET_CONTRACT_ADDRESS, + [SWAPS_TESTNET_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS, + [BNB_CHAIN_ID]: BSC_CONTRACT_ADDRESS, +}; + +export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { + [MAINNET_CHAIN_ID]: ETH_SWAPS_TOKEN_OBJECT, + [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, + [BNB_CHAIN_ID]: BNB_SWAPS_TOKEN_OBJECT, +}; + +export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = { + [BNB_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, +}; diff --git a/shared/modules/swaps.utils.js b/shared/modules/swaps.utils.js new file mode 100644 index 000000000..799f17d1d --- /dev/null +++ b/shared/modules/swaps.utils.js @@ -0,0 +1,33 @@ +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/swaps'; + +/** + * Checks whether the provided address is strictly equal to the address for + * the default swaps token of the provided chain. + * + * @param {string} address - The string to compare to the default token address + * @param {string} chainId - The hex encoded chain ID of the default swaps token to check + * @returns {boolean} Whether the address is the provided chain's default token address + */ +export function isSwapsDefaultTokenAddress(address, chainId) { + if (!address || !chainId) { + return false; + } + + return address === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.address; +} + +/** + * Checks whether the provided symbol is strictly equal to the symbol for + * the default swaps token of the provided chain. + * + * @param {string} symbol - The string to compare to the default token symbol + * @param {string} chainId - The hex encoded chain ID of the default swaps token to check + * @returns {boolean} Whether the symbl is the provided chain's default token symbol + */ +export function isSwapsDefaultTokenSymbol(symbol, chainId) { + if (!symbol || !chainId) { + return false; + } + + return symbol === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol; +} diff --git a/test/unit/app/controllers/swaps.test.js b/test/unit/app/controllers/swaps.test.js index 8b9a2b0f6..3109fe509 100644 --- a/test/unit/app/controllers/swaps.test.js +++ b/test/unit/app/controllers/swaps.test.js @@ -8,8 +8,9 @@ import { ObservableStore } from '@metamask/obs-store'; import { ROPSTEN_NETWORK_ID, MAINNET_NETWORK_ID, + MAINNET_CHAIN_ID, } from '../../../../shared/constants/network'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../ui/app/helpers/constants/swaps'; +import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps'; import { createTestProviderTools } from '../../../stub/provider'; import SwapsController, { utils, @@ -77,6 +78,7 @@ const MOCK_FETCH_METADATA = { symbol: 'FOO', decimals: 18, }, + chainId: MAINNET_CHAIN_ID, }; const MOCK_TOKEN_RATES_STORE = new ObservableStore({ @@ -133,6 +135,8 @@ 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); describe('SwapsController', function () { let provider; @@ -147,6 +151,7 @@ describe('SwapsController', function () { fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, + getCurrentChainId: getCurrentChainIdStub, }); }; @@ -196,6 +201,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; const onNetworkDidChange = networkController.on.getCall(0).args[1]; @@ -220,6 +226,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; const onNetworkDidChange = networkController.on.getCall(0).args[1]; @@ -244,6 +251,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; const onNetworkDidChange = networkController.on.getCall(0).args[1]; @@ -688,7 +696,10 @@ describe('SwapsController', function () { }); assert.strictEqual( - fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS), + fetchTradesInfoStub.calledOnceWithExactly( + MOCK_FETCH_PARAMS, + MOCK_FETCH_METADATA, + ), true, ); }); @@ -710,6 +721,7 @@ describe('SwapsController', function () { allowanceStub.calledOnceWithExactly( MOCK_FETCH_PARAMS.sourceToken, MOCK_FETCH_PARAMS.fromAddress, + MAINNET_CHAIN_ID, ), true, ); diff --git a/ui/app/components/app/asset-list-item/asset-list-item.js b/ui/app/components/app/asset-list-item/asset-list-item.js index 8672a8c8e..d2c9da08d 100644 --- a/ui/app/components/app/asset-list-item/asset-list-item.js +++ b/ui/app/components/app/asset-list-item/asset-list-item.js @@ -26,6 +26,7 @@ const AssetListItem = ({ warning, primary, secondary, + identiconBorder, }) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -115,6 +116,7 @@ const AssetListItem = ({ address={tokenAddress} image={tokenImage} alt={`${primary} ${tokenSymbol}`} + imageBorder={identiconBorder} /> } midContent={midContent} @@ -140,6 +142,7 @@ AssetListItem.propTypes = { warning: PropTypes.node, primary: PropTypes.string, secondary: PropTypes.string, + identiconBorder: PropTypes.bool, }; AssetListItem.defaultProps = { diff --git a/ui/app/components/app/asset-list/asset-list.js b/ui/app/components/app/asset-list/asset-list.js index 7a817d1cb..e4b9efea4 100644 --- a/ui/app/components/app/asset-list/asset-list.js +++ b/ui/app/components/app/asset-list/asset-list.js @@ -13,6 +13,7 @@ import { getCurrentAccountWithSendEtherInfo, getNativeCurrency, getShouldShowFiat, + getNativeCurrencyImage, } from '../../../selectors'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; @@ -63,6 +64,8 @@ const AssetList = ({ onClickAsset }) => { }, ); + const primaryTokenImage = useSelector(getNativeCurrencyImage); + return ( <> { primary={primaryCurrencyProperties.value} tokenSymbol={primaryCurrencyProperties.suffix} secondary={showFiat ? secondaryCurrencyDisplay : undefined} + tokenImage={primaryTokenImage} + identiconBorder /> { diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index e69523329..7f20e03a8 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -5,20 +5,31 @@ import { nonceSortedCompletedTransactionsSelector, nonceSortedPendingTransactionsSelector, } from '../../../selectors/transactions'; +import { getCurrentChainId } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; import TransactionListItem from '../transaction-list-item'; import Button from '../../ui/button'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; -import { SWAPS_CONTRACT_ADDRESS } from '../../../helpers/constants/swaps'; +import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../../shared/constants/swaps'; import { TRANSACTION_CATEGORIES } from '../../../../../shared/constants/transaction'; const PAGE_INCREMENT = 10; -const getTransactionGroupRecipientAddressFilter = (recipientAddress) => { +// When we are on a token page, we only want to show transactions that involve that token. +// In the case of token transfers or approvals, these will be transactions sent to the +// token contract. In the case of swaps, these will be transactions sent to the swaps contract +// and which have the token address in the transaction data. +// +// getTransactionGroupRecipientAddressFilter is used to determine whether a transaction matches +// either of those criteria +const getTransactionGroupRecipientAddressFilter = ( + recipientAddress, + chainId, +) => { return ({ initialTransaction: { txParams } }) => { return ( txParams?.to === recipientAddress || - (txParams?.to === SWAPS_CONTRACT_ADDRESS && + (txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] && txParams.data.match(recipientAddress.slice(2))) ); }; @@ -43,12 +54,13 @@ const getFilteredTransactionGroups = ( transactionGroups, hideTokenTransactions, tokenAddress, + chainId, ) => { if (hideTokenTransactions) { return transactionGroups.filter(tokenTransactionFilter); } else if (tokenAddress) { return transactionGroups.filter( - getTransactionGroupRecipientAddressFilter(tokenAddress), + getTransactionGroupRecipientAddressFilter(tokenAddress, chainId), ); } return transactionGroups; @@ -67,6 +79,7 @@ export default function TransactionList({ const unfilteredCompletedTransactions = useSelector( nonceSortedCompletedTransactionsSelector, ); + const chainId = useSelector(getCurrentChainId); const pendingTransactions = useMemo( () => @@ -74,8 +87,14 @@ export default function TransactionList({ unfilteredPendingTransactions, hideTokenTransactions, tokenAddress, + chainId, ), - [hideTokenTransactions, tokenAddress, unfilteredPendingTransactions], + [ + hideTokenTransactions, + tokenAddress, + unfilteredPendingTransactions, + chainId, + ], ); const completedTransactions = useMemo( () => @@ -83,8 +102,14 @@ export default function TransactionList({ unfilteredCompletedTransactions, hideTokenTransactions, tokenAddress, + chainId, ), - [hideTokenTransactions, tokenAddress, unfilteredCompletedTransactions], + [ + hideTokenTransactions, + tokenAddress, + unfilteredCompletedTransactions, + chainId, + ], ); const viewMore = useCallback( diff --git a/ui/app/components/app/wallet-overview/eth-overview.js b/ui/app/components/app/wallet-overview/eth-overview.js index 38ada5c24..df79de100 100644 --- a/ui/app/components/app/wallet-overview/eth-overview.js +++ b/ui/app/components/app/wallet-overview/eth-overview.js @@ -25,7 +25,9 @@ import { getIsMainnet, getIsTestnet, getCurrentKeyring, - getSwapsEthToken, + getSwapsDefaultToken, + getIsSwapsChain, + getNativeCurrencyImage, } from '../../../selectors/selectors'; import SwapIcon from '../../ui/icon/swap-icon.component'; import BuyIcon from '../../ui/icon/overview-buy-icon.component'; @@ -63,13 +65,16 @@ const EthOverview = ({ className }) => { const { balance } = selectedAccount; const isMainnetChain = useSelector(getIsMainnet); const isTestnetChain = useSelector(getIsTestnet); + const isSwapsChain = useSelector(getIsSwapsChain); + const primaryTokenImage = useSelector(getNativeCurrencyImage); + const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Main View', active_currency: 'ETH' }, category: 'swaps', }); const swapsEnabled = useSelector(getSwapsFeatureLiveness); - const swapsEthToken = useSelector(getSwapsEthToken); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); return ( { {swapsEnabled ? ( { - if (isMainnetChain) { + if (isSwapsChain) { enteredSwapsEvent(); - dispatch(setSwapsFromToken(swapsEthToken)); + dispatch(setSwapsFromToken(defaultSwapsToken)); if (usingHardwareWallet) { global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); } else { @@ -154,7 +159,7 @@ const EthOverview = ({ className }) => { {contents} @@ -164,7 +169,7 @@ const EthOverview = ({ className }) => { } className={className} - icon={} + icon={} /> ); }; diff --git a/ui/app/components/app/wallet-overview/token-overview.js b/ui/app/components/app/wallet-overview/token-overview.js index e705461d9..953de1fd8 100644 --- a/ui/app/components/app/wallet-overview/token-overview.js +++ b/ui/app/components/app/wallet-overview/token-overview.js @@ -25,9 +25,8 @@ import { import { getAssetImages, getCurrentKeyring, - getCurrentChainId, + getIsSwapsChain, } from '../../../selectors/selectors'; -import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network'; import SwapIcon from '../../ui/icon/swap-icon.component'; import SendIcon from '../../ui/icon/overview-send-icon.component'; @@ -58,7 +57,7 @@ const TokenOverview = ({ className, token }) => { balanceToRender, token.symbol, ); - const chainId = useSelector(getCurrentChainId); + const isSwapsChain = useSelector(getIsSwapsChain); const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Token View', active_currency: token.symbol }, @@ -100,10 +99,10 @@ const TokenOverview = ({ className, token }) => { {swapsEnabled ? ( { - if (chainId === MAINNET_CHAIN_ID) { + if (isSwapsChain) { enteredSwapsEvent(); dispatch( setSwapsFromToken({ @@ -125,7 +124,7 @@ const TokenOverview = ({ className, token }) => { {contents} diff --git a/ui/app/components/ui/identicon/identicon.component.js b/ui/app/components/ui/identicon/identicon.component.js index 91f3a11f5..c999b6354 100644 --- a/ui/app/components/ui/identicon/identicon.component.js +++ b/ui/app/components/ui/identicon/identicon.component.js @@ -22,6 +22,7 @@ export default class Identicon extends PureComponent { image: PropTypes.string, useBlockie: PropTypes.bool, alt: PropTypes.string, + imageBorder: PropTypes.bool, }; static defaultProps = { @@ -35,11 +36,13 @@ export default class Identicon extends PureComponent { }; renderImage() { - const { className, diameter, image, alt } = this.props; + const { className, diameter, image, alt, imageBorder } = this.props; return ( {alt} +
); } } diff --git a/ui/app/components/ui/identicon/index.scss b/ui/app/components/ui/identicon/index.scss index 427ac8d45..a467f4bc5 100644 --- a/ui/app/components/ui/identicon/index.scss +++ b/ui/app/components/ui/identicon/index.scss @@ -20,7 +20,7 @@ border-color: $primary-blue; } - &__eth-logo { + &__image-border { border: 1px solid $alto; background: $white; } diff --git a/ui/app/components/ui/identicon/tests/identicon.component.test.js b/ui/app/components/ui/identicon/tests/identicon.component.test.js index 3d33397ea..a482037e0 100644 --- a/ui/app/components/ui/identicon/tests/identicon.component.test.js +++ b/ui/app/components/ui/identicon/tests/identicon.component.test.js @@ -16,13 +16,10 @@ describe('Identicon', function () { const mockStore = configureMockStore(middlewares); const store = mockStore(state); - it('renders default eth_logo identicon with no props', function () { + it('renders empty identicon with no props', function () { const wrapper = mount(); - assert.strictEqual( - wrapper.find('img.identicon__eth-logo').prop('src'), - './images/eth_logo.svg', - ); + assert.ok(wrapper.find('div'), 'Empty identicon found'); }); it('renders custom image and add className props', function () { diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 1354596e2..06e4c759b 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -2,8 +2,6 @@ import { createSlice } from '@reduxjs/toolkit'; import BigNumber from 'bignumber.js'; import log from 'loglevel'; -import { getStorageItem, setStorageItem } from '../../../lib/storage-helpers'; - import { addToken, addUnapprovedTransaction, @@ -41,7 +39,6 @@ import { decimalToHex, getValueFromWeiHex, decGWEIToHexWEI, - hexToDecimal, hexWEIToDecGWEI, } from '../../helpers/utils/conversions.util'; import { conversionLessThan } from '../../helpers/utils/conversion-util'; @@ -50,14 +47,15 @@ import { getSelectedAccount, getTokenExchangeRates, getUSDConversionRate, + getSwapsDefaultToken, + getCurrentChainId, } from '../../selectors'; import { ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, - ETH_SWAPS_TOKEN_OBJECT, SWAP_FAILED_ERROR, SWAPS_FETCH_ORDER_CONFLICT, -} from '../../helpers/constants/swaps'; +} from '../../../../shared/constants/swaps'; import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'; const GAS_PRICES_LOADING_STATES = { @@ -83,7 +81,6 @@ const initialState = { limit: null, loading: GAS_PRICES_LOADING_STATES.INITIAL, priceEstimates: {}, - priceEstimatesLastRetrieved: 0, fallBackPrice: null, }, }; @@ -145,8 +142,6 @@ const slice = createSlice({ swapGasPriceEstimatesFetchCompleted: (state, action) => { state.customGas.priceEstimates = action.payload.priceEstimates; state.customGas.loading = GAS_PRICES_LOADING_STATES.COMPLETED; - state.customGas.priceEstimatesLastRetrieved = - action.payload.priceEstimatesLastRetrieved; }, retrievedFallbackSwapsGasPrice: (state, action) => { state.customGas.fallBackPrice = action.payload; @@ -190,9 +185,6 @@ export const swapGasEstimateLoadingHasFailed = (state) => export const getSwapGasPriceEstimateData = (state) => state.swaps.customGas.priceEstimates; -export const getSwapsPriceEstimatesLastRetrieved = (state) => - state.swaps.customGas.priceEstimatesLastRetrieved; - export const getSwapsFallbackGasPrice = (state) => state.swaps.customGas.fallBackPrice; @@ -377,9 +369,11 @@ export const fetchQuotesAndSetQuoteState = ( metaMetricsEvent, ) => { return async (dispatch, getState) => { + const state = getState(); + const chainId = getCurrentChainId(state); let swapsFeatureIsLive = false; try { - swapsFeatureIsLive = await fetchSwapsFeatureLiveness(); + swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); } catch (error) { log.error('Failed to fetch Swaps liveness, defaulting to false.', error); } @@ -390,21 +384,14 @@ export const fetchQuotesAndSetQuoteState = ( return; } - const state = getState(); const fetchParams = getFetchParams(state); const selectedAccount = getSelectedAccount(state); const balanceError = getBalanceError(state); + const swapsDefaultToken = getSwapsDefaultToken(state); const fetchParamsFromToken = - fetchParams?.metaData?.sourceTokenInfo?.symbol === 'ETH' - ? { - ...ETH_SWAPS_TOKEN_OBJECT, - string: getValueFromWeiHex({ - value: selectedAccount.balance, - numberOfDecimals: 4, - toDenomination: 'ETH', - }), - balance: hexToDecimal(selectedAccount.balance), - } + fetchParams?.metaData?.sourceTokenInfo?.symbol === + swapsDefaultToken.symbol + ? swapsDefaultToken : fetchParams?.metaData?.sourceTokenInfo; const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {}; const selectedToToken = @@ -429,7 +416,10 @@ export const fetchQuotesAndSetQuoteState = ( const contractExchangeRates = getTokenExchangeRates(state); let destinationTokenAddedForSwap = false; - if (toTokenSymbol !== 'ETH' && !contractExchangeRates[toTokenAddress]) { + if ( + toTokenSymbol !== swapsDefaultToken.symbol && + !contractExchangeRates[toTokenAddress] + ) { destinationTokenAddedForSwap = true; await dispatch( addToken( @@ -442,7 +432,7 @@ export const fetchQuotesAndSetQuoteState = ( ); } if ( - fromTokenSymbol !== 'ETH' && + fromTokenSymbol !== swapsDefaultToken.symbol && !contractExchangeRates[fromTokenAddress] && fromTokenBalance && new BigNumber(fromTokenBalance, 16).gt(0) @@ -503,6 +493,7 @@ export const fetchQuotesAndSetQuoteState = ( sourceTokenInfo, destinationTokenInfo, accountBalance: selectedAccount.balance, + chainId, }, ), ); @@ -572,9 +563,12 @@ export const fetchQuotesAndSetQuoteState = ( export const signAndSendTransactions = (history, metaMetricsEvent) => { return async (dispatch, getState) => { + const state = getState(); + const chainId = getCurrentChainId(state); + let swapsFeatureIsLive = false; try { - swapsFeatureIsLive = await fetchSwapsFeatureLiveness(); + swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); } catch (error) { log.error('Failed to fetch Swaps liveness, defaulting to false.', error); } @@ -585,7 +579,6 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { return; } - const state = getState(); const customSwapsGas = getCustomSwapsGas(state); const fetchParams = getFetchParams(state); const { metaData, value: swapTokenValue, slippage } = fetchParams; @@ -746,26 +739,13 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { export function fetchMetaSwapsGasPriceEstimates() { return async (dispatch, getState) => { const state = getState(); - const priceEstimatesLastRetrieved = getSwapsPriceEstimatesLastRetrieved( - state, - ); - const timeLastRetrieved = - priceEstimatesLastRetrieved || - (await getStorageItem('METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED')) || - 0; + const chainId = getCurrentChainId(state); dispatch(swapGasPriceEstimatesFetchStarted()); let priceEstimates; try { - if (Date.now() - timeLastRetrieved > 30000) { - priceEstimates = await fetchSwapsGasPrices(); - } else { - const cachedPriceEstimates = await getStorageItem( - 'METASWAP_GAS_PRICE_ESTIMATES', - ); - priceEstimates = cachedPriceEstimates || (await fetchSwapsGasPrices()); - } + priceEstimates = await fetchSwapsGasPrices(chainId); } catch (e) { log.warn('Fetching swaps gas prices failed:', e); @@ -790,20 +770,9 @@ export function fetchMetaSwapsGasPriceEstimates() { } } - const timeRetrieved = Date.now(); - - await Promise.all([ - setStorageItem('METASWAP_GAS_PRICE_ESTIMATES', priceEstimates), - setStorageItem( - 'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED', - timeRetrieved, - ), - ]); - dispatch( swapGasPriceEstimatesFetchCompleted({ priceEstimates, - priceEstimatesLastRetrieved: timeRetrieved, }), ); return priceEstimates; diff --git a/ui/app/helpers/constants/swaps.js b/ui/app/helpers/constants/swaps.js deleted file mode 100644 index 7ae20c5ae..000000000 --- a/ui/app/helpers/constants/swaps.js +++ /dev/null @@ -1,25 +0,0 @@ -// An address that the metaswap-api recognizes as ETH, in place of the token address that ERC-20 tokens have -const ETH_SWAPS_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; - -export const ETH_SWAPS_TOKEN_OBJECT = { - symbol: 'ETH', - name: 'Ether', - address: ETH_SWAPS_TOKEN_ADDRESS, - decimals: 18, - iconUrl: 'images/black-eth-logo.svg', -}; - -export const QUOTES_EXPIRED_ERROR = 'quotes-expired'; -export const SWAP_FAILED_ERROR = 'swap-failed-error'; -export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes'; -export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable'; -export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance'; -export const SWAPS_FETCH_ORDER_CONFLICT = 'swaps-fetch-order-conflict'; - -// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations -export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0'; - -export const SWAPS_CONTRACT_ADDRESS = - '0x881d40237659c251811cec9c364ef91dc08d300c'; - -export const METASWAP_API_HOST = 'https://api.metaswap.codefi.network'; diff --git a/ui/app/helpers/utils/formatters.js b/ui/app/helpers/utils/formatters.js index c75e30f1c..710770bf3 100644 --- a/ui/app/helpers/utils/formatters.js +++ b/ui/app/helpers/utils/formatters.js @@ -1,3 +1,4 @@ -export function formatETHFee(ethFee) { - return `${ethFee} ETH`; +// TODO: Rename to reflect that this function is used for more cases than ETH, and update all uses. +export function formatETHFee(ethFee, currencySymbol = 'ETH') { + return `${ethFee} ${currencySymbol}`; } diff --git a/ui/app/hooks/tests/useTransactionDisplayData.test.js b/ui/app/hooks/tests/useTransactionDisplayData.test.js index 96a24c08b..4f6d4b3ff 100644 --- a/ui/app/hooks/tests/useTransactionDisplayData.test.js +++ b/ui/app/hooks/tests/useTransactionDisplayData.test.js @@ -12,12 +12,14 @@ import { getShouldShowFiat, getNativeCurrency, getCurrentCurrency, + getCurrentChainId, } from '../../selectors'; import { getTokens } from '../../ducks/metamask/metamask'; import * as i18nhooks from '../useI18nContext'; import { getMessage } from '../../helpers/utils/i18n-helper'; import messages from '../../../../app/_locales/en/messages.json'; import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'; +import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; import { TRANSACTION_CATEGORIES, TRANSACTION_GROUP_CATEGORIES, @@ -164,6 +166,8 @@ describe('useTransactionDisplayData', function () { return 'ETH'; } else if (selector === getCurrentCurrency) { return 'ETH'; + } else if (selector === getCurrentChainId) { + return MAINNET_CHAIN_ID; } return null; }); diff --git a/ui/app/hooks/useCurrentAsset.js b/ui/app/hooks/useCurrentAsset.js index d1d144bed..832576a0c 100644 --- a/ui/app/hooks/useCurrentAsset.js +++ b/ui/app/hooks/useCurrentAsset.js @@ -1,13 +1,18 @@ import { useSelector } from 'react-redux'; import { useRouteMatch } from 'react-router-dom'; import { getTokens } from '../ducks/metamask/metamask'; +import { getCurrentChainId } from '../selectors'; import { ASSET_ROUTE } from '../helpers/constants/routes'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + ETH_SWAPS_TOKEN_OBJECT, +} from '../../../shared/constants/swaps'; /** * Returns a token object for the asset that is currently being viewed. - * Will return the ETH_SWAPS_TOKEN_OBJECT when the user is viewing either - * the primary, unfiltered, activity list or the ETH asset page. + * Will return the default token object for the current chain when the + * user is viewing either the primary, unfiltered, activity list or the + * default token asset page. * @returns {import('./useTokenDisplayValue').Token} */ export function useCurrentAsset() { @@ -22,6 +27,10 @@ export function useCurrentAsset() { const knownTokens = useSelector(getTokens); const token = tokenAddress && knownTokens.find(({ address }) => address === tokenAddress); + const chainId = useSelector(getCurrentChainId); - return token ?? ETH_SWAPS_TOKEN_OBJECT; + return ( + token ?? + (SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId] || ETH_SWAPS_TOKEN_OBJECT) + ); } diff --git a/ui/app/hooks/useSwappedTokenValue.js b/ui/app/hooks/useSwappedTokenValue.js index f48e3de69..efe3c8cc2 100644 --- a/ui/app/hooks/useSwappedTokenValue.js +++ b/ui/app/hooks/useSwappedTokenValue.js @@ -1,6 +1,11 @@ +import { useSelector } from 'react-redux'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../shared/modules/swaps.utils'; import { TRANSACTION_CATEGORIES } from '../../../shared/constants/transaction'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps'; import { getSwapsTokensReceivedFromTxMeta } from '../pages/swaps/swaps.util'; +import { getCurrentChainId } from '../selectors'; import { useTokenFiatAmount } from './useTokenFiatAmount'; /** @@ -14,10 +19,11 @@ import { useTokenFiatAmount } from './useTokenFiatAmount'; /** * A Swap transaction group's primaryTransaction contains details of the swap, * including the source (from) and destination (to) token type (ETH, DAI, etc..) - * When viewing a non ETH asset page, we need to determine if that asset is the - * token that was received (destination) from the swap. In that circumstance we - * would want to show the primaryCurrency in the activity list that is most relevant - * for that token (- 1000 DAI, for example, when swapping DAI for ETH). + * When viewing an asset page that is not for the current chain's default token, we + * need to determine if that asset is the token that was received (destination) from + * the swap. In that circumstance we would want to show the primaryCurrency in the + * activity list that is most relevant for that token (- 1000 DAI, for example, when + * swapping DAI for ETH). * @param {import('../selectors').transactionGroup} transactionGroup - Group of transactions by nonce * @param {import('./useTokenDisplayValue').Token} currentAsset - The current asset the user is looking at * @returns {SwappedTokenValue} @@ -27,11 +33,15 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) { const { primaryTransaction, initialTransaction } = transactionGroup; const { transactionCategory } = initialTransaction; const { from: senderAddress } = initialTransaction.txParams || {}; + const chainId = useSelector(getCurrentChainId); const isViewingReceivedTokenFromSwap = currentAsset?.symbol === primaryTransaction.destinationTokenSymbol || - (currentAsset.address === ETH_SWAPS_TOKEN_OBJECT.address && - primaryTransaction.destinationTokenSymbol === 'ETH'); + (isSwapsDefaultTokenAddress(currentAsset.address, chainId) && + isSwapsDefaultTokenSymbol( + primaryTransaction.destinationTokenSymbol, + chainId, + )); const swapTokenValue = transactionCategory === TRANSACTION_CATEGORIES.SWAP && @@ -42,6 +52,8 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) { address, senderAddress, decimals, + null, + chainId, ) : transactionCategory === TRANSACTION_CATEGORIES.SWAP && primaryTransaction.swapTokenValue; diff --git a/ui/app/hooks/useTokensToSearch.js b/ui/app/hooks/useTokensToSearch.js index 3336ceae0..4542882f0 100644 --- a/ui/app/hooks/useTokensToSearch.js +++ b/ui/app/hooks/useTokensToSearch.js @@ -9,9 +9,11 @@ import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, - getSwapsEthToken, + getSwapsDefaultToken, + getCurrentChainId, } from '../selectors'; import { getSwapsTokens } from '../ducks/swaps/swaps'; +import { isSwapsDefaultTokenSymbol } from '../../../shared/modules/swaps.utils'; import { useEqualityCheck } from './useEqualityCheck'; const tokenList = shuffle( @@ -28,12 +30,15 @@ export function getRenderableTokenData( contractExchangeRates, conversionRate, currentCurrency, + chainId, ) { const { symbol, name, address, iconUrl, string, balance, decimals } = token; const formattedFiat = getTokenFiatAmount( - symbol === 'ETH' ? 1 : contractExchangeRates[address], + isSwapsDefaultTokenSymbol(symbol, chainId) + ? 1 + : contractExchangeRates[address], conversionRate, currentCurrency, string, @@ -42,7 +47,9 @@ export function getRenderableTokenData( ) || ''; const rawFiat = getTokenFiatAmount( - symbol === 'ETH' ? 1 : contractExchangeRates[address], + isSwapsDefaultTokenSymbol(symbol, chainId) + ? 1 + : contractExchangeRates[address], conversionRate, currentCurrency, string, @@ -69,42 +76,36 @@ export function getRenderableTokenData( }; } -export function useTokensToSearch({ - providedTokens, - usersTokens = [], - topTokens = {}, - onlyEth, - singleToken, -}) { +export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { + const chainId = useSelector(getCurrentChainId); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const conversionRate = useSelector(getConversionRate); const currentCurrency = useSelector(getCurrentCurrency); - const swapsEthToken = useSelector(getSwapsEthToken); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); const memoizedTopTokens = useEqualityCheck(topTokens); const memoizedUsersToken = useEqualityCheck(usersTokens); - const ethToken = getRenderableTokenData( - swapsEthToken, + const defaultToken = getRenderableTokenData( + defaultSwapsToken, tokenConversionRates, conversionRate, currentCurrency, + chainId, ); - const memoizedEthToken = useEqualityCheck(ethToken); + const memoizedDefaultToken = useEqualityCheck(defaultToken); const swapsTokens = useSelector(getSwapsTokens) || []; - let tokensToSearch; - if (onlyEth) { - tokensToSearch = [memoizedEthToken]; - } else if (singleToken) { - tokensToSearch = providedTokens; - } else if (providedTokens) { - tokensToSearch = [memoizedEthToken, ...providedTokens]; - } else if (swapsTokens.length) { - tokensToSearch = [memoizedEthToken, ...swapsTokens]; - } else { - tokensToSearch = [memoizedEthToken, ...tokenList]; - } + + const tokensToSearch = swapsTokens.length + ? swapsTokens + : [ + memoizedDefaultToken, + ...tokenList.filter( + (token) => token.symbol !== memoizedDefaultToken.symbol, + ), + ]; + const memoizedTokensToSearch = useEqualityCheck(tokensToSearch); return useMemo(() => { const usersTokensAddressMap = memoizedUsersToken.reduce( @@ -113,7 +114,7 @@ export function useTokensToSearch({ ); const tokensToSearchBuckets = { - owned: singleToken ? [] : [memoizedEthToken], + owned: [], top: [], others: [], }; @@ -124,10 +125,11 @@ export function useTokensToSearch({ tokenConversionRates, conversionRate, currentCurrency, + chainId, ); if ( - usersTokensAddressMap[token.address] && - (renderableDataToken.symbol === 'ETH' || + isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) || + (usersTokensAddressMap[token.address] && Number(renderableDataToken.balance ?? 0) !== 0) ) { tokensToSearchBuckets.owned.push(renderableDataToken); @@ -158,7 +160,6 @@ export function useTokensToSearch({ conversionRate, currentCurrency, memoizedTopTokens, - memoizedEthToken, - singleToken, + chainId, ]); } diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js index 589eb048e..b4bcc2118 100644 --- a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js +++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -4,7 +4,7 @@ import SendRowWrapper from '../send-row-wrapper'; import Identicon from '../../../../components/ui/identicon/identicon.component'; import TokenBalance from '../../../../components/ui/token-balance'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'; -import { ERC20, ETH, PRIMARY } from '../../../../helpers/constants/common'; +import { ERC20, PRIMARY } from '../../../../helpers/constants/common'; export default class SendAssetRow extends Component { static propTypes = { @@ -20,6 +20,7 @@ export default class SendAssetRow extends Component { sendTokenAddress: PropTypes.string, setSendToken: PropTypes.func.isRequired, nativeCurrency: PropTypes.string, + nativeCurrencyImage: PropTypes.string, }; static contextTypes = { @@ -103,7 +104,12 @@ export default class SendAssetRow extends Component { renderNativeCurrency(insideDropdown = false) { const { t } = this.context; - const { accounts, selectedAddress, nativeCurrency } = this.props; + const { + accounts, + selectedAddress, + nativeCurrency, + nativeCurrencyImage, + } = this.props; const balanceValue = accounts[selectedAddress] ? accounts[selectedAddress].balance @@ -121,7 +127,8 @@ export default class SendAssetRow extends Component {
diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js index 5eee73797..f536c4fd7 100644 --- a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js +++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { getMetaMaskAccounts, getNativeCurrency, + getNativeCurrencyImage, getSendTokenAddress, } from '../../../../selectors'; import { updateSendToken } from '../../../../store/actions'; @@ -14,6 +15,7 @@ function mapStateToProps(state) { sendTokenAddress: getSendTokenAddress(state), accounts: getMetaMaskAccounts(state), nativeCurrency: getNativeCurrency(state), + nativeCurrencyImage: getNativeCurrencyImage(state), }; } diff --git a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js index b02639b26..af21ce29b 100644 --- a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js @@ -3,15 +3,18 @@ import React, { useContext, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; +import { createCustomExplorerLink } from '@metamask/etherscan-link'; import { I18nContext } from '../../../contexts/i18n'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { MetaMetricsContext } from '../../../contexts/metametrics.new'; + import { getCurrentChainId, getCurrentCurrency, getRpcPrefsForCurrentProvider, getUSDConversionRate, } from '../../../selectors'; + import { getUsedQuote, getFetchParams, @@ -23,19 +26,24 @@ import { prepareToLeaveSwaps, } from '../../../ducks/swaps/swaps'; import Mascot from '../../../components/ui/mascot'; -import PulseLoader from '../../../components/ui/pulse-loader'; import { QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, -} from '../../../helpers/constants/swaps'; + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, +} from '../../../../../shared/constants/swaps'; +import { CHAIN_ID_TO_TYPE_MAP as VALID_INFURA_CHAIN_IDS } from '../../../../../shared/constants/network'; +import { isSwapsDefaultTokenSymbol } from '../../../../../shared/modules/swaps.utils'; +import PulseLoader from '../../../components/ui/pulse-loader'; + import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getRenderableNetworkFeesForQuote } from '../swaps.util'; import SwapsFooter from '../swaps-footer'; import { getBlockExplorerUrlForTx } from '../../../../../shared/modules/transaction.utils'; + import SwapFailureIcon from './swap-failure-icon'; import SwapSuccessIcon from './swap-success-icon'; import QuotesTimeoutIcon from './quotes-timeout-icon'; @@ -73,16 +81,17 @@ export default function AwaitingSwap({ let feeinUnformattedFiat; if (usedQuote && swapsGasPrice) { - const renderableNetworkFees = getRenderableNetworkFeesForQuote( - usedQuote.gasEstimateWithRefund || usedQuote.averageGas, - approveTxParams?.gas || '0x0', - swapsGasPrice, + const renderableNetworkFees = getRenderableNetworkFeesForQuote({ + tradeGas: usedQuote.gasEstimateWithRefund || usedQuote.averageGas, + approveGas: approveTxParams?.gas || '0x0', + gasPrice: swapsGasPrice, currentCurrency, - usdConversionRate, - usedQuote?.trade?.value, - sourceTokenInfo?.symbol, - usedQuote.sourceAmount, - ); + conversionRate: usdConversionRate, + tradeValue: usedQuote?.trade?.value, + sourceSymbol: sourceTokenInfo?.symbol, + sourceAmount: usedQuote.sourceAmount, + chainId, + }); feeinUnformattedFiat = renderableNetworkFees.rawNetworkFees; } @@ -100,8 +109,21 @@ export default function AwaitingSwap({ category: 'swaps', }); - const blockExplorerUrl = - txHash && getBlockExplorerUrlForTx({ chainId, hash: txHash }, rpcPrefs); + let blockExplorerUrl; + if (txHash && rpcPrefs.blockExplorerUrl) { + blockExplorerUrl = getBlockExplorerUrlForTx({ hash: txHash }, rpcPrefs); + } else if (txHash && SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { + blockExplorerUrl = createCustomExplorerLink( + txHash, + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], + ); + } else if (txHash && VALID_INFURA_CHAIN_IDS[chainId]) { + blockExplorerUrl = getBlockExplorerUrlForTx({ chainId, hash: txHash }); + } + + const isCustomBlockExplorerUrl = + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] || + rpcPrefs.blockExplorerUrl; let headerText; let statusImage; @@ -133,7 +155,7 @@ export default function AwaitingSwap({ ); } else if (errorKey === QUOTES_EXPIRED_ERROR) { @@ -172,7 +194,7 @@ export default function AwaitingSwap({ ); } else if (!errorKey && swapComplete) { @@ -191,7 +213,7 @@ export default function AwaitingSwap({ ); } @@ -228,7 +250,9 @@ export default function AwaitingSwap({ ); } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)); - } else if (destinationTokenInfo?.symbol === 'ETH') { + } else if ( + isSwapsDefaultTokenSymbol(destinationTokenInfo?.symbol, chainId) + ) { history.push(DEFAULT_ROUTE); } else { history.push(`${ASSET_ROUTE}/${destinationTokenInfo?.address}`); diff --git a/ui/app/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js b/ui/app/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js index bb3d07cda..15fb7d081 100644 --- a/ui/app/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js +++ b/ui/app/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js @@ -18,7 +18,7 @@ export default function ViewOnEtherScanLink({ onClick={() => global.platform.openTab({ url: blockExplorerUrl })} > {isCustomBlockExplorerUrl - ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) + ? t('viewOnCustomBlockExplorer', [new URL(blockExplorerUrl).hostname]) : t('viewOnEtherscan')}
); diff --git a/ui/app/pages/swaps/build-quote/build-quote.js b/ui/app/pages/swaps/build-quote/build-quote.js index 9237016ea..dce6ffafc 100644 --- a/ui/app/pages/swaps/build-quote/build-quote.js +++ b/ui/app/pages/swaps/build-quote/build-quote.js @@ -2,10 +2,17 @@ import React, { useContext, useEffect, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; -import { uniqBy } from 'lodash'; +import { uniqBy, isEqual } from 'lodash'; import { useHistory } from 'react-router-dom'; +import { + createCustomTokenTrackerLink, + createTokenTrackerLinkForChain, +} from '@metamask/etherscan-link'; import { MetaMetricsContext } from '../../../contexts/metametrics.new'; -import { useTokensToSearch } from '../../../hooks/useTokensToSearch'; +import { + useTokensToSearch, + getRenderableTokenData, +} from '../../../hooks/useTokensToSearch'; import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; import { I18nContext } from '../../../contexts/i18n'; import DropdownInputPair from '../dropdown-input-pair'; @@ -25,7 +32,14 @@ import { getTopAssets, getFetchParams, } from '../../../ducks/swaps/swaps'; -import { getSwapsEthToken } from '../../../selectors'; +import { + getSwapsDefaultToken, + getTokenExchangeRates, + getConversionRate, + getCurrentCurrency, + getCurrentChainId, + getRpcPrefsForCurrentProvider, +} from '../../../selectors'; import { getValueFromWeiHex, hexToDecimal, @@ -36,7 +50,11 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../helpers/constants/swaps'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../../../shared/modules/swaps.utils'; +import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/swaps'; import { resetSwapsPostFetchState, removeToken } from '../../../store/actions'; import { fetchTokenPrice, fetchTokenBalance } from '../swaps.util'; @@ -76,9 +94,20 @@ export default function BuildQuote({ const topAssets = useSelector(getTopAssets); const fromToken = useSelector(getFromToken); const toToken = useSelector(getToToken) || destinationTokenInfo; - const swapsEthToken = useSelector(getSwapsEthToken); - const fetchParamsFromToken = - sourceTokenInfo?.symbol === 'ETH' ? swapsEthToken : sourceTokenInfo; + const defaultSwapsToken = useSelector(getSwapsDefaultToken); + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + + const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); + const conversionRate = useSelector(getConversionRate); + const currentCurrency = useSelector(getCurrentCurrency); + + const fetchParamsFromToken = isSwapsDefaultTokenSymbol( + sourceTokenInfo?.symbol, + chainId, + ) + ? defaultSwapsToken + : sourceTokenInfo; const { loading, tokensWithBalances } = useTokenTracker(tokens); @@ -86,22 +115,22 @@ export default function BuildQuote({ // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that // the balance of the token can appear in the from token selection dropdown const fromTokenArray = - fromToken?.symbol !== 'ETH' && fromToken?.balance ? [fromToken] : []; + !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance + ? [fromToken] + : []; const usersTokens = uniqBy( [...tokensWithBalances, ...tokens, ...fromTokenArray], 'address', ); const memoizedUsersTokens = useEqualityCheck(usersTokens); - const selectedFromToken = useTokensToSearch({ - providedTokens: - fromToken || fetchParamsFromToken - ? [fromToken || fetchParamsFromToken] - : [], - usersTokens: memoizedUsersTokens, - onlyEth: (fromToken || fetchParamsFromToken)?.symbol === 'ETH', - singleToken: true, - })[0]; + const selectedFromToken = getRenderableTokenData( + fromToken || fetchParamsFromToken, + tokenConversionRates, + conversionRate, + currentCurrency, + chainId, + ); const tokensToSearch = useTokensToSearch({ usersTokens: memoizedUsersTokens, @@ -110,9 +139,9 @@ export default function BuildQuote({ const selectedToToken = tokensToSearch.find(({ address }) => address === toToken?.address) || toToken; - const toTokenIsNotEth = + const toTokenIsNotDefault = selectedToToken?.address && - selectedToToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address; + !isSwapsDefaultTokenAddress(selectedToToken?.address, chainId); const occurances = Number(selectedToToken?.occurances || 0); const { address: fromTokenAddress, @@ -142,8 +171,9 @@ export default function BuildQuote({ { showFiat: true }, true, ); - const swapFromFiatValue = - fromTokenSymbol === 'ETH' ? swapFromEthFiatValue : swapFromTokenFiatValue; + const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) + ? swapFromEthFiatValue + : swapFromTokenFiatValue; const onFromSelect = (token) => { if ( @@ -192,6 +222,30 @@ export default function BuildQuote({ ); }; + let blockExplorerTokenLink; + let blockExplorerLabel; + if (rpcPrefs.blockExplorerUrl) { + blockExplorerTokenLink = createCustomTokenTrackerLink( + selectedToToken.address, + rpcPrefs.blockExplorerUrl, + ); + blockExplorerLabel = new URL(rpcPrefs.blockExplorerUrl).hostname; + } else if (SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { + blockExplorerTokenLink = createCustomTokenTrackerLink( + selectedToToken.address, + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], + ); + blockExplorerLabel = new URL( + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], + ).hostname; + } else { + blockExplorerTokenLink = createTokenTrackerLinkForChain( + selectedToToken.address, + chainId, + ); + blockExplorerLabel = t('etherscan'); + } + const { destinationTokenAddedForSwap } = fetchParams || {}; const { address: toAddress } = toToken || {}; const onToSelect = useCallback( @@ -218,15 +272,17 @@ export default function BuildQuote({ ); useEffect(() => { - const notEth = - tokensWithBalancesFromToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address; + const notDefault = !isSwapsDefaultTokenAddress( + tokensWithBalancesFromToken?.address, + chainId, + ); const addressesAreTheSame = tokensWithBalancesFromToken?.address === previousTokensWithBalancesFromToken?.address; const balanceHasChanged = tokensWithBalancesFromToken?.balance !== previousTokensWithBalancesFromToken?.balance; - if (notEth && addressesAreTheSame && balanceHasChanged) { + if (notDefault && addressesAreTheSame && balanceHasChanged) { dispatch( setSwapsFromToken({ ...fromToken, @@ -240,12 +296,13 @@ export default function BuildQuote({ tokensWithBalancesFromToken, previousTokensWithBalancesFromToken, fromToken, + chainId, ]); // If the eth balance changes while on build quote, we update the selected from token useEffect(() => { if ( - fromToken?.address === ETH_SWAPS_TOKEN_OBJECT.address && + isSwapsDefaultTokenAddress(fromToken?.address, chainId) && fromToken?.balance !== hexToDecimal(ethBalance) ) { dispatch( @@ -260,7 +317,7 @@ export default function BuildQuote({ }), ); } - }, [dispatch, fromToken, ethBalance]); + }, [dispatch, fromToken, ethBalance, chainId]); useEffect(() => { if (prevFromTokenBalance !== fromTokenBalance) { @@ -277,7 +334,7 @@ export default function BuildQuote({
{t('swapSwapFrom')}
- {fromTokenSymbol !== 'ETH' && ( + {!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && (
@@ -375,7 +432,7 @@ export default function BuildQuote({ defaultToAll />
- {toTokenIsNotEth && + {toTokenIsNotDefault && (occurances < 2 ? (
- {t('verifyThisTokenOn', [ - - {t('etherscan')} - , - ])} + {blockExplorerTokenLink && + t('verifyThisTokenOn', [ + + {blockExplorerLabel} + , + ])}
} @@ -410,7 +468,10 @@ export default function BuildQuote({ } type="warning" withRightButton - infoTooltipText={t('swapVerifyTokenExplanation')} + infoTooltipText={ + blockExplorerTokenLink && + t('swapVerifyTokenExplanation', [blockExplorerLabel]) + } /> ) : (
@@ -420,23 +481,29 @@ export default function BuildQuote({ > {t('swapTokenVerificationSources', [occurances])} - {t('swapTokenVerificationMessage', [ - - {t('etherscan')} - , - ])} - + {blockExplorerTokenLink && ( + <> + {t('swapTokenVerificationMessage', [ + + {blockExplorerLabel} + , + ])} + + + )}
))}
@@ -465,7 +532,7 @@ export default function BuildQuote({ !selectedToToken?.address || Number(maxSlippage) === 0 || Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE || - (toTokenIsNotEth && occurances < 2 && !verificationClicked) + (toTokenIsNotDefault && occurances < 2 && !verificationClicked) } hideCancel showTermsOfService diff --git a/ui/app/pages/swaps/index.js b/ui/app/pages/swaps/index.js index 42f7e7e9f..04f4505b3 100644 --- a/ui/app/pages/swaps/index.js +++ b/ui/app/pages/swaps/index.js @@ -12,6 +12,7 @@ import { I18nContext } from '../../contexts/i18n'; import { getSelectedAccount, getCurrentChainId, + getIsSwapsChain, } from '../../selectors/selectors'; import { getQuotes, @@ -44,8 +45,7 @@ import { QUOTES_NOT_AVAILABLE_ERROR, SWAP_FAILED_ERROR, OFFLINE_FOR_MAINTENANCE, -} from '../../helpers/constants/swaps'; -import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; +} from '../../../../shared/constants/swaps'; import { resetBackgroundSwapsState, @@ -96,6 +96,8 @@ export default function Swap() { const fetchingQuotes = useSelector(getFetchingQuotes); let swapsErrorKey = useSelector(getSwapsErrorKey); const swapsEnabled = useSelector(getSwapsFeatureLiveness); + const chainId = useSelector(getCurrentChainId); + const isSwapsChain = useSelector(getIsSwapsChain); const { balance: ethBalance, @@ -116,6 +118,7 @@ export default function Swap() { selectedAccountAddress, destinationTokenInfo?.decimals, approveTxData, + chainId, ); const tradeConfirmed = tradeTxData?.status === TRANSACTION_STATUSES.CONFIRMED; const approveError = @@ -155,26 +158,26 @@ export default function Swap() { }, []); useEffect(() => { - fetchTokens() + fetchTokens(chainId) .then((tokens) => { dispatch(setSwapsTokens(tokens)); }) .catch((error) => console.error(error)); - fetchTopAssets().then((topAssets) => { + fetchTopAssets(chainId).then((topAssets) => { dispatch(setTopAssets(topAssets)); }); - fetchAggregatorMetadata().then((newAggregatorMetadata) => { + fetchAggregatorMetadata(chainId).then((newAggregatorMetadata) => { dispatch(setAggregatorMetadata(newAggregatorMetadata)); }); - dispatch(fetchAndSetSwapsGasPriceInfo()); + dispatch(fetchAndSetSwapsGasPriceInfo(chainId)); return () => { dispatch(prepareToLeaveSwaps()); }; - }, [dispatch]); + }, [dispatch, chainId]); const exitedSwapsEvent = useNewMetricEvent({ event: 'Exited Swaps', @@ -224,8 +227,7 @@ export default function Swap() { return () => window.removeEventListener('beforeunload', fn); }, [dispatch, isLoadingQuotesRoute]); - const chainId = useSelector(getCurrentChainId); - if (chainId !== MAINNET_CHAIN_ID) { + if (!isSwapsChain) { return ; } diff --git a/ui/app/pages/swaps/intro-popup/intro-popup.js b/ui/app/pages/swaps/intro-popup/intro-popup.js index 359cd0d34..658c84bb9 100644 --- a/ui/app/pages/swaps/intro-popup/intro-popup.js +++ b/ui/app/pages/swaps/intro-popup/intro-popup.js @@ -6,7 +6,7 @@ import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { I18nContext } from '../../../contexts/i18n'; import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; -import { getSwapsEthToken } from '../../../selectors'; +import { getSwapsDefaultToken } from '../../../selectors'; import Button from '../../../components/ui/button'; import Popover from '../../../components/ui/popover'; @@ -14,9 +14,14 @@ export default function IntroPopup({ onClose }) { const dispatch = useDispatch(useDispatch); const history = useHistory(); const t = useContext(I18nContext); + + const swapsDefaultToken = useSelector(getSwapsDefaultToken); const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', - properties: { source: 'Intro popup', active_currency: 'ETH' }, + properties: { + source: 'Intro popup', + active_currency: swapsDefaultToken.symbol, + }, category: 'swaps', }); const blogPostVisitedEvent = useNewMetricEvent({ @@ -31,7 +36,6 @@ export default function IntroPopup({ onClose }) { event: 'Product Overview Dismissed', category: 'swaps', }); - const swapsEthToken = useSelector(getSwapsEthToken); return (
@@ -51,7 +55,7 @@ export default function IntroPopup({ onClose }) { onClick={() => { onClose(); enteredSwapsEvent(); - dispatch(setSwapsFromToken(swapsEthToken)); + dispatch(setSwapsFromToken(swapsDefaultToken)); history.push(BUILD_QUOTE_ROUTE); }} > diff --git a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js index 2a717fc36..15efd88d6 100644 --- a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js +++ b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js @@ -8,6 +8,8 @@ import { getDefaultActiveButtonIndex, getRenderableGasButtonData, getUSDConversionRate, + getNativeCurrency, + getSwapsDefaultToken, } from '../../../selectors'; import { @@ -21,7 +23,6 @@ import { shouldShowCustomPriceTooLowWarning, swapCustomGasModalClosed, } from '../../../ducks/swaps/swaps'; - import { addHexes, getValueFromWeiHex, @@ -34,6 +35,9 @@ import SwapsGasCustomizationModalComponent from './swaps-gas-customization-modal const mapStateToProps = (state) => { const currentCurrency = getCurrentCurrency(state); const conversionRate = getConversionRate(state); + const nativeCurrencySymbol = getNativeCurrency(state); + const { symbol: swapsDefaultCurrencySymbol } = getSwapsDefaultToken(state); + const usedCurrencySymbol = nativeCurrencySymbol || swapsDefaultCurrencySymbol; const { modalState: { props: modalProps } = {} } = state.appState.modal || {}; const { @@ -63,6 +67,7 @@ const mapStateToProps = (state) => { true, conversionRate, currentCurrency, + usedCurrencySymbol, ); const gasButtonInfo = [averageEstimateData, fastEstimateData]; @@ -74,13 +79,15 @@ const mapStateToProps = (state) => { const balance = getCurrentEthBalance(state); - const newTotalEth = sumHexWEIsToRenderableEth([ - value, - customGasTotal, - customTotalSupplement, - ]); + const newTotalEth = sumHexWEIsToRenderableEth( + [value, customGasTotal, customTotalSupplement], + usedCurrencySymbol, + ); - const sendAmount = sumHexWEIsToRenderableEth([value, '0x0']); + const sendAmount = sumHexWEIsToRenderableEth( + [value, '0x0'], + usedCurrencySymbol, + ); const insufficientBalance = !isBalanceSufficient({ amount: value, @@ -112,14 +119,16 @@ const mapStateToProps = (state) => { currentCurrency, conversionRate, ), - originalTotalEth: sumHexWEIsToRenderableEth([ - value, - customGasTotal, - customTotalSupplement, - ]), + originalTotalEth: sumHexWEIsToRenderableEth( + [value, customGasTotal, customTotalSupplement], + usedCurrencySymbol, + ), newTotalFiat, newTotalEth, - transactionFee: sumHexWEIsToRenderableEth(['0x0', customGasTotal]), + transactionFee: sumHexWEIsToRenderableEth( + ['0x0', customGasTotal], + usedCurrencySymbol, + ), sendAmount, extraInfoRow, }, @@ -158,13 +167,15 @@ export default connect( mapDispatchToProps, )(SwapsGasCustomizationModalComponent); -function sumHexWEIsToRenderableEth(hexWEIs) { +function sumHexWEIsToRenderableEth(hexWEIs, currencySymbol = 'ETH') { const hexWEIsSum = hexWEIs.filter(Boolean).reduce(addHexes); return formatETHFee( getValueFromWeiHex({ value: hexWEIsSum, - toCurrency: 'ETH', + fromCurrency: currencySymbol, + toCurrency: currencySymbol, numberOfDecimals: 6, }), + currencySymbol, ); } diff --git a/ui/app/pages/swaps/swaps-util-test-constants.js b/ui/app/pages/swaps/swaps-util-test-constants.js index 6bf2207c6..1491b1184 100644 --- a/ui/app/pages/swaps/swaps-util-test-constants.js +++ b/ui/app/pages/swaps/swaps-util-test-constants.js @@ -1,4 +1,4 @@ -import { ETH_SWAPS_TOKEN_OBJECT } from '../../helpers/constants/swaps'; +import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps'; export const TRADES_BASE_PROD_URL = 'https://api.metaswap.codefi.network/trades?'; @@ -9,7 +9,7 @@ export const AGGREGATOR_METADATA_BASE_PROD_URL = export const TOP_ASSET_BASE_PROD_URL = 'https://api.metaswap.codefi.network/topAssets'; -export const TOKENS = [ +const BASE_TOKENS = [ { erc20: true, symbol: 'META', @@ -82,9 +82,12 @@ export const TOKENS = [ decimals: 8, address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', }, - ETH_SWAPS_TOKEN_OBJECT, ]; +export const TOKENS = [...BASE_TOKENS, ETH_SWAPS_TOKEN_OBJECT]; + +export const EXPECTED_TOKENS_RESULT = [ETH_SWAPS_TOKEN_OBJECT, ...BASE_TOKENS]; + export const MOCK_TRADE_RESPONSE_1 = [ { trade: { diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index aed3ce2f0..f4da20606 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -3,9 +3,15 @@ import BigNumber from 'bignumber.js'; import abi from 'human-standard-token-abi'; import { isValidAddress } from 'ethereumjs-util'; import { - ETH_SWAPS_TOKEN_OBJECT, - METASWAP_API_HOST, -} from '../../helpers/constants/swaps'; + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + METASWAP_CHAINID_API_HOST_MAP, +} from '../../../../shared/constants/swaps'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../../shared/modules/swaps.utils'; + +import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; import { calcTokenValue, calcTokenAmount, @@ -30,22 +36,22 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH = const CACHE_REFRESH_ONE_HOUR = 3600000; -const getBaseApi = function (type) { +const getBaseApi = function (type, chainId = MAINNET_CHAIN_ID) { switch (type) { case 'trade': - return `${METASWAP_API_HOST}/trades?`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/trades?`; case 'tokens': - return `${METASWAP_API_HOST}/tokens`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/tokens`; case 'topAssets': - return `${METASWAP_API_HOST}/topAssets`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/topAssets`; case 'featureFlag': - return `${METASWAP_API_HOST}/featureFlag`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/featureFlag`; case 'aggregatorMetadata': - return `${METASWAP_API_HOST}/aggregatorMetadata`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/aggregatorMetadata`; case 'gasPrices': - return `${METASWAP_API_HOST}/gasPrices`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/gasPrices`; case 'refreshTime': - return `${METASWAP_API_HOST}/quoteRefreshRate`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/quoteRefreshRate`; default: throw new Error('getBaseApi requires an api call type'); } @@ -205,15 +211,18 @@ function validateData(validators, object, urlUsed) { }); } -export async function fetchTradesInfo({ - slippage, - sourceToken, - sourceDecimals, - destinationToken, - value, - fromAddress, - exchangeList, -}) { +export async function fetchTradesInfo( + { + slippage, + sourceToken, + sourceDecimals, + destinationToken, + value, + fromAddress, + exchangeList, + }, + { chainId }, +) { const urlParams = { destinationToken, sourceToken, @@ -228,7 +237,7 @@ export async function fetchTradesInfo({ } const queryString = new URLSearchParams(urlParams).toString(); - const tradeURL = `${getBaseApi('trade')}${queryString}`; + const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`; const tradesResponse = await fetchWithCache( tradeURL, { method: 'GET' }, @@ -272,25 +281,30 @@ export async function fetchTradesInfo({ return newQuotes; } -export async function fetchTokens() { - const tokenUrl = getBaseApi('tokens'); +export async function fetchTokens(chainId) { + const tokenUrl = getBaseApi('tokens', chainId); const tokens = await fetchWithCache( tokenUrl, { method: 'GET' }, { cacheRefreshTime: CACHE_REFRESH_ONE_HOUR }, ); - const filteredTokens = tokens.filter((token) => { - return ( - validateData(TOKEN_VALIDATORS, token, tokenUrl) && - token.address !== ETH_SWAPS_TOKEN_OBJECT.address - ); - }); - filteredTokens.push(ETH_SWAPS_TOKEN_OBJECT); + const filteredTokens = [ + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId], + ...tokens.filter((token) => { + return ( + validateData(TOKEN_VALIDATORS, token, tokenUrl) && + !( + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) + ) + ); + }), + ]; return filteredTokens; } -export async function fetchAggregatorMetadata() { - const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata'); +export async function fetchAggregatorMetadata(chainId) { + const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata', chainId); const aggregators = await fetchWithCache( aggregatorMetadataUrl, { method: 'GET' }, @@ -311,8 +325,8 @@ export async function fetchAggregatorMetadata() { return filteredAggregators; } -export async function fetchTopAssets() { - const topAssetsUrl = getBaseApi('topAssets'); +export async function fetchTopAssets(chainId) { + const topAssetsUrl = getBaseApi('topAssets', chainId); const response = await fetchWithCache( topAssetsUrl, { method: 'GET' }, @@ -327,18 +341,18 @@ export async function fetchTopAssets() { return topAssetsMap; } -export async function fetchSwapsFeatureLiveness() { +export async function fetchSwapsFeatureLiveness(chainId) { const status = await fetchWithCache( - getBaseApi('featureFlag'), + getBaseApi('featureFlag', chainId), { method: 'GET' }, { cacheRefreshTime: 600000 }, ); return status?.active; } -export async function fetchSwapsQuoteRefreshTime() { +export async function fetchSwapsQuoteRefreshTime(chainId) { const response = await fetchWithCache( - getBaseApi('refreshTime'), + getBaseApi('refreshTime', chainId), { method: 'GET' }, { cacheRefreshTime: 600000 }, ); @@ -373,12 +387,12 @@ export async function fetchTokenBalance(address, userAddress) { return usersToken; } -export async function fetchSwapsGasPrices() { - const gasPricesUrl = getBaseApi('gasPrices'); +export async function fetchSwapsGasPrices(chainId) { + const gasPricesUrl = getBaseApi('gasPrices', chainId); const response = await fetchWithCache( gasPricesUrl, { method: 'GET' }, - { cacheRefreshTime: 15000 }, + { cacheRefreshTime: 30000 }, ); const responseIsValid = validateData( SWAP_GAS_PRICE_VALIDATOR, @@ -403,7 +417,7 @@ export async function fetchSwapsGasPrices() { }; } -export function getRenderableNetworkFeesForQuote( +export function getRenderableNetworkFeesForQuote({ tradeGas, approveGas, gasPrice, @@ -412,14 +426,19 @@ export function getRenderableNetworkFeesForQuote( tradeValue, sourceSymbol, sourceAmount, -) { + chainId, + nativeCurrencySymbol, +}) { const totalGasLimitForCalculation = new BigNumber(tradeGas || '0x0', 16) .plus(approveGas || '0x0', 16) .toString(16); const gasTotalInWeiHex = calcGasTotal(totalGasLimitForCalculation, gasPrice); const nonGasFee = new BigNumber(tradeValue, 16) - .minus(sourceSymbol === 'ETH' ? sourceAmount : 0, 10) + .minus( + isSwapsDefaultTokenSymbol(sourceSymbol, chainId) ? sourceAmount : 0, + 10, + ) .toString(16); const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16) @@ -438,11 +457,15 @@ export function getRenderableNetworkFeesForQuote( numberOfDecimals: 2, }); const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency); + + const chainCurrencySymbolToUse = + nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol; + return { rawNetworkFees, rawEthFee: ethFee, feeInFiat: formattedNetworkFee, - feeInEth: `${ethFee} ETH`, + feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`, nonGasFee, }; } @@ -454,6 +477,7 @@ export function quotesToRenderableData( currentCurrency, approveGas, tokenConversionRates, + chainId, ) { return Object.values(quotes).map((quote) => { const { @@ -483,16 +507,17 @@ export function quotesToRenderableData( rawNetworkFees, rawEthFee, feeInEth, - } = getRenderableNetworkFeesForQuote( - gasEstimateWithRefund || decimalToHex(averageGas || 800000), + } = getRenderableNetworkFeesForQuote({ + tradeGas: gasEstimateWithRefund || decimalToHex(averageGas || 800000), approveGas, gasPrice, currentCurrency, conversionRate, - trade.value, - sourceTokenInfo.symbol, + tradeValue: trade.value, + sourceSymbol: sourceTokenInfo.symbol, sourceAmount, - ); + chainId, + }); const slippageMultiplier = new BigNumber(100 - slippage).div(100); const minimumAmountReceived = new BigNumber(destinationValue) @@ -501,18 +526,20 @@ export function quotesToRenderableData( const tokenConversionRate = tokenConversionRates[destinationTokenInfo.address]; - const ethValueOfTrade = - destinationTokenInfo.symbol === 'ETH' - ? calcTokenAmount( - destinationAmount, - destinationTokenInfo.decimals, - ).minus(rawEthFee, 10) - : new BigNumber(tokenConversionRate || 0, 10) - .times( - calcTokenAmount(destinationAmount, destinationTokenInfo.decimals), - 10, - ) - .minus(rawEthFee, 10); + const ethValueOfTrade = isSwapsDefaultTokenSymbol( + destinationTokenInfo.symbol, + chainId, + ) + ? calcTokenAmount(destinationAmount, destinationTokenInfo.decimals).minus( + rawEthFee, + 10, + ) + : new BigNumber(tokenConversionRate || 0, 10) + .times( + calcTokenAmount(destinationAmount, destinationTokenInfo.decimals), + 10, + ) + .minus(rawEthFee, 10); let liquiditySourceKey; let renderedSlippage = slippage; @@ -561,9 +588,10 @@ export function getSwapsTokensReceivedFromTxMeta( accountAddress, tokenDecimals, approvalTxMeta, + chainId, ) { const txReceipt = txMeta?.txReceipt; - if (tokenSymbol === 'ETH') { + if (isSwapsDefaultTokenSymbol(tokenSymbol, chainId)) { if ( !txReceipt || !txMeta || diff --git a/ui/app/pages/swaps/swaps.util.test.js b/ui/app/pages/swaps/swaps.util.test.js index f3ee2a497..df305e675 100644 --- a/ui/app/pages/swaps/swaps.util.test.js +++ b/ui/app/pages/swaps/swaps.util.test.js @@ -1,11 +1,13 @@ import { strict as assert } from 'assert'; import proxyquire from 'proxyquire'; +import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; import { TRADES_BASE_PROD_URL, TOKENS_BASE_PROD_URL, AGGREGATOR_METADATA_BASE_PROD_URL, TOP_ASSET_BASE_PROD_URL, TOKENS, + EXPECTED_TOKENS_RESULT, MOCK_TRADE_RESPONSE_2, AGGREGATOR_METADATA, TOP_ASSETS, @@ -88,42 +90,45 @@ describe('Swaps Util', function () { }, }; it('should fetch trade info on prod', async function () { - const result = await fetchTradesInfo({ - TOKENS, - slippage: '3', - sourceToken: TOKENS[0].address, - destinationToken: TOKENS[1].address, - value: '2000000000000000000', - fromAddress: '0xmockAddress', - sourceSymbol: TOKENS[0].symbol, - sourceDecimals: TOKENS[0].decimals, - sourceTokenInfo: { ...TOKENS[0] }, - destinationTokenInfo: { ...TOKENS[1] }, - }); + const result = await fetchTradesInfo( + { + TOKENS, + slippage: '3', + sourceToken: TOKENS[0].address, + destinationToken: TOKENS[1].address, + value: '2000000000000000000', + fromAddress: '0xmockAddress', + sourceSymbol: TOKENS[0].symbol, + sourceDecimals: TOKENS[0].decimals, + sourceTokenInfo: { ...TOKENS[0] }, + destinationTokenInfo: { ...TOKENS[1] }, + }, + { chainId: MAINNET_CHAIN_ID }, + ); assert.deepStrictEqual(result, expectedResult2); }); }); describe('fetchTokens', function () { it('should fetch tokens', async function () { - const result = await fetchTokens(true); - assert.deepStrictEqual(result, TOKENS); + const result = await fetchTokens(MAINNET_CHAIN_ID); + assert.deepStrictEqual(result, EXPECTED_TOKENS_RESULT); }); it('should fetch tokens on prod', async function () { - const result = await fetchTokens(false); - assert.deepStrictEqual(result, TOKENS); + const result = await fetchTokens(MAINNET_CHAIN_ID); + assert.deepStrictEqual(result, EXPECTED_TOKENS_RESULT); }); }); describe('fetchAggregatorMetadata', function () { it('should fetch aggregator metadata', async function () { - const result = await fetchAggregatorMetadata(true); + const result = await fetchAggregatorMetadata(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, AGGREGATOR_METADATA); }); it('should fetch aggregator metadata on prod', async function () { - const result = await fetchAggregatorMetadata(false); + const result = await fetchAggregatorMetadata(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, AGGREGATOR_METADATA); }); }); @@ -147,12 +152,12 @@ describe('Swaps Util', function () { }, }; it('should fetch top assets', async function () { - const result = await fetchTopAssets(true); + const result = await fetchTopAssets(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, expectedResult); }); it('should fetch top assets on prod', async function () { - const result = await fetchTopAssets(false); + const result = await fetchTopAssets(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, expectedResult); }); }); diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 1efa81751..2d7648e90 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -36,7 +36,9 @@ import { getSelectedAccount, getCurrentCurrency, getTokenExchangeRates, - getSwapsEthToken, + getSwapsDefaultToken, + getCurrentChainId, + getNativeCurrency, } from '../../../selectors'; import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util'; import { getTokens } from '../../../ducks/metamask/metamask'; @@ -73,7 +75,7 @@ import { getRenderableNetworkFeesForQuote, } from '../swaps.util'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps'; +import { QUOTES_EXPIRED_ERROR } from '../../../../../shared/constants/swaps'; import CountdownTimer from '../countdown-timer'; import SwapsFooter from '../swaps-footer'; import ViewQuotePriceDifference from './view-quote-price-difference'; @@ -125,7 +127,9 @@ export default function ViewQuote() { const usedQuote = selectedQuote || topQuote; const tradeValue = usedQuote?.trade?.value ?? '0x0'; const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); - const swapsEthToken = useSelector(getSwapsEthToken); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); + const chainId = useSelector(getCurrentChainId); + const nativeCurrencySymbol = useSelector(getNativeCurrency); const { isBestQuote } = usedQuote; @@ -151,8 +155,8 @@ export default function ViewQuote() { const { tokensWithBalances } = useTokenTracker(swapsTokens, true); const balanceToken = - fetchParamsSourceToken === swapsEthToken.address - ? swapsEthToken + fetchParamsSourceToken === defaultSwapsToken.address + ? defaultSwapsToken : tokensWithBalances.find( ({ address }) => address === fetchParamsSourceToken, ); @@ -183,6 +187,7 @@ export default function ViewQuote() { currentCurrency, approveGas, memoizedTokenConversionRates, + chainId, ); }, [ quotes, @@ -191,6 +196,7 @@ export default function ViewQuote() { currentCurrency, approveGas, memoizedTokenConversionRates, + chainId, ]); const renderableDataForUsedQuote = renderablePopoverData.find( @@ -209,31 +215,35 @@ export default function ViewQuote() { sourceTokenIconUrl, } = renderableDataForUsedQuote; - const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote( - usedGasLimit, + const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote({ + tradeGas: usedGasLimit, approveGas, gasPrice, currentCurrency, conversionRate, tradeValue, - sourceTokenSymbol, - usedQuote.sourceAmount, - ); + sourceSymbol: sourceTokenSymbol, + sourceAmount: usedQuote.sourceAmount, + chainId, + nativeCurrencySymbol, + }); const { feeInFiat: maxFeeInFiat, feeInEth: maxFeeInEth, nonGasFee, - } = getRenderableNetworkFeesForQuote( - maxGasLimit, + } = getRenderableNetworkFeesForQuote({ + tradeGas: maxGasLimit, approveGas, gasPrice, currentCurrency, conversionRate, tradeValue, - sourceTokenSymbol, - usedQuote.sourceAmount, - ); + sourceSymbol: sourceTokenSymbol, + sourceAmount: usedQuote.sourceAmount, + chainId, + nativeCurrencySymbol, + }); const tokenCost = new BigNumber(usedQuote.sourceAmount); const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus( @@ -460,7 +470,7 @@ export default function ViewQuote() { extraInfoRow: extraInfoRowLabel ? { label: extraInfoRowLabel, - value: t('amountInEth', [extraNetworkFeeTotalInEth]), + value: `${extraNetworkFeeTotalInEth} ${nativeCurrencySymbol}`, } : null, initialGasPrice: gasPrice, @@ -481,9 +491,9 @@ export default function ViewQuote() { {tokenBalanceNeeded || ethBalanceNeeded} , - tokenBalanceNeeded && !(sourceTokenSymbol === 'ETH') + tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) ? sourceTokenSymbol - : 'ETH', + : defaultSwapsToken.symbol, ]); // Price difference warning @@ -508,9 +518,11 @@ export default function ViewQuote() { let viewQuotePriceDifferenceComponent = null; const priceSlippageFromSource = useEthFiatAmount( usedQuote?.priceSlippage?.sourceAmountInETH || 0, + { showFiat: true }, ); const priceSlippageFromDestination = useEthFiatAmount( usedQuote?.priceSlippage?.destinationAmountInETH || 0, + { showFiat: true }, ); // We cannot present fiat value if there is a calculation error or no slippage @@ -643,7 +655,7 @@ export default function ViewQuote() { setSelectQuotePopoverShown(true); }} tokenConversionRate={ - destinationTokenSymbol === 'ETH' + destinationTokenSymbol === defaultSwapsToken.symbol ? 1 : memoizedTokenConversionRates[destinationToken.address] } @@ -655,7 +667,7 @@ export default function ViewQuote() { setSubmitClicked(true); if (!balanceError) { dispatch(signAndSendTransactions(history, metaMetricsEvent)); - } else if (destinationToken.symbol === 'ETH') { + } else if (destinationToken.symbol === defaultSwapsToken.symbol) { history.push(DEFAULT_ROUTE); } else { history.push(`${ASSET_ROUTE}/${destinationToken.address}`); diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index 2ac6b4767..c84c48c71 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -134,13 +134,18 @@ export function basicPriceEstimateToETHTotal( }); } -export function getRenderableEthFee(estimate, gasLimit, numberOfDecimals = 9) { +export function getRenderableEthFee( + estimate, + gasLimit, + numberOfDecimals = 9, + nativeCurrency = 'ETH', +) { const value = conversionUtil(estimate, { fromNumericBase: 'dec', toNumericBase: 'hex', }); const fee = basicPriceEstimateToETHTotal(value, gasLimit, numberOfDecimals); - return formatETHFee(fee); + return formatETHFee(fee, nativeCurrency); } export function getRenderableConvertedCurrencyFee( @@ -186,12 +191,18 @@ export function getRenderableGasButtonData( showFiat, conversionRate, currentCurrency, + nativeCurrency, ) { const { safeLow, average, fast } = estimates; const slowEstimateData = { gasEstimateType: GAS_ESTIMATE_TYPES.SLOW, - feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit), + feeInPrimaryCurrency: getRenderableEthFee( + safeLow, + gasLimit, + 9, + nativeCurrency, + ), feeInSecondaryCurrency: showFiat ? getRenderableConvertedCurrencyFee( safeLow, @@ -204,7 +215,12 @@ export function getRenderableGasButtonData( }; const averageEstimateData = { gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE, - feeInPrimaryCurrency: getRenderableEthFee(average, gasLimit), + feeInPrimaryCurrency: getRenderableEthFee( + average, + gasLimit, + 9, + nativeCurrency, + ), feeInSecondaryCurrency: showFiat ? getRenderableConvertedCurrencyFee( average, @@ -217,7 +233,12 @@ export function getRenderableGasButtonData( }; const fastEstimateData = { gasEstimateType: GAS_ESTIMATE_TYPES.FAST, - feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit), + feeInPrimaryCurrency: getRenderableEthFee( + fast, + gasLimit, + 9, + nativeCurrency, + ), feeInSecondaryCurrency: showFiat ? getRenderableConvertedCurrencyFee( fast, @@ -295,7 +316,6 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, - true, ), priceInHexWei: getGasPriceInHexWei(safeLow, true), }, @@ -313,7 +333,6 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { average, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, - true, ), priceInHexWei: getGasPriceInHexWei(average, true), }, @@ -331,7 +350,6 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { fast, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, - true, ), priceInHexWei: getGasPriceInHexWei(fast, true), }, diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 6f401220a..ab22f2387 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -5,7 +5,14 @@ import { MAINNET_CHAIN_ID, TEST_CHAINS, NETWORK_TYPE_RPC, + NATIVE_CURRENCY_TOKEN_IMAGE_MAP, } from '../../../shared/constants/network'; + +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + ALLOWED_SWAPS_CHAIN_IDS, +} from '../../../shared/constants/swaps'; + import { shortenAddress, checksumAddress, @@ -15,9 +22,11 @@ import { getValueFromWeiHex, hexToDecimal, } from '../helpers/utils/conversions.util'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps'; + import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/templates'; +import { getNativeCurrency } from './send'; + /** * One of the only remaining valid uses of selecting the network subkey of the * metamask state tree is to determine if the network is currently 'loading'. @@ -441,22 +450,26 @@ export function getWeb3ShimUsageStateForOrigin(state, origin) { * minimal token units (according to its decimals). * `string` is the token balance in a readable format, ready for rendering. * - * Swaps treats ETH as a token, and we use the ETH_SWAPS_TOKEN_OBJECT constant - * to set the standard properties for the token. The getSwapsEthToken selector - * extends that object with `balance` and `balance` values of the same type as - * in regular ERC-20 token objects, per the above description. + * Swaps treats the selected chain's currency as a token, and we use the token constants + * in the SWAPS_CHAINID_DEFAULT_TOKEN_MAP to set the standard properties for + * the token. The getSwapsDefaultToken selector extends that object with + * `balance` and `string` values of the same type as in regular ERC-20 token + * objects, per the above description. * * @param {object} state - the redux state object * @returns {SwapsEthToken} The token object representation of the currently * selected account's ETH balance, as expected by the Swaps API. */ -export function getSwapsEthToken(state) { +export function getSwapsDefaultToken(state) { const selectedAccount = getSelectedAccount(state); const { balance } = selectedAccount; + const chainId = getCurrentChainId(state); + + const defaultTokenObject = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]; return { - ...ETH_SWAPS_TOKEN_OBJECT, + ...defaultTokenObject, balance: hexToDecimal(balance), string: getValueFromWeiHex({ value: balance, @@ -465,3 +478,13 @@ export function getSwapsEthToken(state) { }), }; } + +export function getIsSwapsChain(state) { + const chainId = getCurrentChainId(state); + return ALLOWED_SWAPS_CHAIN_IDS[chainId]; +} + +export function getNativeCurrencyImage(state) { + const nativeCurrency = getNativeCurrency(state).toUpperCase(); + return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency]; +} diff --git a/yarn.lock b/yarn.lock index 28eb70bbc..e8a401a5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,10 +2271,10 @@ human-standard-token-abi "^1.0.2" safe-event-emitter "^1.0.1" -"@metamask/etherscan-link@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-1.5.0.tgz#c6940ea934b3a7dcf04e459d9ea3c630b69f6b5f" - integrity sha512-vPCkZJwZ5p933n20Zh+cC3umJv05un2CRZ8y+14KgMq3I4eOwllqmqxoYf9tn3BLGM8QXm/Nie+aBjmoe/T9ag== +"@metamask/etherscan-link@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.0.0.tgz#89035736515a39532ba1142d87b9a8c2b4f920f1" + integrity sha512-/YS32hS2UTTxs0KyUmAgaDj1w4dzAvOrT+p4TJtpICeH3E/k51r2FO0Or7WJJI/mpzTqNKgcH5yyS2oCtupGiA== "@metamask/forwarder@^1.1.0": version "1.1.0" @@ -17729,10 +17729,10 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== -netmask@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" - integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU= +netmask@^1.0.6, netmask@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.1.tgz#5a5cbdcbb7b6de650870e15e83d3e9553a414cf4" + integrity sha512-gB8eG6ubxz67c7O2gaGiyWdRUIbH61q7anjgueDqCC9kvIs/b4CTtCMaQKeJbv1/Y7FT19I4zKwYmjnjInRQsg== next-tick@1, next-tick@^1.0.0: version "1.0.0" @@ -26205,14 +26205,14 @@ xtend@~3.0.0: integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo= y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + version "3.2.2" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" + integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== y18n@^5.0.5: version "5.0.5"