diff --git a/.github/workflows/wait-for-circleci-workflow-status.yml b/.github/workflows/wait-for-circleci-workflow-status.yml index 18e5ef7825d5..725a4c3b975d 100644 --- a/.github/workflows/wait-for-circleci-workflow-status.yml +++ b/.github/workflows/wait-for-circleci-workflow-status.yml @@ -13,8 +13,13 @@ jobs: OWNER: ${{ github.repository_owner }} REPOSITORY: ${{ github.event.repository.name }} BRANCH: ${{ github.head_ref || github.ref_name }} + # For a `push` event, the HEAD commit hash is `github.sha`. + # For a `pull_request` event, `github.sha` is instead the base branch commit hash. The + # HEAD commit hash is `pull_request.head.sha`. + HEAD_COMMIT_HASH: ${{ github.event.pull_request.head.sha || github.sha }} run: | - pipeline_id=$(curl --silent "https://circleci.com/api/v2/project/gh/$OWNER/$REPOSITORY/pipeline?branch=$BRANCH" | jq -r ".items[0].id") + pipeline_id=$(curl --silent "https://circleci.com/api/v2/project/gh/$OWNER/$REPOSITORY/pipeline?branch=$BRANCH" | jq -r ".items | map(select(.vcs.revision == \"${HEAD_COMMIT_HASH}\" )) | first | .id") + echo "Waiting for pipeline '${pipeline_id}' for commit hash '${HEAD_COMMIT_HASH}'" workflow_status=$(curl --silent "https://circleci.com/api/v2/pipeline/$pipeline_id/workflow" | jq -r ".items[0].status") if [ "$workflow_status" == "running" ]; then diff --git a/.yarn/patches/@metamask-assets-controllers-patch-d6ed5f8213.patch b/.yarn/patches/@metamask-assets-controllers-patch-d6ed5f8213.patch new file mode 100644 index 000000000000..02e6d3f694e5 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-patch-d6ed5f8213.patch @@ -0,0 +1,61 @@ +diff --git a/dist/multicall.cjs b/dist/multicall.cjs +index bf9aa5e86573fc1651f421cc0b64f5af121c3ab2..43a0531ed86cd3ee1774dcda3f990dd40f7f52de 100644 +--- a/dist/multicall.cjs ++++ b/dist/multicall.cjs +@@ -342,9 +342,22 @@ const multicallOrFallback = async (calls, chainId, provider, maxCallsPerMultical + return []; + } + const multicallAddress = MULTICALL_CONTRACT_BY_CHAINID[chainId]; +- return await (multicallAddress +- ? multicall(calls, multicallAddress, provider, maxCallsPerMulticall) +- : fallback(calls, maxCallsParallel)); ++ if (multicallAddress) { ++ try { ++ return await multicall(calls, multicallAddress, provider, maxCallsPerMulticall); ++ } ++ catch (error) { ++ // Fallback only on revert ++ // https://docs.ethers.org/v5/troubleshooting/errors/#help-CALL_EXCEPTION ++ if (!error || ++ typeof error !== 'object' || ++ !('code' in error) || ++ error.code !== 'CALL_EXCEPTION') { ++ throw error; ++ } ++ } ++ } ++ return await fallback(calls, maxCallsParallel); + }; + exports.multicallOrFallback = multicallOrFallback; + //# sourceMappingURL=multicall.cjs.map +\ No newline at end of file +diff --git a/dist/multicall.mjs b/dist/multicall.mjs +index 8fbe0112303d5df1d868e0357a9d31e43a3b6cf9..860dfdbddd813659cb2be5f7faed5d4016db5966 100644 +--- a/dist/multicall.mjs ++++ b/dist/multicall.mjs +@@ -339,8 +339,21 @@ export const multicallOrFallback = async (calls, chainId, provider, maxCallsPerM + return []; + } + const multicallAddress = MULTICALL_CONTRACT_BY_CHAINID[chainId]; +- return await (multicallAddress +- ? multicall(calls, multicallAddress, provider, maxCallsPerMulticall) +- : fallback(calls, maxCallsParallel)); ++ if (multicallAddress) { ++ try { ++ return await multicall(calls, multicallAddress, provider, maxCallsPerMulticall); ++ } ++ catch (error) { ++ // Fallback only on revert ++ // https://docs.ethers.org/v5/troubleshooting/errors/#help-CALL_EXCEPTION ++ if (!error || ++ typeof error !== 'object' || ++ !('code' in error) || ++ error.code !== 'CALL_EXCEPTION') { ++ throw error; ++ } ++ } ++ } ++ return await fallback(calls, maxCallsParallel); + }; + //# sourceMappingURL=multicall.mjs.map +\ No newline at end of file diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index f118bc17df41..ccb81d489af7 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -369,9 +369,6 @@ "loading": { "message": "በመጫን ላይ…" }, - "loadingTokens": { - "message": "ተለዋጭ ስሞችን በመጫን ላይ..." - }, "lock": { "message": "ዘግተህ ውጣ" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index d9717df6b190..00685f39df87 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -385,9 +385,6 @@ "loading": { "message": "جارٍ التحميل..." }, - "loadingTokens": { - "message": "جارِ تحميل العملات الرمزية ..." - }, "localhost": { "message": "المضيف المحلي 8545" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 749b1561dafe..2e51cf4b24b4 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Зарежда се..." }, - "loadingTokens": { - "message": "Зареждане на жетони..." - }, "localhost": { "message": "Локален хост 8545" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 15acaa2e6765..6f3bb290215d 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -375,9 +375,6 @@ "loading": { "message": "লোড হচ্ছে..." }, - "loadingTokens": { - "message": "টোকেনগুলি লোড করছে..." - }, "localhost": { "message": "লোকালহোস্ট 8545" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index fc9e2afb41e6..c54e236d8a21 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -372,9 +372,6 @@ "loading": { "message": "S'està carregant..." }, - "loadingTokens": { - "message": "Carregant els tokens..." - }, "localhost": { "message": "Host local 8545" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 4113f8c5cc42..b6161b00a979 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -175,9 +175,6 @@ "loading": { "message": "Načítám..." }, - "loadingTokens": { - "message": "Načítám tokeny..." - }, "lock": { "message": "Odhlásit" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 37e4663523cf..e9c884f28dbb 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -375,9 +375,6 @@ "loading": { "message": "Indlæser..." }, - "loadingTokens": { - "message": "Indlæser tokens..." - }, "lock": { "message": "Log ud" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 946f556f1abd..04c45bd81348 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Bitte schließen Sie die Transaktion im Snap ab." }, - "loadingTokens": { - "message": "Tokens werden geladen ..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index a02ec275c224..24da4df88460 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Ολοκληρώστε τη συναλλαγή στο Snap." }, - "loadingTokens": { - "message": "Φόρτωση των tokens..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7ddf609b48f0..2d0f8baa4158 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2948,9 +2948,6 @@ "loadingTokenList": { "message": "Loading token list" }, - "loadingTokens": { - "message": "Loading tokens..." - }, "localhost": { "message": "Localhost 8545" }, @@ -2971,10 +2968,10 @@ "message": "Low" }, "lowEstimatedReturnTooltipMessage": { - "message": "Either your rate or your fees are less favorable than usual. It looks like you'll get back less than $1% of the amount you’re bridging." + "message": "You’ll pay more than $1% of your starting amount in fees. Check your receiving amount and network fees." }, "lowEstimatedReturnTooltipTitle": { - "message": "Low estimated return" + "message": "High cost" }, "lowGasSettingToolTipMessage": { "message": "Use $1 to wait for a cheaper price. Time estimates are much less accurate as prices are somewhat unpredictable.", diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 2548f8bf0cfb..d242dc7e88f8 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -2641,9 +2641,6 @@ "loadingScreenSnapMessage": { "message": "Please complete the transaction on the Snap." }, - "loadingTokens": { - "message": "Loading tokens..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 8acce0ad02d9..0f24f5bc5b1a 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Por favor, complete la transacción en el Snap." }, - "loadingTokens": { - "message": "Cargando tokens..." - }, "localhost": { "message": "Host local 8545" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 8006ca4405be..5d940a269091 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1086,9 +1086,6 @@ "loading": { "message": "Cargando..." }, - "loadingTokens": { - "message": "Cargando tokens..." - }, "localhost": { "message": "Host local 8545" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 38125572b8ec..c5f55c6d3327 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Laadimine..." }, - "loadingTokens": { - "message": "Lubade laadimine..." - }, "lock": { "message": "Logi välja" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index c1a4deb11ce4..8399ecb91aec 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "در حال بارکردن…" }, - "loadingTokens": { - "message": "در حال بارگیری رمزیاب ها..." - }, "localhost": { "message": "Localhost 8545 " }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 89e274dd4466..5002c4eed2b3 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Ladataan..." }, - "loadingTokens": { - "message": "Käyttötunnuksia ladataan..." - }, "localhost": { "message": "Paikallisjoukko 8545" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 498c1878fd10..abee8f9c0a5e 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -335,9 +335,6 @@ "loading": { "message": "Naglo-load..." }, - "loadingTokens": { - "message": "Naglo-load ng Mga Token..." - }, "lock": { "message": "Mag-log out" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 68e6626c4688..ce615bddd591 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Veuillez conclure la transaction sur le snap." }, - "loadingTokens": { - "message": "Chargement des jetons..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 413bf21d586b..3bd6b6b67408 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "טוען..." }, - "loadingTokens": { - "message": "טוען טוקנים..." - }, "lock": { "message": "התנתקות" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 1f96413e81ff..05488711d94f 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "कृपया Snap पर ट्रांजेक्शन पूरा करें।" }, - "loadingTokens": { - "message": "टोकन लोड हो रहे हैं..." - }, "localhost": { "message": "लोकलहोस्ट 8545" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index a4e2e37bde22..f6bb938c4886 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -152,9 +152,6 @@ "loading": { "message": "लोड हो रहा है ....." }, - "loadingTokens": { - "message": "टोकन लोड हो रहा है ....." - }, "localhost": { "message": "स्थानीयहोस्ट 8545" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 7f9334f49f5c..d1ca9bac8057 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Učitavanje..." }, - "loadingTokens": { - "message": "Učitavanje tokena..." - }, "lock": { "message": "Odjava" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 7309b04dbd05..8d0e7e703559 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -260,9 +260,6 @@ "loading": { "message": "Telechaje..." }, - "loadingTokens": { - "message": "Telechaje Tokens..." - }, "lock": { "message": "Dekonekte" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 7b2b429ae5ed..ee8699a64545 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Betöltés..." }, - "loadingTokens": { - "message": "Tokenek betöltése..." - }, "lock": { "message": "Kilépés" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 68bbf6477be5..c918a6cc0fb0 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Selesaikan transaksi di Snap." }, - "loadingTokens": { - "message": "Memuat token..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index c00040b84086..da1da37952f6 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -965,9 +965,6 @@ "loading": { "message": "Caricamento..." }, - "loadingTokens": { - "message": "Caricamento Tokens..." - }, "lock": { "message": "Disconnetti" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 05f359973442..ead0da87b7b8 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Snapでトランザクションを完了させてください。" }, - "loadingTokens": { - "message": "トークンをロードしています..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 120651f0b759..805de3c78fc0 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "ಲೋಡ್ ಆಗುತ್ತಿದೆ..." }, - "loadingTokens": { - "message": "ಟೋಕನ್‌ಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ..." - }, "localhost": { "message": "ಲೋಕಲ್‌ಹೋಸ್ಟ್ 8545" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f261c0b24c06..f22491040b4f 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Snap에서 트랜잭션을 완료하세요." }, - "loadingTokens": { - "message": "토큰 불러오는 중..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index fe825ae6b798..cd034a303c4a 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Įkeliama..." }, - "loadingTokens": { - "message": "Įkeliami žetonai..." - }, "localhost": { "message": "Vietinis serveris 8545" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 697af7849327..be5d43f4afd9 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Notiek ielāde..." }, - "loadingTokens": { - "message": "Ielādē marķierus..." - }, "localhost": { "message": "Resursdators 8545" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index dc42e639ff2a..20161aaf34da 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -374,9 +374,6 @@ "loading": { "message": "Memuatkan..." }, - "loadingTokens": { - "message": "Memuatkan Token..." - }, "lock": { "message": "Log keluar" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index cbebb9a14563..7ac22889df2d 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -149,9 +149,6 @@ "loading": { "message": "Bezig met laden..." }, - "loadingTokens": { - "message": "Tokens laden ..." - }, "lock": { "message": "Uitloggen" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 45a101fc83a5..67948544fdbf 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -369,9 +369,6 @@ "loading": { "message": "Laster inn ..." }, - "loadingTokens": { - "message": "Laster tokener ..." - }, "localhost": { "message": "Lokalvert 8545" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 3d40dbfefd77..af995dbf5f5f 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -700,9 +700,6 @@ "loading": { "message": "Nilo-load..." }, - "loadingTokens": { - "message": "Nilo-load ang Mga Token..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index d22673fa9f1f..5baeee9b8a00 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Ładowanie..." }, - "loadingTokens": { - "message": "Ładowanie tokenów..." - }, "localhost": { "message": "Serwer lokalny 8545" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 1c3bba86966d..50bf0a7d9996 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Por favor, conclua a transação no Snap." }, - "loadingTokens": { - "message": "Carregando tokens..." - }, "localhost": { "message": "Host local 8545" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 0bc1005ad674..3c61987263b7 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1086,9 +1086,6 @@ "loading": { "message": "Carregando..." }, - "loadingTokens": { - "message": "Carregando tokens..." - }, "localhost": { "message": "Host local 8545" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 912accba29be..f149a976cf1a 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -375,9 +375,6 @@ "loading": { "message": "Se încarcă…" }, - "loadingTokens": { - "message": "Se încarcă token-urile..." - }, "lock": { "message": "Deconectați-vă" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index fd0b0b84fcb4..b1445befdd17 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Завершите транзакцию в Snap." }, - "loadingTokens": { - "message": "Загрузка токенов..." - }, "localhost": { "message": "Локальный хост 8545" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 829435f28ff7..1616af135d1a 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -368,9 +368,6 @@ "loading": { "message": "Načítám..." }, - "loadingTokens": { - "message": "Načítám tokeny..." - }, "lock": { "message": "Odhlásit" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index cb82e0358212..dd73bc8e373f 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -375,9 +375,6 @@ "loading": { "message": "Nalaganje ..." }, - "loadingTokens": { - "message": "Nalaganje žetonov ..." - }, "lock": { "message": "Odjava" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index e15ae23086b3..c3986a0c0b98 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -378,9 +378,6 @@ "loading": { "message": "Учитава се..." }, - "loadingTokens": { - "message": "Učitavanje tokena..." - }, "lock": { "message": "Odjavi se" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 163cdebc426e..278515e908eb 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -375,9 +375,6 @@ "loading": { "message": "Läser in..." }, - "loadingTokens": { - "message": "Laddar tokens..." - }, "lock": { "message": "Logga ut" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index c1535d76cdd8..7c4a52f733fa 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -372,9 +372,6 @@ "loading": { "message": "Inapakia..." }, - "loadingTokens": { - "message": "Inapakia Vianzio..." - }, "lock": { "message": "Toka kwenye akaunti" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index d5d1929a2dc4..c5c36501cbb9 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -209,9 +209,6 @@ "loading": { "message": "ஏற்றுகிறது…" }, - "loadingTokens": { - "message": "டோக்கன்களை ஏற்றுகிறது ..." - }, "localhost": { "message": "லோக்கல் ஹோஸ்ட் 8545" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index e6c074fe1264..08c9cb0df6dc 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -194,9 +194,6 @@ "loading": { "message": "กำลังโหลด..." }, - "loadingTokens": { - "message": "กำลังโหลดโทเค็น..." - }, "lock": { "message": "ออกจากระบบ" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 4c201294dcdb..327be7838680 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Mangyaring kumpletuhin ang transaksyon sa Snap." }, - "loadingTokens": { - "message": "Nilo-load ang Mga Token..." - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index f55ea0c69c14..35ec9700e7d0 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Lütfen işlemi Snap üzerinde tamamlayın." }, - "loadingTokens": { - "message": "Tokenler yükleniyor..." - }, "localhost": { "message": "Yerel Ana Bilgisayar 8545" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index b0c011690910..36a181b03ab2 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -381,9 +381,6 @@ "loading": { "message": "Завантаження..." }, - "loadingTokens": { - "message": "Завантаження токенів…" - }, "localhost": { "message": "Локальний хост 8545" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 07262f16cbd2..21b187998966 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "Vui lòng hoàn tất giao dịch trên Snap." }, - "loadingTokens": { - "message": "Đang tải token..." - }, "localhost": { "message": "Máy chủ cục bộ 8545" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 944ad47289f0..635753750343 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -2775,9 +2775,6 @@ "loadingScreenSnapMessage": { "message": "请在Snap上完成交易。" }, - "loadingTokens": { - "message": "加载代币中……" - }, "localhost": { "message": "Localhost 8545" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index ed1793427a90..87d8ebfb5520 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -699,9 +699,6 @@ "loading": { "message": "載入..." }, - "loadingTokens": { - "message": "載入代幣..." - }, "lock": { "message": "鎖定" }, diff --git a/app/scripts/controllers/bridge-status/utils.ts b/app/scripts/controllers/bridge-status/utils.ts index d8dbac9e1590..5447b483e34f 100644 --- a/app/scripts/controllers/bridge-status/utils.ts +++ b/app/scripts/controllers/bridge-status/utils.ts @@ -8,9 +8,7 @@ import { StatusRequestWithSrcTxHash, StatusRequestDto, } from '../../../../shared/types/bridge-status'; -// TODO fix this -// eslint-disable-next-line import/no-restricted-paths -import { Quote } from '../../../../ui/pages/bridge/types'; +import type { Quote } from '../../../../shared/types/bridge'; import { validateResponse, validators } from './validators'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 3b0d095fa0c3..e35d3c41e67c 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -5,20 +5,19 @@ import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps'; import { flushPromises } from '../../../../test/lib/timer-helpers'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import * as bridgeUtil from '../../../../ui/pages/bridge/bridge.util'; +import * as bridgeUtil from '../../../../shared/modules/bridge-utils/bridge.util'; import * as balanceUtils from '../../../../shared/modules/bridge-utils/balance'; import mockBridgeQuotesErc20Native from '../../../../test/data/bridge/mock-quotes-erc20-native.json'; import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; import mockBridgeQuotesNativeErc20Eth from '../../../../test/data/bridge/mock-quotes-native-erc20-eth.json'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { QuoteResponse } from '../../../../ui/pages/bridge/types'; +import { + type QuoteResponse, + RequestStatus, +} from '../../../../shared/types/bridge'; import { decimalToHex } from '../../../../shared/modules/conversion.utils'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; -import { DEFAULT_BRIDGE_CONTROLLER_STATE, RequestStatus } from './constants'; +import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; const EMPTY_INIT_STATE = { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 4770c342587c..005e2d334e9f 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -12,9 +12,7 @@ import { fetchBridgeFeatureFlags, fetchBridgeQuotes, fetchBridgeTokens, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../../ui/pages/bridge/bridge.util'; +} from '../../../../shared/modules/bridge-utils/bridge.util'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; @@ -23,30 +21,24 @@ import { sumHexes, } from '../../../../shared/modules/conversion.utils'; import { - L1GasFees, - QuoteRequest, - QuoteResponse, - TxData, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../../ui/pages/bridge/types'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { isValidQuoteRequest } from '../../../../ui/pages/bridge/utils/quote'; + type L1GasFees, + type QuoteRequest, + type QuoteResponse, + type TxData, + type BridgeControllerState, + BridgeFeatureFlagsKey, + RequestStatus, +} from '../../../../shared/types/bridge'; +import { isValidQuoteRequest } from '../../../../shared/modules/bridge-utils/quote'; import { hasSufficientBalance } from '../../../../shared/modules/bridge-utils/balance'; import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { REFRESH_INTERVAL_MS } from '../../../../shared/constants/bridge'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, - REFRESH_INTERVAL_MS, - RequestStatus, METABRIDGE_CHAIN_TO_ADDRESS_MAP, } from './constants'; -import { - BridgeControllerState, - BridgeControllerMessenger, - BridgeFeatureFlagsKey, -} from './types'; +import type { BridgeControllerMessenger } from './types'; const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { bridgeState: { diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index 4903a9ee2858..65e2840cfb73 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -2,21 +2,15 @@ import { zeroAddress } from 'ethereumjs-util'; import { Hex } from '@metamask/utils'; import { BRIDGE_DEFAULT_SLIPPAGE, + DEFAULT_MAX_REFRESH_COUNT, METABRIDGE_ETHEREUM_ADDRESS, + REFRESH_INTERVAL_MS, } from '../../../../shared/constants/bridge'; import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { BridgeControllerState, BridgeFeatureFlagsKey } from './types'; +import { BridgeFeatureFlagsKey } from '../../../../shared/types/bridge'; +import type { BridgeControllerState } from '../../../../shared/types/bridge'; export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; -export const REFRESH_INTERVAL_MS = 30 * 1000; -const DEFAULT_MAX_REFRESH_COUNT = 5; - -export enum RequestStatus { - LOADING, - FETCHED, - ERROR, -} - export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { bridgeFeatureFlags: { [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index 7cdfa43cabd0..811b8dfb119b 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -2,64 +2,18 @@ import { ControllerStateChangeEvent, RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { Hex } from '@metamask/utils'; import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import { NetworkControllerFindNetworkClientIdByChainIdAction, NetworkControllerGetSelectedNetworkClientAction, } from '@metamask/network-controller'; -import { SwapsTokenObject } from '../../../../shared/constants/swaps'; -import { - L1GasFees, - QuoteRequest, - QuoteResponse, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../../ui/pages/bridge/types'; -import { ChainConfiguration } from '../../../../shared/types/bridge'; +import type { + BridgeBackgroundAction, + BridgeControllerState, + BridgeUserAction, +} from '../../../../shared/types/bridge'; import BridgeController from './bridge-controller'; -import { BRIDGE_CONTROLLER_NAME, RequestStatus } from './constants'; - -export enum BridgeFeatureFlagsKey { - EXTENSION_CONFIG = 'extensionConfig', -} - -export type BridgeFeatureFlags = { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; -}; - -export type BridgeControllerState = { - bridgeFeatureFlags: BridgeFeatureFlags; - srcTokens: Record; - srcTopAssets: { address: string }[]; - srcTokensLoadingStatus?: RequestStatus; - destTokensLoadingStatus?: RequestStatus; - destTokens: Record; - destTopAssets: { address: string }[]; - quoteRequest: Partial; - quotes: (QuoteResponse & L1GasFees)[]; - quotesInitialLoadTime?: number; - quotesLastFetched?: number; - quotesLoadingStatus?: RequestStatus; - quoteFetchError?: string; - quotesRefreshCount: number; -}; - -export enum BridgeUserAction { - SELECT_SRC_NETWORK = 'selectSrcNetwork', - SELECT_DEST_NETWORK = 'selectDestNetwork', - UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', -} -export enum BridgeBackgroundAction { - SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', - RESET_STATE = 'resetState', - GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', -} +import { BRIDGE_CONTROLLER_NAME } from './constants'; type BridgeControllerAction = { type: `${typeof BRIDGE_CONTROLLER_NAME}:${FunctionName}`; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8ab78f2c7e92..933522c449f6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -250,6 +250,10 @@ import { isSnapId } from '../../ui/helpers/utils/snaps'; import { BridgeStatusAction } from '../../shared/types/bridge-status'; import { ENVIRONMENT } from '../../development/build/constants'; import fetchWithCache from '../../shared/lib/fetch-with-cache'; +import { + BridgeUserAction, + BridgeBackgroundAction, +} from '../../shared/types/bridge'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -365,10 +369,6 @@ import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; import { decodeTransactionData } from './lib/transaction/decode/util'; -import { - BridgeUserAction, - BridgeBackgroundAction, -} from './controllers/bridge/types'; import BridgeController from './controllers/bridge/bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; import { diff --git a/app/scripts/migrations/133.2.test.ts b/app/scripts/migrations/133.2.test.ts new file mode 100644 index 000000000000..18251d8f4b2b --- /dev/null +++ b/app/scripts/migrations/133.2.test.ts @@ -0,0 +1,185 @@ +import { migrate, version } from './133.2'; + +const oldVersion = 133.1; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('does nothing if theres no tokens controller state defined', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if theres empty tokens controller state', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: {}, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if theres empty tokens controller state for allTokens', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: { + allTokens: {}, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if theres empty tokens controller state for mainnet', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: { + allTokens: { + '0x1': {}, + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('Does nothing if theres no tokens with empty address', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: { + allTokens: { + '0x1': { + '0x123': [ + { address: '0x1', symbol: 'TOKEN1', decimals: 18 }, + { address: '0x2', symbol: 'TOKEN2', decimals: 18 }, + ], + '0x123456': [ + { address: '0x3', symbol: 'TOKEN3', decimals: 18 }, + { address: '0x4', symbol: 'TOKEN4', decimals: 18 }, + ], + }, + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('Removes tokens with empty address', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: { + allTokens: { + '0x1': { + '0x123': [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'eth', + decimals: 18, + }, + { address: '0x2', symbol: 'TOKEN2', decimals: 18 }, + ], + }, + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + TokensController: { + allTokens: { + '0x1': { + '0x123': [{ address: '0x2', symbol: 'TOKEN2', decimals: 18 }], + }, + }, + }, + }); + }); + + it('Removes tokens with empty address across multiple accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: { + allTokens: { + '0x1': { + '0x123': [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'eth', + decimals: 18, + }, + { address: '0x2', symbol: 'TOKEN2', decimals: 18 }, + ], + '0x456': [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'eth', + decimals: 18, + }, + { address: '0x3', symbol: 'TOKEN3', decimals: 18 }, + ], + '0x789': [{ address: '0x4', symbol: 'TOKEN4', decimals: 18 }], + }, + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + TokensController: { + allTokens: { + '0x1': { + '0x123': [{ address: '0x2', symbol: 'TOKEN2', decimals: 18 }], + '0x456': [{ address: '0x3', symbol: 'TOKEN3', decimals: 18 }], + '0x789': [{ address: '0x4', symbol: 'TOKEN4', decimals: 18 }], + }, + }, + }, + }); + }); + + it('Does not change state on chains other than mainnet', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + TokensController: { + allTokens: { + '0x999': { + '0x123': [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'eth', + decimals: 18, + }, + { address: '0x2', symbol: 'TOKEN2', decimals: 18 }, + ], + }, + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); +}); diff --git a/app/scripts/migrations/133.2.ts b/app/scripts/migrations/133.2.ts new file mode 100644 index 000000000000..6ad8ff888cfd --- /dev/null +++ b/app/scripts/migrations/133.2.ts @@ -0,0 +1,53 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 133.2; + +/** + * This migration removes tokens on mainnet with the + * zero address, since this is not a valid erc20 token. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to disk. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record): void { + if ( + !hasProperty(state, 'TokensController') || + !isObject(state.TokensController) || + !isObject(state.TokensController.allTokens) + ) { + return; + } + + const chainIds = ['0x1']; + + for (const chainId of chainIds) { + const allTokensOnChain = state.TokensController.allTokens[chainId]; + + if (isObject(allTokensOnChain)) { + for (const [account, tokens] of Object.entries(allTokensOnChain)) { + if (Array.isArray(tokens)) { + allTokensOnChain[account] = tokens.filter( + (token) => + token?.address !== '0x0000000000000000000000000000000000000000', + ); + } + } + } + } +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 6673d1e62d2f..8700b833d8de 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -156,6 +156,7 @@ const migrations = [ require('./132'), require('./133'), require('./133.1'), + require('./133.2'), require('./134'), ]; diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 82075f709022..05a22743204c 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2155,7 +2155,7 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": true } }, "@noble/hashes": { @@ -2170,6 +2170,24 @@ "crypto": true } }, + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { "globals": { "TextEncoder": true, @@ -2248,7 +2266,7 @@ "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/utils>@scure/base": true } }, @@ -3448,7 +3466,7 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true } }, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 82075f709022..05a22743204c 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2155,7 +2155,7 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": true } }, "@noble/hashes": { @@ -2170,6 +2170,24 @@ "crypto": true } }, + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { "globals": { "TextEncoder": true, @@ -2248,7 +2266,7 @@ "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/utils>@scure/base": true } }, @@ -3448,7 +3466,7 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true } }, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 82075f709022..05a22743204c 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2155,7 +2155,7 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": true } }, "@noble/hashes": { @@ -2170,6 +2170,24 @@ "crypto": true } }, + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { "globals": { "TextEncoder": true, @@ -2248,7 +2266,7 @@ "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/utils>@scure/base": true } }, @@ -3448,7 +3466,7 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true } }, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index a2453b1686dc..27ddfc86ff15 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2247,7 +2247,7 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": true } }, "@noble/hashes": { @@ -2262,6 +2262,24 @@ "crypto": true } }, + "@ethereumjs/tx>ethereum-cryptography>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { "globals": { "TextEncoder": true, @@ -2340,7 +2358,7 @@ "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/utils>@scure/base": true } }, @@ -3540,7 +3558,7 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography>@noble/curves": true, - "@noble/hashes": true, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true } }, diff --git a/package.json b/package.json index 261ddd874484..4534c2c15c2f 100644 --- a/package.json +++ b/package.json @@ -286,7 +286,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A45.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@npm%253A45.1.0%23~/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch%3A%3Aversion=45.1.0&hash=cfcadc#~/.yarn/patches/@metamask-assets-controllers-patch-d6ed5f8213.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/browser-passworder": "^4.3.0", @@ -342,15 +342,15 @@ "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^19.0.0", "@metamask/signature-controller": "^23.1.0", - "@metamask/smart-transactions-controller": "^15.0.0", + "@metamask/smart-transactions-controller": "^16.0.0", "@metamask/snaps-controllers": "^9.15.0", "@metamask/snaps-execution-environments": "^6.10.0", "@metamask/snaps-rpc-methods": "^11.7.0", "@metamask/snaps-sdk": "^6.13.0", "@metamask/snaps-utils": "^8.6.1", - "@metamask/solana-wallet-snap": "^1.0.3", + "@metamask/solana-wallet-snap": "^1.0.4", "@metamask/transaction-controller": "^42.0.0", - "@metamask/user-operation-controller": "^19.0.0", + "@metamask/user-operation-controller": "^21.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", "@noble/hashes": "^1.3.3", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index fc5dafb7333c..24c6e9ae27da 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -3,6 +3,7 @@ "accounts.api.cx.metamask.io", "acl.execution.metamask.io", "api.blockchair.com", + "api.devnet.solana.com", "api.lens.dev", "api.segment.io", "api.web3modal.com", @@ -59,6 +60,7 @@ "sepolia.infura.io", "signature-insights.api.cx.metamask.io", "snaps.metamask.io", + "solana.rpc.grove.city", "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index ef7cb7f8a785..fc5ecb157016 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -27,7 +27,7 @@ export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; export const METABRIDGE_ETHEREUM_ADDRESS = '0x0439e60F02a8900a951603950d8D4527f400C3f1'; export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour -export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.8; // if a quote returns in x times less return than the best quote, ignore it +export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; @@ -47,3 +47,5 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< [CHAIN_IDS.BASE]: 'Base', }; export const BRIDGE_MM_FEE_RATE = 0.875; +export const REFRESH_INTERVAL_MS = 30 * 1000; +export const DEFAULT_MAX_REFRESH_COUNT = 5; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 9b99140b7d24..8b59e95e650c 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -645,6 +645,9 @@ export enum MetaMetricsEventName { AppUnlockedFailed = 'App Unlocked Failed', AppLocked = 'App Locked', AppWindowExpanded = 'App Window Expanded', + BannerDisplay = 'Banner Display', + BannerCloseAll = 'Banner Close All', + BannerSelect = 'Banner Select', BridgeLinkClicked = 'Bridge Link Clicked', BitcoinSupportToggled = 'Bitcoin Support Toggled', BitcoinTestnetSupportToggled = 'Bitcoin Testnet Support Toggled', @@ -912,6 +915,7 @@ export enum MetaMetricsEventCategory { App = 'App', Auth = 'Auth', Background = 'Background', + Banner = 'Banner', // The TypeScript ESLint rule is incorrectly marking this line. /* eslint-disable-next-line @typescript-eslint/no-shadow */ Error = 'Error', diff --git a/ui/pages/bridge/bridge.util.test.ts b/shared/modules/bridge-utils/bridge.util.test.ts similarity index 98% rename from ui/pages/bridge/bridge.util.test.ts rename to shared/modules/bridge-utils/bridge.util.test.ts index f302cd44090c..555de4fc2516 100644 --- a/ui/pages/bridge/bridge.util.test.ts +++ b/shared/modules/bridge-utils/bridge.util.test.ts @@ -1,8 +1,8 @@ -import fetchWithCache from '../../../shared/lib/fetch-with-cache'; -import { CHAIN_IDS } from '../../../shared/constants/network'; +import { zeroAddress } from 'ethereumjs-util'; +import fetchWithCache from '../../lib/fetch-with-cache'; +import { CHAIN_IDS } from '../../constants/network'; import mockBridgeQuotesErc20Erc20 from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json'; -import { zeroAddress } from '../../__mocks__/ethereumjs-util'; import { fetchBridgeFeatureFlags, fetchBridgeQuotes, diff --git a/ui/pages/bridge/bridge.util.ts b/shared/modules/bridge-utils/bridge.util.ts similarity index 87% rename from ui/pages/bridge/bridge.util.ts rename to shared/modules/bridge-utils/bridge.util.ts index 534ea3418219..b7da42ba9cc6 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/shared/modules/bridge-utils/bridge.util.ts @@ -1,36 +1,25 @@ import { Contract } from '@ethersproject/contracts'; import { Hex, add0x } from '@metamask/utils'; import { abiERC20 } from '@metamask/metamask-eth-abis'; -import { - BridgeFeatureFlagsKey, - BridgeFeatureFlags, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../app/scripts/controllers/bridge/types'; import { BRIDGE_API_BASE_URL, BRIDGE_CLIENT_ID, ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS, -} from '../../../shared/constants/bridge'; -import { MINUTE } from '../../../shared/constants/time'; -import fetchWithCache from '../../../shared/lib/fetch-with-cache'; -import { - decimalToHex, - hexToDecimal, -} from '../../../shared/modules/conversion.utils'; + REFRESH_INTERVAL_MS, +} from '../../constants/bridge'; +import { MINUTE } from '../../constants/time'; +import fetchWithCache from '../../lib/fetch-with-cache'; +import { decimalToHex, hexToDecimal } from '../conversion.utils'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, SwapsTokenObject, -} from '../../../shared/constants/swaps'; +} from '../../constants/swaps'; import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, -} from '../../../shared/modules/swaps.utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; -import { CHAIN_IDS } from '../../../shared/constants/network'; +} from '../swaps.utils'; +import { CHAIN_IDS } from '../../constants/network'; import { BridgeAsset, BridgeFlag, @@ -41,7 +30,9 @@ import { QuoteRequest, QuoteResponse, TxData, -} from './types'; + BridgeFeatureFlagsKey, + BridgeFeatureFlags, +} from '../../types/bridge'; import { FEATURE_FLAG_VALIDATORS, QUOTE_VALIDATORS, @@ -50,7 +41,7 @@ import { validateResponse, QUOTE_RESPONSE_VALIDATORS, FEE_DATA_VALIDATORS, -} from './utils/validators'; +} from './validators'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; diff --git a/shared/modules/bridge-utils/quote.ts b/shared/modules/bridge-utils/quote.ts new file mode 100644 index 000000000000..3fe4406fa333 --- /dev/null +++ b/shared/modules/bridge-utils/quote.ts @@ -0,0 +1,36 @@ +import type { QuoteRequest } from '../../types/bridge'; + +export const isValidQuoteRequest = ( + partialRequest: Partial, + requireAmount = true, +): partialRequest is QuoteRequest => { + const STRING_FIELDS = ['srcTokenAddress', 'destTokenAddress']; + if (requireAmount) { + STRING_FIELDS.push('srcTokenAmount'); + } + const NUMBER_FIELDS = ['srcChainId', 'destChainId', 'slippage']; + + return ( + STRING_FIELDS.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'string' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + partialRequest[field as keyof typeof partialRequest] !== '' && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + NUMBER_FIELDS.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'number' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + !isNaN(Number(partialRequest[field as keyof typeof partialRequest])) && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + (requireAmount + ? Boolean((partialRequest.srcTokenAmount ?? '').match(/^[1-9]\d*$/u)) + : true) + ); +}; diff --git a/ui/pages/bridge/utils/validators.ts b/shared/modules/bridge-utils/validators.ts similarity index 96% rename from ui/pages/bridge/utils/validators.ts rename to shared/modules/bridge-utils/validators.ts index 08fc3519ef52..edfbabdafaa3 100644 --- a/ui/pages/bridge/utils/validators.ts +++ b/shared/modules/bridge-utils/validators.ts @@ -1,10 +1,7 @@ import { isStrictHexString } from '@metamask/utils'; import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; -import { - truthyDigitString, - validateData, -} from '../../../../shared/lib/swaps-utils'; -import { BridgeFlag, FeatureFlagResponse } from '../types'; +import { truthyDigitString, validateData } from '../../lib/swaps-utils'; +import { BridgeFlag, FeatureFlagResponse } from '../../types/bridge'; type Validator = { property: keyof ExpectedResponse | string; diff --git a/shared/types/bridge-status.ts b/shared/types/bridge-status.ts index fc9357ef968a..3e81f6e74f1d 100644 --- a/shared/types/bridge-status.ts +++ b/shared/types/bridge-status.ts @@ -1,12 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -// TODO fix this -import { - ChainId, - Quote, - QuoteMetadata, - QuoteResponse, - // eslint-disable-next-line import/no-restricted-paths -} from '../../ui/pages/bridge/types'; +import type { ChainId, Quote, QuoteMetadata, QuoteResponse } from './bridge'; // All fields need to be types not interfaces, same with their children fields // o/w you get a type error diff --git a/shared/types/bridge.ts b/shared/types/bridge.ts index 6cfab93f47ed..763c2670abad 100644 --- a/shared/types/bridge.ts +++ b/shared/types/bridge.ts @@ -1,4 +1,202 @@ +import type { Hex } from '@metamask/utils'; +import type { BigNumber } from 'bignumber.js'; +import type { AssetType } from '../constants/transaction'; +import type { SwapsTokenObject } from '../constants/swaps'; + export type ChainConfiguration = { isActiveSrc: boolean; isActiveDest: boolean; }; + +export type L1GasFees = { + l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller +}; +// Values derived from the quote response +// valueInCurrency values are calculated based on the user's selected currency + +export type QuoteMetadata = { + gasFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; + totalNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // estimatedGasFees + relayerFees + totalMaxNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // maxGasFees + relayerFees + toTokenAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; + adjustedReturn: { valueInCurrency: BigNumber | null }; // destTokenAmount - totalNetworkFee + sentAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; // srcTokenAmount + metabridgeFee + swapRate: BigNumber; // destTokenAmount / sentAmount + cost: { valueInCurrency: BigNumber | null }; // sentAmount - adjustedReturn +}; +// Sort order set by the user + +export enum SortOrder { + COST_ASC = 'cost_ascending', + ETA_ASC = 'time_descending', +} + +export type BridgeToken = { + type: AssetType.native | AssetType.token; + address: string; + symbol: string; + image: string; + decimals: number; + chainId: Hex; + balance: string; // raw balance + string: string | undefined; // normalized balance as a stringified number + tokenFiatAmount?: number | null; +} | null; +// Types copied from Metabridge API + +export enum BridgeFlag { + EXTENSION_CONFIG = 'extension-config', +} +type DecimalChainId = string; +export type GasMultiplierByChainId = Record; + +export type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; + }; +}; + +export type BridgeAsset = { + chainId: ChainId; + address: string; + symbol: string; + name: string; + decimals: number; + icon?: string; +}; + +export type QuoteRequest = { + walletAddress: string; + destWalletAddress?: string; + srcChainId: ChainId; + destChainId: ChainId; + srcTokenAddress: string; + destTokenAddress: string; + srcTokenAmount: string; // This is the amount sent + slippage: number; + aggIds?: string[]; + bridgeIds?: string[]; + insufficientBal?: boolean; + resetApproval?: boolean; + refuel?: boolean; +}; +type Protocol = { + name: string; + displayName?: string; + icon?: string; +}; +enum ActionTypes { + BRIDGE = 'bridge', + SWAP = 'swap', + REFUEL = 'refuel', +} +type Step = { + action: ActionTypes; + srcChainId: ChainId; + destChainId?: ChainId; + srcAsset: BridgeAsset; + destAsset: BridgeAsset; + srcAmount: string; + destAmount: string; + protocol: Protocol; +}; +type RefuelData = Step; + +export type Quote = { + requestId: string; + srcChainId: ChainId; + srcAsset: BridgeAsset; + // Some tokens have a fee of 0, so sometimes it's equal to amount sent + srcTokenAmount: string; // Atomic amount, the amount sent - fees + destChainId: ChainId; + destAsset: BridgeAsset; + destTokenAmount: string; // Atomic amount, the amount received + feeData: Record & + Partial>; + bridgeId: string; + bridges: string[]; + steps: Step[]; + refuel?: RefuelData; +}; + +export type QuoteResponse = { + quote: Quote; + approval: TxData | null; + trade: TxData; + estimatedProcessingTimeInSeconds: number; +}; + +export enum ChainId { + ETH = 1, + OPTIMISM = 10, + BSC = 56, + POLYGON = 137, + ZKSYNC = 324, + BASE = 8453, + ARBITRUM = 42161, + AVALANCHE = 43114, + LINEA = 59144, +} + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', +} +export type FeeData = { + amount: string; + asset: BridgeAsset; +}; +export type TxData = { + chainId: ChainId; + to: string; + from: string; + value: string; + data: string; + gasLimit: number | null; +}; +export enum BridgeFeatureFlagsKey { + EXTENSION_CONFIG = 'extensionConfig', +} + +export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; + }; +}; +export enum RequestStatus { + LOADING, + FETCHED, + ERROR, +} +export enum BridgeUserAction { + SELECT_SRC_NETWORK = 'selectSrcNetwork', + SELECT_DEST_NETWORK = 'selectDestNetwork', + UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', +} +export enum BridgeBackgroundAction { + SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', + RESET_STATE = 'resetState', + GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', +} +export type BridgeControllerState = { + bridgeFeatureFlags: BridgeFeatureFlags; + srcTokens: Record; + srcTopAssets: { address: string }[]; + srcTokensLoadingStatus?: RequestStatus; + destTokensLoadingStatus?: RequestStatus; + destTokens: Record; + destTopAssets: { address: string }[]; + quoteRequest: Partial; + quotes: (QuoteResponse & L1GasFees)[]; + quotesInitialLoadTime?: number; + quotesLastFetched?: number; + quotesLoadingStatus?: RequestStatus; + quoteFetchError?: string; + quotesRefreshCount: number; +}; diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 3131b8335287..9ea2e674e7a3 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1891,6 +1891,7 @@ "watchEthereumAccountEnabled": false, "bitcoinSupportEnabled": false, "bitcoinTestnetSupportEnabled": false, + "solanaSupportEnabled": false, "pendingApprovals": { "testApprovalId": { "id": "testApprovalId", diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 8296cdc3e3d1..9d4d5bbf3c8d 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -57,6 +57,9 @@ export const DEFAULT_BTC_FEES_RATE = 0.00001; // BTC /* Default BTC conversion rate to USD */ export const DEFAULT_BTC_CONVERSION_RATE = 62000; // USD +/* Default SOL conversion rate to USD */ +export const DEFAULT_SOL_CONVERSION_RATE = 226; // USD + /* Default BTC transaction ID */ export const DEFAULT_BTC_TRANSACTION_ID = 'e4111a707317da67d49a71af4cbcf6c0546f900ca32c3842d2254e315d1fca18'; diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index 05f382281e29..db2db85eb554 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -14,6 +14,7 @@ import { Driver } from '../../webdriver/driver'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import { ACCOUNT_TYPE } from '../../page-objects/common'; const QUICKNODE_URL_REGEX = /^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u; @@ -217,7 +218,7 @@ export async function withBtcAccountSnap( await new HeaderNavbar(driver).openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewBtcAccount(); + await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); await test(driver, mockServer); }, ); diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index 0dfb24a8a931..f563454ad1e6 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -7,6 +7,7 @@ import LoginPage from '../../page-objects/pages/login-page'; import PrivacySettings from '../../page-objects/pages/settings/privacy-settings'; import ResetPasswordPage from '../../page-objects/pages/reset-password-page'; import SettingsPage from '../../page-objects/pages/settings/settings-page'; +import { ACCOUNT_TYPE } from '../../page-objects/common'; import { withBtcAccountSnap } from './common-btc'; describe('Create BTC Account', function (this: Suite) { @@ -34,14 +35,12 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewBtcAccount({ - btcAccountCreationEnabled: false, - }); - - // check the number of available accounts is 2 - await headerNavbar.openAccountMenu(); - await accountListPage.check_pageIsLoaded(); await accountListPage.check_numberOfAvailableAccounts(2); + await accountListPage.openAddAccountModal(); + assert.equal( + await accountListPage.isBtcAccountCreationButtonEnabled(), + false, + ); }, ); }); @@ -91,8 +90,14 @@ describe('Create BTC Account', function (this: Suite) { // Recreate account and check that the address is the same await headerNavbar.openAccountMenu(); - await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewBtcAccount(); + await accountListPage.openAddAccountModal(); + assert.equal( + await accountListPage.isBtcAccountCreationButtonEnabled(), + true, + ); + await accountListPage.closeAccountModal(); + await headerNavbar.openAccountMenu(); + await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); await headerNavbar.check_accountLabel('Bitcoin Account'); await headerNavbar.openAccountMenu(); @@ -146,7 +151,7 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.check_pageIsLoaded(); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewBtcAccount(); + await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); await headerNavbar.check_accountLabel('Bitcoin Account'); await headerNavbar.openAccountMenu(); diff --git a/test/e2e/flask/solana/common-solana.ts b/test/e2e/flask/solana/common-solana.ts new file mode 100644 index 000000000000..2ac107bb442c --- /dev/null +++ b/test/e2e/flask/solana/common-solana.ts @@ -0,0 +1,71 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import FixtureBuilder from '../../fixture-builder'; +import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +const SOLANA_URL_REGEX = /^https:\/\/.*\.solana.*/u; + +export enum SendFlowPlaceHolders { + AMOUNT = 'Enter amount to send', + RECIPIENT = 'Enter receiving address', + LOADING = 'Preparing transaction', +} + +export async function mockSolanaBalanceQuote(mockServer: Mockttp) { + return await mockServer + .forPost(SOLANA_URL_REGEX) + .withJsonBodyIncluding({ + method: 'getBalance', + }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: { + context: { + apiVersion: '2.0.15', + slot: 305352614, + }, + value: 0, + }, + }, + }; + }); +} + +export async function withSolanaAccountSnap( + { + title, + solanaSupportEnabled, + }: { title?: string; solanaSupportEnabled?: boolean }, + test: (driver: Driver, mockServer: Mockttp) => Promise, +) { + console.log('Starting withSolanaAccountSnap'); + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + solanaSupportEnabled: solanaSupportEnabled ?? true, + }) + .build(), + title, + dapp: true, + testSpecificMock: async (mockServer: Mockttp) => { + console.log('Setting up test-specific mocks'); + return [await mockSolanaBalanceQuote(mockServer)]; + }, + }, + async ({ driver, mockServer }: { driver: Driver; mockServer: Mockttp }) => { + await loginWithBalanceValidation(driver); + const headerComponen = new HeaderNavbar(driver); + await headerComponen.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.addAccount(ACCOUNT_TYPE.Solana, 'Solana 1'); + await test(driver, mockServer); + }, + ); +} diff --git a/test/e2e/flask/solana/create-solana-account.spec.ts b/test/e2e/flask/solana/create-solana-account.spec.ts new file mode 100644 index 000000000000..cca4f5222993 --- /dev/null +++ b/test/e2e/flask/solana/create-solana-account.spec.ts @@ -0,0 +1,62 @@ +import { Suite } from 'mocha'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { withSolanaAccountSnap } from './common-solana'; + +// Scenarios skipped due to https://consensyssoftware.atlassian.net/browse/SOL-87 +describe('Create Solana Account', function (this: Suite) { + it.skip('Creates 2 Solana accounts', async function () { + await withSolanaAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + // check that we have one Solana account + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.check_accountLabel('Solana 1'); + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_accountDisplayedInAccountList('Account 1'); + await accountListPage.addAccount(ACCOUNT_TYPE.Solana, 'Solana 2'); + await headerNavbar.check_accountLabel('Solana 2'); + await headerNavbar.openAccountMenu(); + await accountListPage.check_numberOfAvailableAccounts(3); + }, + ); + }); + it('Creates a Solana account from the menu', async function () { + await withSolanaAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.check_accountLabel('Solana 1'); + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_accountDisplayedInAccountList('Account 1'); + await accountListPage.check_accountDisplayedInAccountList('Solana 1'); + }, + ); + }); +}); +it.skip('Removes Solana account after creating it', async function () { + await withSolanaAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + // check that we have one Solana account + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('Solana 1'); + // check user can cancel the removal of the Solana account + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_accountDisplayedInAccountList('Account 1'); + await accountListPage.removeAccount('Solana 1', true); + await headerNavbar.check_accountLabel('Account 1'); + await headerNavbar.openAccountMenu(); + await accountListPage.check_accountDisplayedInAccountList('Account 1'); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'Solana 1', + ); + }, + ); +}); diff --git a/test/e2e/flask/solana/solana-eth-networks.spec.ts b/test/e2e/flask/solana/solana-eth-networks.spec.ts new file mode 100644 index 000000000000..56a68135fad8 --- /dev/null +++ b/test/e2e/flask/solana/solana-eth-networks.spec.ts @@ -0,0 +1,24 @@ +import { Suite } from 'mocha'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import { withSolanaAccountSnap } from './common-solana'; + +describe('Solana/Evm accounts', function (this: Suite) { + it('Network picker is disabled when Solana account is selected', async function () { + await withSolanaAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.check_accountLabel('Solana 1'); + await headerNavbar.check_currentSelectedNetwork('Solana'); + await headerNavbar.check_ifNetworkPickerClickable(false); + await headerNavbar.openAccountMenu(); + const accountMenu = new AccountListPage(driver); + await accountMenu.switchToAccount('Account 1'); + await headerNavbar.check_currentSelectedNetwork('Localhost 8545'); + await headerNavbar.check_ifNetworkPickerClickable(true); + }, + ); + }); +}); diff --git a/test/e2e/page-objects/common.ts b/test/e2e/page-objects/common.ts index 5bf1a91e1859..40eb625d94ac 100644 --- a/test/e2e/page-objects/common.ts +++ b/test/e2e/page-objects/common.ts @@ -2,3 +2,9 @@ export type RawLocator = | string | { css?: string; text?: string } | { tag: string; text: string }; + +export enum ACCOUNT_TYPE { + Ethereum, + Bitcoin, + Solana, +} diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 50eb70ce553c..628d758e7e71 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,7 +1,7 @@ -import { strict as assert } from 'assert'; import { Driver } from '../../webdriver/driver'; import { largeDelayMs, regularDelayMs } from '../../helpers'; import messages from '../../../../app/_locales/en/messages.json'; +import { ACCOUNT_TYPE } from '../common'; class AccountListPage { private readonly driver: Driver; @@ -40,6 +40,11 @@ class AccountListPage { tag: 'button', }; + private readonly addSolanaAccountButton = { + text: messages.addNewSolanaAccount.message, + tag: 'button', + }; + private readonly addEthereumAccountButton = '[data-testid="multichain-account-menu-popover-add-account"]'; @@ -163,48 +168,11 @@ class AccountListPage { ); } - /** - * Adds a new BTC account with an optional custom name. - * - * @param options - Options for adding a new BTC account. - * @param [options.btcAccountCreationEnabled] - Indicates if the BTC account creation is expected to be enabled or disabled. Defaults to true. - * @param [options.accountName] - The custom name for the BTC account. Defaults to an empty string, which means the default name will be used. - */ - async addNewBtcAccount({ - btcAccountCreationEnabled = true, - accountName = '', - }: { - btcAccountCreationEnabled?: boolean; - accountName?: string; - } = {}): Promise { - console.log( - `Adding new BTC account${ - accountName ? ` with custom name: ${accountName}` : ' with default name' - }`, + async isBtcAccountCreationButtonEnabled() { + const createButton = await this.driver.findElement( + this.addBtcAccountButton, ); - await this.driver.clickElement(this.createAccountButton); - if (btcAccountCreationEnabled) { - await this.driver.clickElement(this.addBtcAccountButton); - // needed to mitigate a race condition with the state update - // there is no condition we can wait for in the UI - await this.driver.delay(largeDelayMs); - if (accountName) { - await this.driver.fill(this.accountNameInput, accountName); - } - await this.driver.clickElementAndWaitToDisappear( - this.addAccountConfirmButton, - // Longer timeout than usual, this reduces the flakiness - // around Bitcoin account creation (mainly required for - // Firefox) - 5000, - ); - } else { - const createButton = await this.driver.findElement( - this.addBtcAccountButton, - ); - assert.equal(await createButton.isEnabled(), false); - await this.driver.clickElement(this.closeAccountModalButton); - } + return await createButton.isEnabled(); } /** @@ -234,6 +202,48 @@ class AccountListPage { } } + /** + * Adds a new account of the specified type with an optional custom name. + * + * @param accountType - The type of account to add (Ethereum, Bitcoin, or Solana) + * @param accountName - Optional custom name for the new account + * @throws {Error} If the specified account type is not supported + * @example + * // Add a new Ethereum account with default name + * await accountListPage.addAccount(ACCOUNT_TYPE.Ethereum); + * + * // Add a new Bitcoin account with custom name + * await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, 'My BTC Wallet'); + */ + async addAccount(accountType: ACCOUNT_TYPE, accountName?: string) { + await this.driver.clickElement(this.createAccountButton); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let addAccountButton: any; + switch (accountType) { + case ACCOUNT_TYPE.Ethereum: + addAccountButton = this.addEthereumAccountButton; + break; + case ACCOUNT_TYPE.Bitcoin: + addAccountButton = this.addBtcAccountButton; + break; + case ACCOUNT_TYPE.Solana: + addAccountButton = this.addSolanaAccountButton; + break; + default: + throw new Error('Account type not supported'); + } + + await this.driver.clickElement(addAccountButton); + if (accountName) { + await this.driver.fill(this.accountNameInput, accountName); + } + + await this.driver.clickElementAndWaitToDisappear( + this.addAccountConfirmButton, + 5000, + ); + } + /** * Changes the label of the current account. * @@ -600,11 +610,17 @@ class AccountListPage { console.log( `Verify the number of accounts in the account menu is: ${expectedNumberOfAccounts}`, ); + + await this.driver.waitForSelector(this.accountListItem); await this.driver.wait(async () => { const internalAccounts = await this.driver.findElements( this.accountListItem, ); - return internalAccounts.length === expectedNumberOfAccounts; + const isValid = internalAccounts.length === expectedNumberOfAccounts; + console.log( + `Number of accounts: ${internalAccounts.length} is equal to ${expectedNumberOfAccounts}? ${isValid}`, + ); + return isValid; }, 20000); } diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index df45ecd0277d..57e02e864624 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -1,3 +1,4 @@ +import { strict as assert } from 'assert'; import { Driver } from '../../webdriver/driver'; class HeaderNavbar { @@ -22,6 +23,8 @@ class HeaderNavbar { private readonly switchNetworkDropDown = '[data-testid="network-display"]'; + private readonly networkPicker = '.mm-picker-network'; + constructor(driver: Driver) { this.driver = driver; } @@ -80,6 +83,21 @@ class HeaderNavbar { await this.driver.clickElement(this.switchNetworkDropDown); } + async check_currentSelectedNetwork(networkName: string): Promise { + console.log(`Validate the Switch network to ${networkName}`); + await this.driver.waitForSelector( + `button[data-testid="network-display"][aria-label="Network Menu ${networkName}"]`, + ); + } + + async check_ifNetworkPickerClickable(clickable: boolean): Promise { + console.log('Check whether the network picker is clickable or not'); + assert.equal( + await (await this.driver.findElement(this.networkPicker)).isEnabled(), + clickable, + ); + } + /** * Verifies that the displayed account label in header matches the expected label. * @@ -94,18 +112,6 @@ class HeaderNavbar { text: expectedLabel, }); } - - /** - * Validates that the currently selected network matches the expected network name. - * - * @param networkName - The expected name of the currently selected network. - */ - async check_currentSelectedNetwork(networkName: string): Promise { - console.log(`Validate the Switch network to ${networkName}`); - await this.driver.waitForSelector( - `button[data-testid="network-display"][aria-label="Network Menu ${networkName}"]`, - ); - } } export default HeaderNavbar; diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index cf57deb59f96..842ff31fff39 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -12,7 +12,7 @@ import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { Driver } from '../../webdriver/driver'; import { isManifestV3 } from '../../../../shared/modules/mv3.utils'; -import { FeatureFlagResponse } from '../../../../ui/pages/bridge/types'; +import type { FeatureFlagResponse } from '../../../../shared/types/bridge'; import { DEFAULT_FEATURE_FLAGS_RESPONSE, ETH_CONVERSION_RATE_USD, diff --git a/test/e2e/tests/bridge/constants.ts b/test/e2e/tests/bridge/constants.ts index d5b6da9afc61..844cec673509 100644 --- a/test/e2e/tests/bridge/constants.ts +++ b/test/e2e/tests/bridge/constants.ts @@ -1,4 +1,4 @@ -import { FeatureFlagResponse } from '../../../../ui/pages/bridge/types'; +import type { FeatureFlagResponse } from '../../../../shared/types/bridge'; export const DEFAULT_FEATURE_FLAGS_RESPONSE: FeatureFlagResponse = { 'extension-config': { diff --git a/test/e2e/tests/petnames/petnames-signatures.spec.js b/test/e2e/tests/petnames/petnames-signatures.spec.js index 4641424e053f..1ca7f47340d7 100644 --- a/test/e2e/tests/petnames/petnames-signatures.spec.js +++ b/test/e2e/tests/petnames/petnames-signatures.spec.js @@ -169,7 +169,7 @@ describe('Petnames - Signatures', function () { await createSignatureRequest(driver, SIGNATURE_TYPE.TYPED_V4); await switchToNotificationWindow(driver, 3); await expectName(driver, 'test.lens', true); - await expectName(driver, 'Test Token 2', true); + await expectName(driver, 'Test Toke...', true); await showThirdPartyDetails(driver); await expectName(driver, 'Custom Name', true); }, @@ -269,7 +269,7 @@ describe('Petnames - Signatures', function () { await createSignatureRequest(driver, SIGNATURE_TYPE.TYPED_V4); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await expectName(driver, 'test.lens', true); - await expectName(driver, 'Test Token 2', true); + await expectName(driver, 'Test Toke...', true); await expectName(driver, 'Custom Name', true); }, ); diff --git a/test/integration/confirmations/signatures/permit-batch.test.tsx b/test/integration/confirmations/signatures/permit-batch.test.tsx index ea311537001c..7a3050dc15c8 100644 --- a/test/integration/confirmations/signatures/permit-batch.test.tsx +++ b/test/integration/confirmations/signatures/permit-batch.test.tsx @@ -1,10 +1,11 @@ import { act, fireEvent, screen } from '@testing-library/react'; import nock from 'nock'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; -import { createMockImplementation } from '../../helpers'; import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation } from '../../helpers'; import { getMetaMaskStateWithUnapprovedPermitSign, verifyDetails, @@ -15,10 +16,20 @@ jest.mock('../../../../ui/store/background-connection', () => ({ submitRequestToBackground: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), }; +const mockedAssetDetails = jest.mocked(useAssetDetails); const renderPermitBatchSignature = async () => { const account = @@ -58,6 +69,10 @@ describe('Permit Batch Signature Tests', () => { getTokenStandardAndDetails: { decimals: '2' }, }), ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/signatures/permit-seaport.test.tsx b/test/integration/confirmations/signatures/permit-seaport.test.tsx index 7829a2714b57..dc32671da245 100644 --- a/test/integration/confirmations/signatures/permit-seaport.test.tsx +++ b/test/integration/confirmations/signatures/permit-seaport.test.tsx @@ -1,10 +1,11 @@ import { act, fireEvent, screen } from '@testing-library/react'; import nock from 'nock'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; -import { createMockImplementation } from '../../helpers'; import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation } from '../../helpers'; import { getMetaMaskStateWithUnapprovedPermitSign, verifyDetails, @@ -15,10 +16,20 @@ jest.mock('../../../../ui/store/background-connection', () => ({ submitRequestToBackground: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), }; +const mockedAssetDetails = jest.mocked(useAssetDetails); const renderSeaportSignature = async () => { const account = @@ -58,6 +69,10 @@ describe('Permit Seaport Tests', () => { getTokenStandardAndDetails: { decimals: '2' }, }), ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/signatures/permit-single.test.tsx b/test/integration/confirmations/signatures/permit-single.test.tsx index abb5d07c1587..96fe5c26491d 100644 --- a/test/integration/confirmations/signatures/permit-single.test.tsx +++ b/test/integration/confirmations/signatures/permit-single.test.tsx @@ -1,10 +1,11 @@ import { act, fireEvent, screen } from '@testing-library/react'; import nock from 'nock'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; -import { createMockImplementation } from '../../helpers'; import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation } from '../../helpers'; import { getMetaMaskStateWithUnapprovedPermitSign, verifyDetails, @@ -15,10 +16,20 @@ jest.mock('../../../../ui/store/background-connection', () => ({ submitRequestToBackground: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), }; +const mockedAssetDetails = jest.mocked(useAssetDetails); const renderSingleBatchSignature = async () => { const account = @@ -58,6 +69,10 @@ describe('Permit Single Signature Tests', () => { getTokenStandardAndDetails: { decimals: '2' }, }), ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/signatures/permit-tradeOrder.test.tsx b/test/integration/confirmations/signatures/permit-tradeOrder.test.tsx index 429e533ff8f8..3f915967dc00 100644 --- a/test/integration/confirmations/signatures/permit-tradeOrder.test.tsx +++ b/test/integration/confirmations/signatures/permit-tradeOrder.test.tsx @@ -1,10 +1,11 @@ import { act, screen } from '@testing-library/react'; import nock from 'nock'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; -import { createMockImplementation } from '../../helpers'; import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation } from '../../helpers'; import { getMetaMaskStateWithUnapprovedPermitSign, verifyDetails, @@ -15,10 +16,20 @@ jest.mock('../../../../ui/store/background-connection', () => ({ submitRequestToBackground: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), }; +const mockedAssetDetails = jest.mocked(useAssetDetails); const renderTradeOrderSignature = async () => { const account = @@ -58,6 +69,10 @@ describe('Permit Trade Order Tests', () => { getTokenStandardAndDetails: { decimals: '2' }, }), ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 6b736e4add90..332cebc3a6b2 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -7,6 +7,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { integrationTestRender } from '../../../lib/render-helpers'; import mockMetaMaskState from '../../data/integration-init-state.json'; @@ -18,10 +19,20 @@ jest.mock('../../../../ui/store/background-connection', () => ({ submitRequestToBackground: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), }; +const mockedAssetDetails = jest.mocked(useAssetDetails); describe('Permit Confirmation', () => { beforeEach(() => { @@ -31,6 +42,10 @@ describe('Permit Confirmation', () => { getTokenStandardAndDetails: { decimals: '2', standard: 'ERC20' }, }), ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 03685f46ab7b..d58e0d441e03 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,6 +1,6 @@ import { ApprovalType } from '@metamask/controller-utils'; -import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -8,6 +8,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { integrationTestRender } from '../../../lib/render-helpers'; import mockMetaMaskState from '../../data/integration-init-state.json'; @@ -17,7 +18,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ submitRequestToBackground: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -68,6 +79,10 @@ const getMetaMaskStateWithUnapprovedPersonalSign = (accountAddress: string) => { describe('PersonalSign Confirmation', () => { beforeEach(() => { jest.resetAllMocks(); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); it('displays the header account modal with correct data', async () => { diff --git a/test/integration/confirmations/transactions/alerts.test.tsx b/test/integration/confirmations/transactions/alerts.test.tsx index b21a7dbb679c..ee7e95762ac3 100644 --- a/test/integration/confirmations/transactions/alerts.test.tsx +++ b/test/integration/confirmations/transactions/alerts.test.tsx @@ -1,12 +1,13 @@ import { randomUUID } from 'crypto'; -import { act, fireEvent, screen } from '@testing-library/react'; import { ApprovalType } from '@metamask/controller-utils'; +import { act, fireEvent, screen } from '@testing-library/react'; import nock from 'nock'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; -import { createMockImplementation, mock4byte } from '../../helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -15,7 +16,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -92,6 +103,10 @@ describe('Contract Interaction Confirmation Alerts', () => { setupSubmitRequestToBackgroundMocks(); const APPROVE_NFT_HEX_SIG = '0x095ea7b3'; mock4byte(APPROVE_NFT_HEX_SIG); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx index 7698ce3ef8b2..45410c0a76d9 100644 --- a/test/integration/confirmations/transactions/contract-deployment.test.tsx +++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx @@ -13,6 +13,7 @@ import { MetaMetricsEventLocation, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; @@ -26,7 +27,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -136,6 +147,10 @@ describe('Contract Deployment Confirmation', () => { setupSubmitRequestToBackgroundMocks(); const DEPOSIT_HEX_SIG = '0xd0e30db0'; mock4byte(DEPOSIT_HEX_SIG); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx index 5db121bc9fda..e3da57aeceb3 100644 --- a/test/integration/confirmations/transactions/contract-interaction.test.tsx +++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx @@ -13,6 +13,7 @@ import { MetaMetricsEventLocation, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; @@ -31,7 +32,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -156,6 +167,10 @@ describe('Contract Interaction Confirmation', () => { setupSubmitRequestToBackgroundMocks(); const MINT_NFT_HEX_SIG = '0x3b4b1381'; mock4byte(MINT_NFT_HEX_SIG); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/transactions/erc20-approve.test.tsx b/test/integration/confirmations/transactions/erc20-approve.test.tsx index c25b2ee3627d..f77c8162b79d 100644 --- a/test/integration/confirmations/transactions/erc20-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc20-approve.test.tsx @@ -3,6 +3,7 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; @@ -17,7 +18,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockImplementation(() => ({ + decimals: '4', + })), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -140,6 +151,10 @@ describe('ERC20 Approve Confirmation', () => { const APPROVE_ERC20_HEX_SIG = '0x095ea7b3'; const APPROVE_ERC20_TEXT_SIG = 'approve(address,uint256)'; mock4byte(APPROVE_ERC20_HEX_SIG, APPROVE_ERC20_TEXT_SIG); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/transactions/erc721-approve.test.tsx b/test/integration/confirmations/transactions/erc721-approve.test.tsx index 4f211576e6cd..cce5456ae0a2 100644 --- a/test/integration/confirmations/transactions/erc721-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc721-approve.test.tsx @@ -3,6 +3,7 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; @@ -17,7 +18,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -140,6 +151,10 @@ describe('ERC721 Approve Confirmation', () => { const APPROVE_NFT_HEX_SIG = '0x095ea7b3'; const APPROVE_NFT_TEXT_SIG = 'approve(address,uint256)'; mock4byte(APPROVE_NFT_HEX_SIG, APPROVE_NFT_TEXT_SIG); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { @@ -220,6 +235,7 @@ describe('ERC721 Approve Confirmation', () => { const approveDetails = await screen.findByTestId( 'confirmation__approve-details', ); + expect(approveDetails).toBeInTheDocument(); const approveDetailsSpender = await screen.findByTestId( 'confirmation__approve-spender', diff --git a/test/integration/confirmations/transactions/increase-allowance.test.tsx b/test/integration/confirmations/transactions/increase-allowance.test.tsx index 810477d3a3a5..49b33bf2d21b 100644 --- a/test/integration/confirmations/transactions/increase-allowance.test.tsx +++ b/test/integration/confirmations/transactions/increase-allowance.test.tsx @@ -3,6 +3,7 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; @@ -17,7 +18,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -144,6 +155,10 @@ describe('ERC20 increaseAllowance Confirmation', () => { INCREASE_ALLOWANCE_ERC20_HEX_SIG, INCREASE_ALLOWANCE_ERC20_TEXT_SIG, ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx index ebe680983a6c..236162c7b08f 100644 --- a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx +++ b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx @@ -3,6 +3,7 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAssetDetails } from '../../../../ui/pages/confirmations/hooks/useAssetDetails'; import * as backgroundConnection from '../../../../ui/store/background-connection'; import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; @@ -17,7 +18,17 @@ jest.mock('../../../../ui/store/background-connection', () => ({ callBackgroundMethod: jest.fn(), })); +jest.mock('../../../../ui/pages/confirmations/hooks/useAssetDetails', () => ({ + ...jest.requireActual( + '../../../../ui/pages/confirmations/hooks/useAssetDetails', + ), + useAssetDetails: jest.fn().mockResolvedValue({ + decimals: '4', + }), +})); + const mockedBackgroundConnection = jest.mocked(backgroundConnection); +const mockedAssetDetails = jest.mocked(useAssetDetails); const backgroundConnectionMocked = { onNotification: jest.fn(), @@ -144,6 +155,10 @@ describe('ERC721 setApprovalForAll Confirmation', () => { INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG, INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG, ); + mockedAssetDetails.mockImplementation(() => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decimals: '4' as any, + })); }); afterEach(() => { diff --git a/test/lib/render-helpers.js b/test/lib/render-helpers.js index 574415f2f3c6..d2161fe1cf2e 100644 --- a/test/lib/render-helpers.js +++ b/test/lib/render-helpers.js @@ -69,10 +69,15 @@ const createProviderWrapper = (store, pathname = '/') => { }; }; -export function renderWithProvider(component, store, pathname = '/') { +export function renderWithProvider( + component, + store, + pathname = '/', + renderer = render, +) { const { history, Wrapper } = createProviderWrapper(store, pathname); return { - ...render(component, { wrapper: Wrapper }), + ...renderer(component, { wrapper: Wrapper }), history, }; } diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 3cd06ec4686b..19d9ecdd4c0d 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -127,6 +127,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { className="" actionButtonOnClick={() => setShowDetectedTokens(true)} margin={4} + marginBottom={1} /> ) : null} diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 5b84827e6fc7..5555c8782f8a 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -28,7 +28,6 @@ import { calculateTokenFiatAmount } from '../util/calculateTokenFiatAmount'; import { endTrace, TraceName } from '../../../../../shared/lib/trace'; import { useTokenBalances } from '../../../../hooks/useTokenBalances'; import { setTokenNetworkFilter } from '../../../../store/actions'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; @@ -85,7 +84,6 @@ export default function TokenList({ onTokenClick, nativeToken, }: TokenListProps) { - const t = useI18nContext(); const dispatch = useDispatch(); const currentNetwork = useSelector(getCurrentNetwork); const allNetworks = useSelector(getNetworkConfigurationIdByChainId); @@ -234,12 +232,6 @@ export default function TokenList({ return React.cloneElement(nativeToken as React.ReactElement); } - // TODO: We can remove this string. However it will result in a huge file 50+ file diff - // Lets remove it in a separate PR - if (sortedFilteredTokens === undefined) { - console.log(t('loadingTokens')); - } - const shouldShowFiat = useMultichainSelector( getMultichainShouldShowFiat, selectedAccount, diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index 03f8625733b9..962b630562d1 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -1,83 +1,111 @@ -import Button from '../../ui/button'; -import Chip from '../../ui/chip'; -import DefinitionList from '../../ui/definition-list'; -import TruncatedDefinitionList from '../../ui/truncated-definition-list'; -import Popover from '../../ui/popover'; -import Typography from '../../ui/typography'; -import Box from '../../ui/box'; -import MetaMaskTranslation from '../metamask-translation'; -import NetworkDisplay from '../network-display'; -import TextArea from '../../ui/textarea/textarea'; -import TextField from '../../ui/text-field'; import ConfirmationNetworkSwitch from '../../../pages/confirmations/confirmation/components/confirmation-network-switch'; -import UrlIcon from '../../ui/url-icon'; -import Tooltip from '../../ui/tooltip/tooltip'; +import { SmartTransactionStatusPage } from '../../../pages/smart-transactions/smart-transaction-status-page'; import { AvatarIcon, + BannerAlert, FormTextField, Text, - BannerAlert, } from '../../component-library'; -import ActionableMessage from '../../ui/actionable-message/actionable-message'; import { AccountListItem } from '../../multichain'; +import ActionableMessage from '../../ui/actionable-message/actionable-message'; +import Box from '../../ui/box'; +import Button from '../../ui/button'; +import Chip from '../../ui/chip'; +import DefinitionList from '../../ui/definition-list'; +import Preloader from '../../ui/icon/preloader'; +import OriginPill from '../../ui/origin-pill/origin-pill'; +import Popover from '../../ui/popover'; +import Spinner from '../../ui/spinner'; +import TextField from '../../ui/text-field'; +import TextArea from '../../ui/textarea/textarea'; +import Tooltip from '../../ui/tooltip/tooltip'; +import TruncatedDefinitionList from '../../ui/truncated-definition-list'; +import Typography from '../../ui/typography'; +import UrlIcon from '../../ui/url-icon'; import { ConfirmInfoRow, ConfirmInfoRowAddress, ConfirmInfoRowValueDouble, } from '../confirm/info/row'; -import { SnapDelineator } from '../snaps/snap-delineator'; +import MetaMaskTranslation from '../metamask-translation'; +import NetworkDisplay from '../network-display'; import { Copyable } from '../snaps/copyable'; -import Spinner from '../../ui/spinner'; -import Preloader from '../../ui/icon/preloader'; -import { SnapUIMarkdown } from '../snaps/snap-ui-markdown'; -import { SnapUILink } from '../snaps/snap-ui-link'; -import { SmartTransactionStatusPage } from '../../../pages/smart-transactions/smart-transaction-status-page'; +import { SnapDelineator } from '../snaps/snap-delineator'; +import { SnapUIAddress } from '../snaps/snap-ui-address'; +import { SnapUIAvatar } from '../snaps/snap-ui-avatar'; +import { SnapUIButton } from '../snaps/snap-ui-button'; +import { SnapUICard } from '../snaps/snap-ui-card'; +import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; +import { SnapUIDropdown } from '../snaps/snap-ui-dropdown'; +import { SnapUIFileInput } from '../snaps/snap-ui-file-input'; +import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button'; +import { SnapUIForm } from '../snaps/snap-ui-form'; import { SnapUIIcon } from '../snaps/snap-ui-icon'; import { SnapUIImage } from '../snaps/snap-ui-image'; -import { SnapUIFileInput } from '../snaps/snap-ui-file-input'; import { SnapUIInput } from '../snaps/snap-ui-input'; -import { SnapUIForm } from '../snaps/snap-ui-form'; -import { SnapUIButton } from '../snaps/snap-ui-button'; -import { SnapUIDropdown } from '../snaps/snap-ui-dropdown'; +import { SnapUILink } from '../snaps/snap-ui-link'; +import { SnapUIMarkdown } from '../snaps/snap-ui-markdown'; import { SnapUIRadioGroup } from '../snaps/snap-ui-radio-group'; -import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; -import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; -import { SnapUICard } from '../snaps/snap-ui-card'; -import { SnapUIAddress } from '../snaps/snap-ui-address'; -import { SnapUIAvatar } from '../snaps/snap-ui-avatar'; import { SnapUISelector } from '../snaps/snap-ui-selector'; -import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button'; +import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import { SnapAccountSuccessMessage } from '../../../pages/confirmations/components/snap-account-success-message'; import { SnapAccountErrorMessage } from '../../../pages/confirmations/components/snap-account-error-message'; +import { SnapAccountSuccessMessage } from '../../../pages/confirmations/components/snap-account-success-message'; import { CreateSnapAccount } from '../../../pages/create-snap-account'; -import { CreateNamedSnapAccount } from '../../multichain/create-named-snap-account'; import { RemoveSnapAccount, SnapAccountCard, } from '../../../pages/remove-snap-account'; import { SnapAccountRedirect } from '../../../pages/snap-account-redirect'; +import { CreateNamedSnapAccount } from '../../multichain/create-named-snap-account'; import SnapAuthorshipHeader from '../snaps/snap-authorship-header'; ///: END:ONLY_INCLUDE_IF export const safeComponentList = { a: 'a', - ActionableMessage, AccountListItem, + ActionableMessage, AvatarIcon, b: 'b', + BannerAlert, Box, Button, Chip, ConfirmationNetworkSwitch, + ConfirmInfoRow, + ConfirmInfoRowAddress, + ConfirmInfoRowValueDouble, + Copyable, DefinitionList, div: 'div', + FormTextField, i: 'i', MetaMaskTranslation, NetworkDisplay, + OriginPill, p: 'p', Popover, + Preloader, + SnapDelineator, + SnapUIAddress, + SnapUIAvatar, + SnapUIButton, + SnapUICard, + SnapUICheckbox, + SnapUIDropdown, + SnapUIFileInput, + SnapUIForm, + SnapUIFooterButton, + SnapUIIcon, + SnapUIImage, + SnapUIInput, + SnapUILink, + SnapUIMarkdown, + SnapUIRadioGroup, + SnapUISelector, + SnapUITooltip, span: 'span', + Spinner, Text, TextArea, TextField, @@ -86,40 +114,14 @@ export const safeComponentList = { Typography, SmartTransactionStatusPage, UrlIcon, - Copyable, - SnapDelineator, - SnapUIMarkdown, - SnapUILink, - SnapUIIcon, - SnapUIImage, - BannerAlert, - Spinner, - Preloader, - ConfirmInfoRow, - ConfirmInfoRowAddress, - ConfirmInfoRowValueDouble, - SnapUIFileInput, - SnapUIInput, - SnapUIButton, - SnapUIForm, - SnapUIDropdown, - SnapUIRadioGroup, - SnapUICheckbox, - SnapUITooltip, - SnapUICard, - SnapUISelector, - SnapUIAddress, - SnapUIAvatar, - SnapUIFooterButton, - FormTextField, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + CreateNamedSnapAccount, CreateSnapAccount, RemoveSnapAccount, - CreateNamedSnapAccount, - SnapAccountSuccessMessage, + SnapAccountCard, SnapAccountErrorMessage, - SnapAuthorshipHeader, SnapAccountRedirect, - SnapAccountCard, + SnapAccountSuccessMessage, + SnapAuthorshipHeader, ///: END:ONLY_INCLUDE_IF }; diff --git a/ui/components/app/name/__snapshots__/name.test.tsx.snap b/ui/components/app/name/__snapshots__/name.test.tsx.snap index 286429760c1e..883717fca1f4 100644 --- a/ui/components/app/name/__snapshots__/name.test.tsx.snap +++ b/ui/components/app/name/__snapshots__/name.test.tsx.snap @@ -79,7 +79,7 @@ exports[`Name renders address with long saved name 1`] = `

- Very long an... + Very long...

diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap index 3520a1b64a13..8ddce02439c9 100644 --- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap +++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap @@ -753,7 +753,7 @@ exports[`NameDetails renders with recognized name 1`] = `

- iZUMi Bond U... + iZUMi Bon...

diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx index 4871cc481f0c..1b5305c2b61f 100644 --- a/ui/components/app/name/name.tsx +++ b/ui/components/app/name/name.tsx @@ -106,7 +106,7 @@ const Name = memo( const MAX_PET_NAME_LENGTH = 12; const formattedName = shortenString(name || undefined, { truncatedCharLimit: MAX_PET_NAME_LENGTH, - truncatedStartChars: MAX_PET_NAME_LENGTH, + truncatedStartChars: MAX_PET_NAME_LENGTH - 3, truncatedEndChars: 0, skipCharacterInEnd: true, }); diff --git a/ui/components/multichain/account-overview/account-overview-layout.tsx b/ui/components/multichain/account-overview/account-overview-layout.tsx index 01396b77d2c1..69fc2297ffea 100644 --- a/ui/components/multichain/account-overview/account-overview-layout.tsx +++ b/ui/components/multichain/account-overview/account-overview-layout.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useContext, useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { isEqual } from 'lodash'; @@ -16,6 +16,12 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import useBridging from '../../../hooks/bridge/useBridging'; ///: END:ONLY_INCLUDE_IF +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventName, + MetaMetricsEventCategory, +} from '../../../../shared/constants/metametrics'; +import type { CarouselSlide } from '../../../../shared/constants/app-state'; import { AccountOverviewTabsProps, AccountOverviewTabs, @@ -42,6 +48,8 @@ export const AccountOverviewLayout = ({ const slides = useSelector(getSlides); const totalBalance = useSelector(getSelectedAccountCachedBalance); const isLoading = useSelector(getAppIsLoading); + const trackEvent = useContext(MetaMetricsContext); + const [hasRendered, setHasRendered] = useState(false); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); @@ -76,8 +84,8 @@ export const AccountOverviewLayout = ({ dispatch(updateSlides(defaultSlides)); }, [hasZeroBalance]); - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const handleCarouselClick = (id: string) => { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) if (id === 'bridge') { openBridgeExperience( 'Carousel', @@ -85,26 +93,57 @@ export const AccountOverviewLayout = ({ location.pathname.includes('asset') ? '&token=native' : '', ); } + ///: END:ONLY_INCLUDE_IF + + trackEvent({ + event: MetaMetricsEventName.BannerSelect, + category: MetaMetricsEventCategory.Banner, + properties: { + banner_name: id, + }, + }); }; - ///: END:ONLY_INCLUDE_IF - const handleRemoveSlide = (id: string) => { + const handleRemoveSlide = (isLastSlide: boolean, id: string) => { if (id === 'fund' && hasZeroBalance) { return; } + if (isLastSlide) { + trackEvent({ + event: MetaMetricsEventName.BannerCloseAll, + category: MetaMetricsEventCategory.Banner, + }); + } dispatch(removeSlide(id)); }; + const handleRenderSlides = useCallback( + (renderedSlides: CarouselSlide[]) => { + if (!hasRendered) { + renderedSlides.forEach((slide) => { + trackEvent({ + event: MetaMetricsEventName.BannerDisplay, + category: MetaMetricsEventCategory.Banner, + properties: { + banner_name: slide.id, + }, + }); + }); + setHasRendered(true); + } + }, + [hasRendered, trackEvent], + ); + return ( <>
{children}
diff --git a/ui/components/multichain/carousel/carousel.test.tsx b/ui/components/multichain/carousel/carousel.test.tsx index 71b9e4f4faa8..25fe8b562600 100644 --- a/ui/components/multichain/carousel/carousel.test.tsx +++ b/ui/components/multichain/carousel/carousel.test.tsx @@ -3,6 +3,40 @@ import { render, fireEvent } from '@testing-library/react'; import { Carousel } from './carousel'; import { MARGIN_VALUES, WIDTH_VALUES } from './constants'; +jest.mock('react-responsive-carousel', () => ({ + Carousel: ({ + children, + onChange, + }: { + children: React.ReactNode; + onChange?: (index: number) => void; + }) => ( +
+ {children} +
+
+
+ ), +})); + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), + useDispatch: () => jest.fn(), +})); + +jest.mock('reselect', () => ({ + createSelector: jest.fn(), + createDeepEqualSelector: jest.fn(), + createSelectorCreator: jest.fn(() => jest.fn()), + lruMemoize: jest.fn(), +})); + +jest.mock('../../../selectors/approvals', () => ({ + selectPendingApproval: jest.fn(), +})); + jest.mock('../../../hooks/useI18nContext', () => ({ useI18nContext: () => (key: string) => key, })); @@ -46,13 +80,24 @@ describe('Carousel', () => { expect(closeButtons).toHaveLength(2); fireEvent.click(closeButtons[0]); - expect(mockOnClose).toHaveBeenCalledWith('1'); + expect(mockOnClose).toHaveBeenCalledWith(false, '1'); const remainingSlides = mockSlides.filter((slide) => slide.id !== '1'); rerender(); - const updatedSlides = container.querySelectorAll('.mm-carousel-slide'); - expect(updatedSlides).toHaveLength(1); + const updatedCloseButtons = container.querySelectorAll( + '.mm-carousel-slide__close-button', + ); + expect(updatedCloseButtons).toHaveLength(1); + + fireEvent.click(updatedCloseButtons[0]); + expect(mockOnClose).toHaveBeenCalledWith(true, '2'); + + const finalSlides = remainingSlides.filter((slide) => slide.id !== '2'); + rerender(); + + const finalSlideElements = container.querySelectorAll('.mm-carousel-slide'); + expect(finalSlideElements).toHaveLength(0); }); it('should handle slide navigation', () => { @@ -65,7 +110,7 @@ describe('Carousel', () => { fireEvent.click(dots[1]); const slides = container.querySelectorAll('.mm-carousel-slide'); - expect(slides[1].parentElement).toHaveClass('selected'); + expect(slides[1].parentElement).toHaveClass('mock-carousel'); }); it('should return null when no slides are present', () => { diff --git a/ui/components/multichain/carousel/carousel.tsx b/ui/components/multichain/carousel/carousel.tsx index 3fbbe955a8eb..83423948c530 100644 --- a/ui/components/multichain/carousel/carousel.tsx +++ b/ui/components/multichain/carousel/carousel.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Carousel as ResponsiveCarousel } from 'react-responsive-carousel'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { Box, BoxProps, BannerBase } from '../../component-library'; @@ -24,6 +24,7 @@ export const Carousel = React.forwardRef( isLoading = false, onClose, onClick, + onRenderSlides, ...props }: CarouselProps, ref: React.Ref, @@ -44,6 +45,17 @@ export const Carousel = React.forwardRef( }) .slice(0, MAX_SLIDES); + useEffect(() => { + if ( + visibleSlides && + visibleSlides.length > 0 && + onRenderSlides && + !isLoading + ) { + onRenderSlides(visibleSlides); + } + }, [visibleSlides, onRenderSlides, isLoading]); + const handleClose = (e: React.MouseEvent, slideId: string) => { e.preventDefault(); e.stopPropagation(); @@ -65,7 +77,7 @@ export const Carousel = React.forwardRef( setSelectedIndex(newSelectedIndex); if (onClose) { - onClose(slideId); + onClose(visibleSlides.length === 1, slideId); } }; diff --git a/ui/components/multichain/carousel/carousel.types.ts b/ui/components/multichain/carousel/carousel.types.ts index a8aef8df4839..3a6289e76a4b 100644 --- a/ui/components/multichain/carousel/carousel.types.ts +++ b/ui/components/multichain/carousel/carousel.types.ts @@ -3,6 +3,7 @@ import { CarouselSlide } from '../../../../shared/constants/app-state'; export type CarouselProps = { slides: CarouselSlide[]; isLoading?: boolean; - onClose?: (id: string) => void; + onClose?: (isLastSlide: boolean, id: string) => void; onClick?: (id: string) => void; + onRenderSlides?: (slides: CarouselSlide[]) => void; }; diff --git a/ui/components/ui/origin-pill/origin-pill.test.tsx b/ui/components/ui/origin-pill/origin-pill.test.tsx new file mode 100644 index 000000000000..13034a6a83e1 --- /dev/null +++ b/ui/components/ui/origin-pill/origin-pill.test.tsx @@ -0,0 +1,27 @@ +import { screen } from '@testing-library/dom'; +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import OriginPill from './origin-pill'; + +describe('', () => { + it('renders correct elements', () => { + const defaultProps = { + origin: 'Test Origin', + dataTestId: 'test-data-test-id', + }; + const store = configureMockStore()(mockState); + + renderWithProvider(, store); + + expect(screen.getByTestId(defaultProps.dataTestId)).toBeDefined(); + expect( + screen.getByTestId(`${defaultProps.dataTestId}-avatar-favicon`), + ).toBeDefined(); + expect(screen.getByTestId(`${defaultProps.dataTestId}-text`)).toBeDefined(); + expect( + screen.getByTestId(`${defaultProps.dataTestId}-text`), + ).toHaveTextContent(defaultProps.origin); + }); +}); diff --git a/ui/components/ui/origin-pill/origin-pill.tsx b/ui/components/ui/origin-pill/origin-pill.tsx new file mode 100644 index 000000000000..6a0863f43a71 --- /dev/null +++ b/ui/components/ui/origin-pill/origin-pill.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { + AlignItems, + BorderColor, + BorderRadius, + BorderStyle, + Display, + JustifyContent, + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { getSubjectMetadata } from '../../../selectors'; +import { AvatarFavicon, Box, Text } from '../../component-library'; + +type OriginPillProps = { + origin: string; + dataTestId: string; +}; + +export default function OriginPill({ origin, dataTestId }: OriginPillProps) { + const subjectMetadata = useSelector(getSubjectMetadata); + + const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {}; + + return ( + + + + {origin} + + + ); +} diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 5597503206da..e2ea02bad39d 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -1,16 +1,12 @@ -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { Hex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; import { BridgeBackgroundAction, BridgeUserAction, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../app/scripts/controllers/bridge/types'; + QuoteRequest, +} from '../../../shared/types/bridge'; import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; -import { QuoteRequest } from '../../pages/bridge/types'; -import { MetaMaskReduxDispatch } from '../../store/store'; +import type { MetaMaskReduxDispatch } from '../../store/store'; import { bridgeSlice, setDestTokenExchangeRates, diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index d317d1b53bb8..30e2f0fe880a 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -7,9 +7,7 @@ import { setBackgroundConnection } from '../../store/background-connection'; import { BridgeBackgroundAction, BridgeUserAction, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../app/scripts/controllers/bridge/types'; +} from '../../../shared/types/bridge'; import * as util from '../../helpers/utils/util'; import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../shared/constants/bridge'; import bridgeReducer from './bridge'; diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 82bbba964868..14786b51d4dd 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -1,12 +1,11 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { Hex } from '@metamask/utils'; -import { swapsSlice } from '../swaps/swaps'; import { - BridgeToken, - QuoteMetadata, - QuoteResponse, + type BridgeToken, + type QuoteMetadata, + type QuoteResponse, SortOrder, -} from '../../pages/bridge/types'; +} from '../../../shared/types/bridge'; import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../shared/constants/bridge'; import { getTokenExchangeRate } from './utils'; @@ -57,7 +56,6 @@ const bridgeSlice = createSlice({ name: 'bridge', initialState: { ...initialState }, reducers: { - ...swapsSlice.reducer, setToChainId: (state, action) => { state.toChainId = action.payload; }, diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 0e948ee18784..f44b5d8dacea 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -11,10 +11,10 @@ import { mockNetworkState } from '../../../test/stub/networks'; import mockErc20Erc20Quotes from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json'; import { - QuoteMetadata, - QuoteResponse, + type QuoteMetadata, + type QuoteResponse, SortOrder, -} from '../../pages/bridge/types'; +} from '../../../shared/types/bridge'; import { getAllBridgeableNetworks, getBridgeQuotes, @@ -1212,7 +1212,7 @@ describe('Bridge selectors', () => { ).toStrictEqual(false); }); - it('should return isEstimatedReturnLow=true return value is 20% less than sent funds', () => { + it('should return isEstimatedReturnLow=true return value is 50% less than sent funds', () => { const state = createBridgeMockStore({ featureFlagOverrides: { extensionConfig: { @@ -1228,7 +1228,7 @@ describe('Bridge selectors', () => { toToken: { address: zeroAddress(), symbol: 'TEST' }, fromTokenInputValue: '1', fromTokenExchangeRate: 2524.25, - toTokenExchangeRate: 0.798781, + toTokenExchangeRate: 0.61, }, bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, @@ -1264,11 +1264,11 @@ describe('Bridge selectors', () => { expect( getBridgeQuotes(state as never).activeQuote?.adjustedReturn .valueInCurrency, - ).toStrictEqual(new BigNumber('16.99676538473491988')); + ).toStrictEqual(new BigNumber('12.38316502627291988')); expect(result.isEstimatedReturnLow).toStrictEqual(true); }); - it('should return isEstimatedReturnLow=false when return value is more than 80% of sent funds', () => { + it('should return isEstimatedReturnLow=false when return value is more than 50% of sent funds', () => { const state = createBridgeMockStore({ featureFlagOverrides: { extensionConfig: { @@ -1283,7 +1283,8 @@ describe('Bridge selectors', () => { fromToken: { address: zeroAddress(), symbol: 'ETH' }, toToken: { address: zeroAddress(), symbol: 'TEST' }, fromTokenExchangeRate: 2524.25, - toTokenExchangeRate: 0.998781, + toTokenExchangeRate: 0.63, + fromTokenInputValue: 1, }, bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, @@ -1320,7 +1321,7 @@ describe('Bridge selectors', () => { expect( getBridgeQuotes(state as never).activeQuote?.adjustedReturn .valueInCurrency, - ).toStrictEqual(new BigNumber('21.88454578473491988')); + ).toStrictEqual(new BigNumber('12.87194306627291988')); expect(result.isEstimatedReturnLow).toStrictEqual(false); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 9241af57db6d..f86745e69ae3 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -1,11 +1,11 @@ -import { +import type { AddNetworkFields, NetworkConfiguration, NetworkState, } from '@metamask/network-controller'; import { orderBy, uniqBy } from 'lodash'; import { createSelector } from 'reselect'; -import { GasFeeEstimates } from '@metamask/gas-fee-controller'; +import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; import { BigNumber } from 'bignumber.js'; import { calcTokenAmount } from '@metamask/notification-services-controller/push-services'; import { @@ -20,12 +20,7 @@ import { BRIDGE_PREFERRED_GAS_ESTIMATE, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, } from '../../../shared/constants/bridge'; -import { - BridgeControllerState, - BridgeFeatureFlagsKey, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../app/scripts/controllers/bridge/types'; +import type { BridgeControllerState } from '../../../shared/types/bridge'; import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps'; import { @@ -33,16 +28,15 @@ import { getNetworkConfigurationsByChainId, } from '../../../shared/modules/selectors/networks'; import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { - L1GasFees, - BridgeToken, - QuoteMetadata, - QuoteResponse, + type L1GasFees, + type BridgeToken, + type QuoteMetadata, + type QuoteResponse, SortOrder, -} from '../../pages/bridge/types'; + BridgeFeatureFlagsKey, + RequestStatus, +} from '../../../shared/types/bridge'; import { calcAdjustedReturn, calcCost, @@ -64,7 +58,7 @@ import { exchangeRateFromMarketData, tokenPriceInNativeAsset, } from './utils'; -import { BridgeState } from './bridge'; +import type { BridgeState } from './bridge'; type BridgeAppState = { metamask: { bridgeState: BridgeControllerState } & NetworkState & { diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts index a563b5af2c73..050829d3ca65 100644 --- a/ui/ducks/bridge/utils.ts +++ b/ui/ducks/bridge/utils.ts @@ -1,14 +1,14 @@ -import { Hex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { getAddress } from 'ethers/lib/utils'; -import { ContractMarketData } from '@metamask/assets-controllers'; +import type { ContractMarketData } from '@metamask/assets-controllers'; import { AddNetworkFields, NetworkConfiguration, } from '@metamask/network-controller'; import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils'; import { Numeric } from '../../../shared/modules/Numeric'; -import { TxData } from '../../pages/bridge/types'; +import type { TxData } from '../../../shared/types/bridge'; import { getTransaction1559GasFeeEstimates } from '../../pages/swaps/swaps.util'; import { fetchTokenExchangeRates as fetchTokenExchangeRatesUtil } from '../../helpers/utils/util'; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index f3453b77e7d9..c9c57057f2f9 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -373,7 +373,7 @@ export function isEIP1559Network(state, networkClientId) { return ( state.metamask.networksMetadata?.[ networkClientId ?? selectedNetworkClientId - ].EIPS[1559] === true + ]?.EIPS[1559] === true ); } diff --git a/ui/hooks/bridge/useBridgeTokens.ts b/ui/hooks/bridge/useBridgeTokens.ts index acddb7ec2fb0..354b90bc4a09 100644 --- a/ui/hooks/bridge/useBridgeTokens.ts +++ b/ui/hooks/bridge/useBridgeTokens.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { getAllBridgeableNetworks } from '../../ducks/bridge/selectors'; -import { fetchBridgeTokens } from '../../pages/bridge/bridge.util'; +import { fetchBridgeTokens } from '../../../shared/modules/bridge-utils/bridge.util'; // This hook is used to fetch the bridge tokens for all bridgeable networks export const useBridgeTokens = () => { diff --git a/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts b/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts index ad4b3698fe84..bd3a14b4ce4b 100644 --- a/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts +++ b/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts @@ -5,7 +5,7 @@ import { MetaMetricsEventName, MetaMetricsSwapsEventSource, } from '../../../shared/constants/metametrics'; -import { SortOrder } from '../../pages/bridge/types'; +import { SortOrder } from '../../../shared/types/bridge'; import { RequestParams, RequestMetadata, diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts index 25f0d0936791..f255a3a10b9b 100644 --- a/ui/hooks/bridge/useLatestBalance.test.ts +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -1,8 +1,8 @@ import { BigNumber } from 'bignumber.js'; +import { zeroAddress } from 'ethereumjs-util'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; -import { zeroAddress } from '../../__mocks__/ethereumjs-util'; import { createTestProviderTools } from '../../../test/stub/provider'; import * as tokenutil from '../../../shared/lib/token-util'; import useLatestBalance from './useLatestBalance'; diff --git a/ui/hooks/metamask-notifications/useSwitchNotifications.test.tsx b/ui/hooks/metamask-notifications/useSwitchNotifications.test.tsx index b67c2ea80cef..b228698f391e 100644 --- a/ui/hooks/metamask-notifications/useSwitchNotifications.test.tsx +++ b/ui/hooks/metamask-notifications/useSwitchNotifications.test.tsx @@ -1,125 +1,183 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { renderHook, act } from '@testing-library/react-hooks'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import type { Store } from 'redux'; -import * as actions from '../../store/actions'; -import { MetamaskNotificationsProvider } from '../../contexts/metamask-notifications/metamask-notifications'; +import { waitFor } from '@testing-library/react'; +import * as ActionsModule from '../../store/actions'; +import * as NotificationSelectorsModule from '../../selectors/metamask-notifications/metamask-notifications'; +import { renderHookWithProviderTyped } from '../../../test/lib/render-helpers'; import { useSwitchFeatureAnnouncementsChange, - useSwitchAccountNotifications, useSwitchAccountNotificationsChange, + useAccountSettingsProps, } from './useSwitchNotifications'; -const middlewares = [thunk]; -const mockStore = configureStore(middlewares); - -jest.mock('../../store/actions', () => ({ - setFeatureAnnouncementsEnabled: jest.fn(), - checkAccountsPresence: jest.fn(), - updateOnChainTriggersByAccount: jest.fn(), - deleteOnChainTriggersByAccount: jest.fn(), - showLoadingIndication: jest.fn(), - hideLoadingIndication: jest.fn(), - fetchAndUpdateMetamaskNotifications: jest.fn(), -})); - -describe('useSwitchNotifications', () => { - let store: Store; - +describe('useSwitchFeatureAnnouncementsChange() tests', () => { beforeEach(() => { - store = mockStore({ - metamask: { - isFeatureAnnouncementsEnabled: false, - internalAccounts: { - accounts: { - '0x123': { - address: '0x123', - id: 'account1', - metadata: {}, - options: {}, - methods: [], - type: 'eip155:eoa', - }, - }, - }, - }, - }); - - store.dispatch = jest.fn().mockImplementation((action) => { - if (typeof action === 'function') { - return action(store.dispatch, store.getState); - } - return Promise.resolve(); - }); + jest.restoreAllMocks(); + }); - jest.clearAllMocks(); + const arrangeMocks = () => { + const mockSetFeatureAnnouncementsEnabled = jest.spyOn( + ActionsModule, + 'setFeatureAnnouncementsEnabled', + ); + return { + mockSetFeatureAnnouncementsEnabled, + }; + }; + + it('should update feature announcement when callback invoked', async () => { + const mocks = arrangeMocks(); + const hook = renderHookWithProviderTyped( + () => useSwitchFeatureAnnouncementsChange(), + {}, + ); + + await hook.result.current.onChange(true); + expect(mocks.mockSetFeatureAnnouncementsEnabled).toHaveBeenCalled(); }); - it('should toggle feature announcements', async () => { - const { result } = renderHook(() => useSwitchFeatureAnnouncementsChange(), { - wrapper: ({ children }) => ( - - - {children} - - - ), + it('should update error state when callback fails', async () => { + const mocks = arrangeMocks(); + mocks.mockSetFeatureAnnouncementsEnabled.mockImplementation(() => { + throw new Error('Mock Fail'); }); + const hook = renderHookWithProviderTyped( + () => useSwitchFeatureAnnouncementsChange(), + {}, + ); - await act(async () => { - await result.current.onChange(true); - }); + await hook.result.current.onChange(true); + expect(hook.result.current.error).toBeDefined(); + }); +}); - expect(actions.setFeatureAnnouncementsEnabled).toHaveBeenCalledWith(true); +describe('useSwitchAccountNotificationsChange() tests', () => { + const arrangeMocks = () => { + const mockUpdateOnChainTriggersByAccount = jest.spyOn( + ActionsModule, + 'updateOnChainTriggersByAccount', + ); + const mockDeleteOnChainTriggersByAccount = jest.spyOn( + ActionsModule, + 'deleteOnChainTriggersByAccount', + ); + + return { + mockUpdateOnChainTriggersByAccount, + mockDeleteOnChainTriggersByAccount, + }; + }; + + it('should invoke update notification triggers when an address is enabled', async () => { + const mocks = arrangeMocks(); + const hook = renderHookWithProviderTyped( + () => useSwitchAccountNotificationsChange(), + {}, + ); + + await hook.result.current.onChange(['0x1'], true); + expect(mocks.mockUpdateOnChainTriggersByAccount).toHaveBeenCalledWith([ + '0x1', + ]); + expect(mocks.mockDeleteOnChainTriggersByAccount).not.toHaveBeenCalled(); }); - it('should check account presence', async () => { - const { result } = renderHook(() => useSwitchAccountNotifications(), { - wrapper: ({ children }) => ( - - - {children} - - - ), - }); + it('should invoke delete notification triggers when an address is disabled', async () => { + const mocks = arrangeMocks(); + const hook = renderHookWithProviderTyped( + () => useSwitchAccountNotificationsChange(), + {}, + ); + + await hook.result.current.onChange(['0x1'], false); + expect(mocks.mockUpdateOnChainTriggersByAccount).not.toHaveBeenCalled(); + expect(mocks.mockDeleteOnChainTriggersByAccount).toHaveBeenCalledWith([ + '0x1', + ]); + }); - await act(async () => { - await result.current.switchAccountNotifications(['0x123']); + it('should return an error value if it fails to update or delete triggers', async () => { + const mocks = arrangeMocks(); + mocks.mockUpdateOnChainTriggersByAccount.mockImplementation(() => { + throw new Error('Mock Error'); + }); + mocks.mockDeleteOnChainTriggersByAccount.mockImplementation(() => { + throw new Error('Mock Error'); }); - expect(actions.checkAccountsPresence).toHaveBeenCalledWith(['0x123']); - }); + const act = async (testEnableOrDisable: boolean) => { + const hook = renderHookWithProviderTyped( + () => useSwitchAccountNotificationsChange(), + {}, + ); + await hook.result.current.onChange(['0x1'], testEnableOrDisable); + return hook.result.current.error; + }; - it('should handle account notification changes', async () => { - const { result } = renderHook(() => useSwitchAccountNotificationsChange(), { - wrapper: ({ children }) => ( - - - {children} - - - ), - }); + const enableError = await act(true); + expect(enableError).toBeDefined(); + + const disableError = await act(false); + expect(disableError).toBeDefined(); + }); +}); - // Test enabling notifications - await act(async () => { - await result.current.onChange(['0x123'], true); +describe('useAccountSettingsProps() tests', () => { + const arrangeMocks = () => { + const mockCheckAccountsPresence = jest.spyOn( + ActionsModule, + 'checkAccountsPresence', + ); + const mockGetIsUpdatingMetamaskNotificationsAccount = jest + .spyOn( + NotificationSelectorsModule, + 'getIsUpdatingMetamaskNotificationsAccount', + ) + .mockReturnValue([]); + const mockSelectIsMetamaskNotificationsEnabled = jest + .spyOn( + NotificationSelectorsModule, + 'selectIsMetamaskNotificationsEnabled', + ) + .mockReturnValue(true); + + return { + mockCheckAccountsPresence, + mockGetIsUpdatingMetamaskNotificationsAccount, + mockSelectIsMetamaskNotificationsEnabled, + }; + }; + + it('Should invoke effect when notifications are enabled', async () => { + const mocks = arrangeMocks(); + renderHookWithProviderTyped(() => useAccountSettingsProps(['0x1']), {}); + + await waitFor(() => { + expect(mocks.mockCheckAccountsPresence).toHaveBeenCalled(); }); + }); - expect(actions.updateOnChainTriggersByAccount).toHaveBeenCalledWith([ - '0x123', - ]); + it('Should not invoke effect when notifications are disabled', async () => { + const mocks = arrangeMocks(); + mocks.mockSelectIsMetamaskNotificationsEnabled.mockReturnValue(false); + renderHookWithProviderTyped(() => useAccountSettingsProps(['0x1']), {}); - // Test disabling notifications - await act(async () => { - await result.current.onChange(['0x123'], false); + await waitFor(() => { + expect(mocks.mockCheckAccountsPresence).not.toHaveBeenCalled(); }); + }); - expect(actions.deleteOnChainTriggersByAccount).toHaveBeenCalledWith([ - '0x123', - ]); + it('Should be able to invoke refetch accounts function', async () => { + const mocks = arrangeMocks(); + const hook = renderHookWithProviderTyped( + () => useAccountSettingsProps(['0x1']), + {}, + ); + + await hook.result.current.update(['0x1', '0x2']); + await waitFor(() => { + expect(mocks.mockCheckAccountsPresence).toHaveBeenCalledWith([ + '0x1', + '0x2', + ]); + }); }); }); diff --git a/ui/hooks/metamask-notifications/useSwitchNotifications.ts b/ui/hooks/metamask-notifications/useSwitchNotifications.ts index 53e121473bac..08a03c33e4f1 100644 --- a/ui/hooks/metamask-notifications/useSwitchNotifications.ts +++ b/ui/hooks/metamask-notifications/useSwitchNotifications.ts @@ -8,7 +8,10 @@ import { updateOnChainTriggersByAccount, hideLoadingIndication, } from '../../store/actions'; -import { getIsUpdatingMetamaskNotificationsAccount } from '../../selectors/metamask-notifications/metamask-notifications'; +import { + getIsUpdatingMetamaskNotificationsAccount, + selectIsMetamaskNotificationsEnabled, +} from '../../selectors/metamask-notifications/metamask-notifications'; export function useSwitchFeatureAnnouncementsChange(): { onChange: (state: boolean) => Promise; @@ -28,7 +31,6 @@ export function useSwitchFeatureAnnouncementsChange(): { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e ?? ''); setError(errorMessage); - throw e; } }, [dispatch], @@ -42,44 +44,6 @@ export function useSwitchFeatureAnnouncementsChange(): { export type UseSwitchAccountNotificationsData = { [address: string]: boolean }; -export function useSwitchAccountNotifications(): { - switchAccountNotifications: ( - accounts: string[], - ) => Promise; - isLoading: boolean; - error: string | null; -} { - const dispatch = useDispatch(); - - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const switchAccountNotifications = useCallback( - async ( - accounts: string[], - ): Promise => { - setIsLoading(true); - setError(null); - - try { - const data = await dispatch(checkAccountsPresence(accounts)); - return data as unknown as UseSwitchAccountNotificationsData; - } catch (e) { - const errorMessage = - e instanceof Error ? e.message : JSON.stringify(e ?? ''); - setError(errorMessage); - log.error(errorMessage); - throw e; - } finally { - setIsLoading(false); - } - }, - [dispatch], - ); - - return { switchAccountNotifications, isLoading, error }; -} - export function useSwitchAccountNotificationsChange(): { onChange: (addresses: string[], state: boolean) => Promise; error: string | null; @@ -103,7 +67,6 @@ export function useSwitchAccountNotificationsChange(): { e instanceof Error ? e.message : JSON.stringify(e ?? ''); log.error(errorMessage); setError(errorMessage); - throw e; } dispatch(hideLoadingIndication()); }, @@ -146,6 +109,7 @@ export function useAccountSettingsProps(accounts: string[]) { const accountsBeingUpdated = useSelector( getIsUpdatingMetamaskNotificationsAccount, ); + const isEnabled = useSelector(selectIsMetamaskNotificationsEnabled); const fetchAccountSettings = useRefetchAccountSettings(); const [data, setData] = useState({}); const [loading, setLoading] = useState(false); @@ -169,15 +133,12 @@ export function useAccountSettingsProps(accounts: string[]) { // Effect - async get if accounts are enabled/disabled useEffect(() => { - try { - const memoAccounts: string[] = JSON.parse(jsonAccounts); - update(memoAccounts); - } catch { - setError('Failed to get account settings'); - } finally { - setLoading(false); + if (!isEnabled) { + return; } - }, [jsonAccounts, fetchAccountSettings]); + const memoAccounts: string[] = JSON.parse(jsonAccounts); + update(memoAccounts); + }, [jsonAccounts, fetchAccountSettings, isEnabled]); return { data, diff --git a/ui/pages/bridge/hooks/useAddToken.ts b/ui/pages/bridge/hooks/useAddToken.ts index 1f2dc117513b..f1a148ce7732 100644 --- a/ui/pages/bridge/hooks/useAddToken.ts +++ b/ui/pages/bridge/hooks/useAddToken.ts @@ -1,6 +1,6 @@ import { useDispatch, useSelector } from 'react-redux'; -import { NetworkConfiguration } from '@metamask/network-controller'; -import { QuoteResponse } from '../types'; +import type { NetworkConfiguration } from '@metamask/network-controller'; +import type { QuoteResponse } from '../../../../shared/types/bridge'; import { FEATURED_RPCS } from '../../../../shared/constants/network'; import { addToken, addNetwork } from '../../../store/actions'; import { diff --git a/ui/pages/bridge/hooks/useHandleApprovalTx.ts b/ui/pages/bridge/hooks/useHandleApprovalTx.ts index 23f4b19cf2b8..7e469bc78ead 100644 --- a/ui/pages/bridge/hooks/useHandleApprovalTx.ts +++ b/ui/pages/bridge/hooks/useHandleApprovalTx.ts @@ -1,8 +1,15 @@ import { TransactionType } from '@metamask/transaction-controller'; -import { Hex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; -import { TxData, QuoteResponse, FeeType } from '../types'; -import { isEthUsdt, getEthUsdtResetData } from '../bridge.util'; +import { + type TxData, + type QuoteResponse, + FeeType, +} from '../../../../shared/types/bridge'; +import { + isEthUsdt, + getEthUsdtResetData, +} from '../../../../shared/modules/bridge-utils/bridge.util'; import { ETH_USDT_ADDRESS } from '../../../../shared/constants/bridge'; import { getBridgeERC20Allowance } from '../../../ducks/bridge/actions'; import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; diff --git a/ui/pages/bridge/hooks/useHandleBridgeTx.ts b/ui/pages/bridge/hooks/useHandleBridgeTx.ts index feb7400acc71..5d7e1a1b527c 100644 --- a/ui/pages/bridge/hooks/useHandleBridgeTx.ts +++ b/ui/pages/bridge/hooks/useHandleBridgeTx.ts @@ -1,7 +1,7 @@ import { BigNumber } from 'bignumber.js'; import { TransactionType } from '@metamask/transaction-controller'; import { Numeric } from '../../../../shared/modules/Numeric'; -import { FeeType, QuoteResponse } from '../types'; +import { FeeType, type QuoteResponse } from '../../../../shared/types/bridge'; import useHandleTx from './useHandleTx'; export default function useHandleBridgeTx() { diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts index c3a3ede01002..59b6c7ee9c89 100644 --- a/ui/pages/bridge/hooks/useHandleTx.ts +++ b/ui/pages/bridge/hooks/useHandleTx.ts @@ -15,7 +15,7 @@ import { } from '../../../ducks/bridge/utils'; import { getGasFeeEstimates } from '../../../ducks/metamask/metamask'; import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; -import { ChainId } from '../types'; +import type { ChainId } from '../../../../shared/types/bridge'; import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { getIsSmartTransaction } from '../../../../shared/modules/selectors'; diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index 6f8ec559a6a5..e34c4e9400c6 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -3,7 +3,10 @@ import { zeroAddress } from 'ethereumjs-util'; import { useHistory } from 'react-router-dom'; import { TransactionMeta } from '@metamask/transaction-controller'; import { createProjectLogger, Hex } from '@metamask/utils'; -import { QuoteMetadata, QuoteResponse } from '../types'; +import type { + QuoteMetadata, + QuoteResponse, +} from '../../../../shared/types/bridge'; import { AWAITING_SIGNATURES_ROUTE, CROSS_CHAIN_SWAP_ROUTE, diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx index 5dc368043d3a..92d6591a2808 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -4,9 +4,7 @@ import configureStore from '../../../store/store'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { RequestStatus } from '../../../../app/scripts/controllers/bridge/constants'; +import { RequestStatus } from '../../../../shared/types/bridge'; import { BridgeCTAButton } from './bridge-cta-button'; describe('BridgeCTAButton', () => { diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx index 1734624b7dcc..ac545fb834dd 100644 --- a/ui/pages/bridge/prepare/bridge-input-group.tsx +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { BigNumber } from 'bignumber.js'; import { getAddress } from 'ethers/lib/utils'; @@ -7,7 +7,6 @@ import { TextField, TextFieldType, ButtonLink, - PopoverPosition, Button, ButtonSize, } from '../../../components/component-library'; @@ -17,7 +16,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getLocale } from '../../../selectors'; import { getCurrentCurrency } from '../../../ducks/metamask/metamask'; import { formatCurrencyAmount, formatTokenAmount } from '../utils/quote'; -import { Column, Row, Tooltip } from '../layout'; +import { Column, Row } from '../layout'; import { Display, FontWeight, @@ -27,14 +26,13 @@ import { TextColor, } from '../../../helpers/constants/design-system'; import { AssetType } from '../../../../shared/constants/transaction'; -import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge'; import useLatestBalance from '../../../hooks/bridge/useLatestBalance'; import { getBridgeQuotes, getValidationErrors, } from '../../../ducks/bridge/selectors'; import { shortenString } from '../../../helpers/utils/util'; -import { BridgeToken } from '../types'; +import type { BridgeToken } from '../../../../shared/types/bridge'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { MINUTE } from '../../../../shared/constants/time'; import { BridgeAssetPickerButton } from './components/bridge-asset-picker-button'; @@ -87,8 +85,6 @@ export const BridgeInputGroup = ({ const inputRef = useRef(null); - const [isLowReturnTooltipOpen, setIsLowReturnTooltipOpen] = useState(true); - useEffect(() => { if (inputRef.current) { inputRef.current.value = amountFieldProps?.value?.toString() ?? ''; @@ -189,23 +185,6 @@ export const BridgeInputGroup = ({ - {isAmountReadOnly && - isEstimatedReturnLow && - isLowReturnTooltipOpen && ( - setIsLowReturnTooltipOpen(false)} - triggerElement={} - flip={false} - offset={[0, 80]} - > - {t('lowEstimatedReturnTooltipMessage', [ - BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE * 100, - ])} - - )} ( ); const mockFeatureFlags = { - srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], - destNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], extensionSupport: true, extensionConfig: { refreshRate: 30000, maxRefreshCount: 5, + support: true, + chains: { + '0x1': { isActiveSrc: true, isActiveDest: true }, + '0xa': { isActiveSrc: true, isActiveDest: true }, + }, }, }; const mockBridgeSlice = { diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 8c36e5571988..4385a4176e77 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -63,18 +63,15 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps'; import { useTokensWithFiltering } from '../../../hooks/bridge/useTokensWithFiltering'; import { setActiveNetwork } from '../../../store/actions'; -import { - decimalToHex, - hexToDecimal, -} from '../../../../shared/modules/conversion.utils'; -import { QuoteRequest } from '../types'; +import { hexToDecimal, decimalToHex } from '../../../../shared/modules/conversion.utils'; +import type { QuoteRequest } from '../../../../shared/types/bridge'; import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; import { BridgeQuoteCard } from '../quotes/bridge-quote-card'; import { formatTokenAmount, isQuoteExpired as isQuoteExpiredUtil, - isValidQuoteRequest, } from '../utils/quote'; +import { isValidQuoteRequest } from '../../../../shared/modules/bridge-utils/quote'; import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { CrossChainSwapsEventProperties, @@ -94,6 +91,7 @@ import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens'; import { getCurrentKeyring, getLocale } from '../../../selectors'; import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { SECOND } from '../../../../shared/constants/time'; +import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge'; import { BridgeInputGroup } from './bridge-input-group'; import { BridgeCTAButton } from './bridge-cta-button'; @@ -154,10 +152,12 @@ const PrepareBridgePage = () => { const ticker = useSelector(getNativeCurrency); const { + isEstimatedReturnLow, isNoQuotesAvailable, isInsufficientGasForQuote, isInsufficientBalance, } = useSelector(getValidationErrors); + const { quotesRefreshCount } = useSelector(getBridgeQuotes); const { openBuyCryptoInPdapp } = useRamps(); const { balanceAmount: nativeAssetBalance } = useLatestBalance( @@ -193,6 +193,10 @@ const PrepareBridgePage = () => { const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); + // Resets the banner visibility when the estimated return is low + const [isLowReturnBannerOpen, setIsLowReturnBannerOpen] = useState(true); + useEffect(() => setIsLowReturnBannerOpen(true), [quotesRefreshCount]); + // Background updates are debounced when the switch button is clicked // To prevent putting the frontend in an unexpected state, prevent the user // from switching tokens within the debounce period @@ -231,16 +235,27 @@ const PrepareBridgePage = () => { } }, []); - const scrollRef = useRef(null); - + // Scroll to bottom of the page when banners are shown + const insufficientBalanceBannerRef = useRef(null); + const isEstimatedReturnLowRef = useRef(null); useEffect(() => { if (isInsufficientGasForQuote(nativeAssetBalance)) { - scrollRef.current?.scrollIntoView({ + insufficientBalanceBannerRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + if (isEstimatedReturnLow) { + isEstimatedReturnLowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', }); } - }, [isInsufficientGasForQuote(nativeAssetBalance)]); + }, [ + isEstimatedReturnLow, + isInsufficientGasForQuote(nativeAssetBalance), + isLowReturnBannerOpen, + ]); const quoteParams = useMemo( () => ({ @@ -360,8 +375,6 @@ const PrepareBridgePage = () => { input: 'token_source', value: token.address, }); - dispatch(setFromToken(token)); - dispatch(setFromTokenInputValue(null)); }} networkProps={{ network: fromChain, @@ -625,12 +638,26 @@ const PrepareBridgePage = () => { textAlign={TextAlign.Left} /> )} + {isEstimatedReturnLow && isLowReturnBannerOpen && ( + setIsLowReturnBannerOpen(false)} + /> + )} {!isLoading && activeQuote && !isInsufficientBalance(srcTokenBalance) && isInsufficientGasForQuote(nativeAssetBalance) && (

Network fees

@@ -106,19 +107,10 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = ` class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center" >

- $2.52 -

-

- - -

-

- $2.52 + $2.52 - $2.52

Network fees

@@ -270,19 +263,10 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center" >

- $2.52 -

-

- - -

-

- $2.52 + $2.52 - $2.52