diff --git a/.circleci/config.yml b/.circleci/config.yml index be6f2b4e772f..531912bff9ff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,6 +105,9 @@ workflows: - prep-deps - check-pr-tag - prep-deps + - get-changed-files-with-git-diff: + requires: + - prep-deps - test-deps-audit: requires: - prep-deps @@ -187,41 +190,51 @@ workflows: - test-e2e-chrome: requires: - prep-build-test + - get-changed-files-with-git-diff - test-e2e-chrome-confirmation-redesign: requires: - prep-build-confirmation-redesign-test + - get-changed-files-with-git-diff - test-e2e-firefox: requires: - prep-build-test-mv2 + - get-changed-files-with-git-diff - test-e2e-firefox-confirmation-redesign: <<: *develop_master_rc_only requires: - prep-build-confirmation-redesign-test-mv2 + - get-changed-files-with-git-diff - test-e2e-chrome-rpc: requires: - prep-build-test + - get-changed-files-with-git-diff - test-api-specs: requires: - prep-build-test - test-e2e-chrome-multiple-providers: requires: - prep-build-test + - get-changed-files-with-git-diff - test-e2e-chrome-flask: requires: - prep-build-test-flask + - get-changed-files-with-git-diff - test-e2e-firefox-flask: <<: *develop_master_rc_only requires: - prep-build-test-flask-mv2 + - get-changed-files-with-git-diff - test-e2e-chrome-mmi: requires: - prep-build-test-mmi + - get-changed-files-with-git-diff - test-e2e-mmi-playwright - OPTIONAL: requires: - prep-build-test-mmi-playwright - test-e2e-chrome-rpc-mmi: requires: - prep-build-test-mmi + - get-changed-files-with-git-diff - test-e2e-chrome-vault-decryption: filters: branches: @@ -230,6 +243,7 @@ workflows: - /^Version-v(\d+)[.](\d+)[.](\d+)/ requires: - prep-build + - get-changed-files-with-git-diff - test-unit-global: requires: - prep-deps @@ -459,6 +473,23 @@ jobs: - node_modules - build-artifacts + # This job is used for the e2e quality gate. + # It must be run before any job which uses the run-all.js script. + get-changed-files-with-git-diff: + executor: node-browsers-small + steps: + - run: *shallow-git-clone + - run: sudo corepack enable + - attach_workspace: + at: . + - run: + name: Get changed files with git diff + command: npx tsx .circleci/scripts/git-diff-develop.ts + - persist_to_workspace: + root: . + paths: + - changed-files + validate-lavamoat-allow-scripts: executor: node-browsers-small steps: @@ -1121,7 +1152,7 @@ jobs: fi no_output_timeout: 5m environment: - ENABLE_CONFIRMATION_REDESIGN: "true" + ENABLE_CONFIRMATION_REDESIGN: 'true' - store_artifacts: path: test-artifacts destination: test-artifacts @@ -1412,7 +1443,7 @@ jobs: fi no_output_timeout: 5m environment: - ENABLE_CONFIRMATION_REDESIGN: "true" + ENABLE_CONFIRMATION_REDESIGN: 'true' - store_artifacts: path: test-artifacts destination: test-artifacts diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts new file mode 100644 index 000000000000..8b5680b17d3f --- /dev/null +++ b/.circleci/scripts/git-diff-develop.ts @@ -0,0 +1,99 @@ +import { hasProperty } from '@metamask/utils'; +import { exec as execCallback } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; + +const exec = promisify(execCallback); + +/** + * Fetches the git repository with a specified depth. + * + * @param depth - The depth to use for the fetch command. + * @returns True if the fetch is successful, otherwise false. + */ +async function fetchWithDepth(depth: number): Promise { + try { + await exec(`git fetch --depth ${depth} origin develop`); + await exec(`git fetch --depth ${depth} origin ${process.env.CIRCLE_BRANCH}`); + return true; + } catch (error: unknown) { + console.error(`Failed to fetch with depth ${depth}:`, error); + return false; + } +} + +/** + * Attempts to fetch the necessary commits until the merge base is found. + * It tries different fetch depths and performs a full fetch if needed. + * + * @throws If an unexpected error occurs during the execution of git commands. + */ +async function fetchUntilMergeBaseFound() { + const depths = [1, 10, 100]; + for (const depth of depths) { + console.log(`Attempting git diff with depth ${depth}...`); + await fetchWithDepth(depth); + + try { + await exec(`git merge-base origin/HEAD HEAD`); + return; + } catch (error: unknown) { + if ( + error instanceof Error && + hasProperty(error, 'code') && + error.code === 1 + ) { + console.error(`Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`); + } else { + throw error; + } + } + } + await exec(`git fetch --unshallow origin develop`); +} + +/** + * Performs a git diff command to get the list of files changed between the current branch and the origin. + * It first ensures that the necessary commits are fetched until the merge base is found. + * + * @returns The output of the git diff command, listing the changed files. + * @throws If unable to get the diff after fetching the merge base or if an unexpected error occurs. + */ +async function gitDiff(): Promise { + await fetchUntilMergeBaseFound(); + const { stdout: diffResult } = await exec(`git diff --name-only origin/HEAD...${process.env.CIRCLE_BRANCH}`); + if (!diffResult) { + throw new Error('Unable to get diff after full checkout.'); + } + return diffResult; +} + +/** + * Stores the output of git diff to a file. + * + * @returns Returns a promise that resolves when the git diff output is successfully stored. + */ +async function storeGitDiffOutput() { + try { + console.log("Attempting to get git diff..."); + const diffOutput = await gitDiff(); + console.log(diffOutput); + + // Create the directory + const outputDir = 'changed-files'; + fs.mkdirSync(outputDir, { recursive: true }); + + // Store the output of git diff + const outputPath = path.resolve(outputDir, 'changed-files.txt'); + fs.writeFileSync(outputPath, diffOutput); + + console.log(`Git diff results saved to ${outputPath}`); + process.exit(0); + } catch (error: any) { + console.error('An error occurred:', error.message); + process.exit(1); + } +} + +storeGitDiffOutput(); diff --git a/.eslintrc.js b/.eslintrc.js index 9f7fed5928ed..0aea1e739e3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -281,6 +281,7 @@ module.exports = { 'app/scripts/controllers/mmi-controller.test.ts', 'app/scripts/metamask-controller.actions.test.js', 'app/scripts/detect-multiple-instances.test.js', + 'app/scripts/controllers/bridge.test.ts', 'app/scripts/controllers/swaps.test.js', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 61d549728d99..f5e1a0552dd1 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -1,18 +1,11 @@ name: Sonar on: - workflow_call: - secrets: - SONAR_TOKEN: - required: true -# pull_request: -# branches: -# - develop -# types: -# - opened -# - reopened -# - synchronize -# - labeled -# - unlabeled + push: + branches: + - develop + pull_request: + branches: + - develop jobs: sonarcloud: @@ -25,8 +18,5 @@ jobs: - name: SonarCloud Scan # This is SonarSource/sonarcloud-github-action@v2.0.0 uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 - with: - args: > - -Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.metamaskrc.dist b/.metamaskrc.dist index c7431cc0719b..429addc860be 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -6,6 +6,7 @@ INFURA_PROJECT_ID=00000000000 ;PASSWORD=METAMASK PASSWORD ;SEGMENT_WRITE_KEY= +;BRIDGE_USE_DEV_APIS= ;SWAPS_USE_DEV_APIS= ;PORTFOLIO_URL= ;TRANSACTION_SECURITY_PROVIDER= diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 2500ff05f254..0160c032ed88 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Bestätigen" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Ich habe die Benachrichtigungen zur Kenntnis genommen und möchte trotzdem fortfahren" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Ihre erste Transaktion wurde vom Netzwerk bestätigt. Klicken Sie auf „OK“, um zurückzukehren." }, - "inlineAlert": { - "message": "Warnung" - }, "inputLogicEmptyState": { "message": "Geben Sie nur eine Nummer ein, die Sie den Drittanbieter jetzt oder in Zukunft ausgeben lassen möchten. Sie können die Ausgabenobergrenze später jederzeit ändern." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index bff686407193..a53b48a7be40 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Επιβεβαίωση" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Έχω ενημερωθεί για τις ειδοποιήσεις και εξακολουθώ να θέλω να συνεχίσω" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Η αρχική σας συναλλαγή επιβεβαιώθηκε από το δίκτυο. Κάντε κλικ στο OK για να επιστρέψετε." }, - "inlineAlert": { - "message": "Ειδοποίηση" - }, "inputLogicEmptyState": { "message": "Πληκτρολογήστε μόνο έναν αριθμό που σας βολεύει να ξοδέψει ο τρίτος τώρα ή στο μέλλον. Μπορείτε πάντα να αυξήσετε το ανώτατο όριο δαπανών αργότερα." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 53961ff86b35..25f3c5436158 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -311,6 +311,9 @@ "message": "Can’t find a token? You can manually add any token by pasting its address. Token contract addresses can be found on $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Add URL" + }, "addingCustomNetwork": { "message": "Adding Network" }, @@ -320,6 +323,9 @@ "additionalNetworks": { "message": "Additional networks" }, + "additionalRpcUrl": { + "message": "Additional RPC URL" + }, "address": { "message": "Address" }, @@ -412,6 +418,9 @@ "alertMessagePendingTransactions": { "message": "This transaction won’t go through until a previous transaction is complete. Learn how to cancel or speed up a transaction." }, + "alertMessageSignInDomainMismatch": { + "message": "The site making the request is not the site you’re signing into. This could be an attempt to steal your login credentials." + }, "alertMessageSigningOrSubmitting": { "message": "This transaction will only go through once your previous transaction is complete." }, @@ -445,6 +454,9 @@ "alertReasonPendingTransactions": { "message": "Pending transaction" }, + "alertReasonSignIn": { + "message": "Suspicious sign-in request" + }, "alertSettingsUnconnectedAccount": { "message": "Browsing a website with an unconnected account selected" }, @@ -945,10 +957,10 @@ "confirm": { "message": "Confirm" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "I have acknowledged the alerts and still want to proceed" }, - "confirmAlertModalAcknowledgeBlockaid": { + "confirmAlertModalAcknowledgeSingle": { "message": "I have acknowledged the alert and still want to proceed" }, "confirmAlertModalDetails": { @@ -966,6 +978,9 @@ "confirmConnectionTitle": { "message": "Confirm connection to $1" }, + "confirmDeletion": { + "message": "Confirm deletion" + }, "confirmFieldPaymaster": { "message": "Fee paid by" }, @@ -978,6 +993,9 @@ "confirmRecoveryPhrase": { "message": "Confirm Secret Recovery Phrase" }, + "confirmRpcUrlDeletionMessage": { + "message": "Are you sure you want to delete the RPC URL? Your information will not be saved for this network." + }, "confirmTitleDescContractInteractionTransaction": { "message": "Only confirm this transaction if you fully understand the content and trust the requesting site." }, @@ -1457,6 +1475,9 @@ "message": "Delete $1 network?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Delete RPC URL" + }, "deposit": { "message": "Deposit" }, @@ -2295,9 +2316,6 @@ "initialTransactionConfirmed": { "message": "Your initial transaction was confirmed by the network. Click OK to go back." }, - "inlineAlert": { - "message": "Alert" - }, "inputLogicEmptyState": { "message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later." }, @@ -2933,6 +2951,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "The name associated with this network." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 6a70bf40e68f..244c70313634 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "Confirmar" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Soy consciente de las alertas y aun así deseo continuar" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "La red confirmó la transacción inicial. Haga clic en Aceptar para volver." }, - "inlineAlert": { - "message": "Alerta" - }, "inputLogicEmptyState": { "message": "Ingrese solo una cantidad que esté dispuesto a gastar en el tercero ahora o en el futuro. Siempre puede aumentar el límite de gastos más adelante." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 0cd50d4788f8..1b2a54f3c4c1 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Confirmer" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "J’ai pris connaissance des alertes, mais je souhaite quand même continuer" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Votre transaction initiale a été confirmée par le réseau. Cliquez sur OK pour retourner à l’écran précédent." }, - "inlineAlert": { - "message": "Alerte" - }, "inputLogicEmptyState": { "message": "N'entrez qu'une somme que vous pouvez accepter que le tiers dépense aujourd'hui ou à l'avenir. Vous pourrez toujours augmenter le plafond de dépenses ultérieurement." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 5f83ad80bfdf..dc6318004958 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "कन्फर्म करें" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "मैंने एलर्ट को स्वीकार कर लिया है और इसके बावजूद आगे बढ़ना चाहता/चाहती हूं" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "नेटवर्क द्वारा आपके प्रारंभिक ट्रांसेक्शन को कन्फर्म किया गया था। वापस जाने के लिए ठीक पर क्लिक करें।" }, - "inlineAlert": { - "message": "एलर्ट" - }, "inputLogicEmptyState": { "message": "केवल वही संख्या डालें जो आप अभी या भविष्य में थर्ड पार्टी खर्च के साथ सहज महसूस करते हैं। आप बाद में कभी भी खर्च करने की लिमिट बढ़ा सकते हैं।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 45073d2af42b..0faef246fd39 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Konfirmasikan" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Saya telah mengetahui peringatannya dan tetap ingin melanjutkan" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Transaksi awal Anda dikonfirmasikan oleh jaringan. Klik Oke untuk kembali." }, - "inlineAlert": { - "message": "Peringatan" - }, "inputLogicEmptyState": { "message": "Masukkan angka yang menurut Anda dapat digunakan pihak ketiga sekarang atau di masa mendatang. Anda selalu dapat meningkatkan batas penggunaan nantinya." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ad69880ffd8e..4308665d7bcb 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "確認" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "アラートを確認したうえで続行します" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "最初のトランザクションはネットワークによって承認されました。戻るには「OK」をクリックします。" }, - "inlineAlert": { - "message": "アラート" - }, "inputLogicEmptyState": { "message": "現在または今後サードパーティが使用しても構わない額のみを入力してください。使用上限は後でいつでも増額できます。" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 9121a9b84281..464c3bf9e250 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "컨펌" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "경고를 인지했으며, 계속 진행합니다" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "최초 트랜잭션을 네트워크에서 컨펌했습니다. 돌아가려면 컨펌을 클릭하세요." }, - "inlineAlert": { - "message": "경고" - }, "inputLogicEmptyState": { "message": "타사에서 현재나 추후 지출하기에 무리가 없는 금액만 입력하세요. 지출 한도는 나중에 언제든지 상향할 수 있습니다." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e83e1b6d4eea..f201f475a4ec 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Confirmar" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Confirmo que recebi os alertas e ainda quero prosseguir" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Sua transação inicial foi confirmada pela rede. Clique em OK para voltar." }, - "inlineAlert": { - "message": "Alerta" - }, "inputLogicEmptyState": { "message": "Somente insira um número com o qual esteja confortável de o terceiro gastar agora ou no futuro. Você pode aumentar o limite de gastos a qualquer momento." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index cb41df257fe1..3c419a274034 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Подтвердить" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Я подтвердил(-а) получение оповещений и все еще хочу продолжить" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Ваша первоначальная транзакция подтверждена сетью. Нажмите ОК, чтобы вернуться." }, - "inlineAlert": { - "message": "Оповещение" - }, "inputLogicEmptyState": { "message": "Введите только ту сумму, которую третья сторона, по вашему мнению, может тратить сейчас или в будущем. Вы всегда можете увеличить лимит расходов позже." }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index cbd81acfcbc6..b6d454c012af 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "Kumpirmahin" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Kinikilala ko ang mga alerto at nais ko pa rin magpatuloy" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "Nakumpirma na ng network ang iyong inisyal na transaksyon. I-click ang OK para bumalik." }, - "inlineAlert": { - "message": "Alerto" - }, "inputLogicEmptyState": { "message": "Maglagay lamang ng numero na komportable ka sa paggastos ng third party ngayon o sa hinaharap. Maaari mong palaging taasan ang limitasyon sa paggastos sa ibang pagkakataon." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 13887164bfd1..cd6508892738 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Onayla" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Uyarıları kabul ediyorum ve yine de ilerlemek istiyorum" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "İlk işleminiz ağ tarafından onaylanmıştır. Geri gitmek için Tamam düğmesine tıklayın." }, - "inlineAlert": { - "message": "Uyarı" - }, "inputLogicEmptyState": { "message": "Sadece şu anda ya da gelecekte üçüncü taraf harcaması konusunda rahat olduğunuz bir sayı girin. Harcama üst limitini daha sonra dilediğiniz zaman artırabilirsiniz." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 3afd6bf2403e..ffc867cf6e76 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "Xác nhận" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Tôi đã hiểu rõ các cảnh báo và vẫn muốn tiếp tục" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "Mạng đã xác nhận giao dịch ban đầu của bạn. Nhấp OK để quay lại." }, - "inlineAlert": { - "message": "Cảnh báo" - }, "inputLogicEmptyState": { "message": "Chỉ nhập số mà bạn cảm thấy thoải mái đối với hạn mức chi tiêu ở hiện tại hoặc trong tương lai của bên thứ ba. Bạn luôn có thể tăng hạn mức chi tiêu sau này." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 6cdb6aaf1e3f..14c8e355be1f 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "确认" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "我已知晓提醒并仍想继续" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "您的初始交易已被网络确认。请点击“确定”返回。" }, - "inlineAlert": { - "message": "提醒" - }, "inputLogicEmptyState": { "message": "仅需输入一个您觉得比较恰当的现在或将来第三方支出的数字。以后您可以随时提高支出上限。" }, diff --git a/app/scripts/controllers/bridge.test.ts b/app/scripts/controllers/bridge.test.ts new file mode 100644 index 000000000000..a6001f7aa0d7 --- /dev/null +++ b/app/scripts/controllers/bridge.test.ts @@ -0,0 +1,29 @@ +import BridgeController from './bridge'; + +const EMPTY_INIT_STATE = { + bridgeState: { + bridgeFeatureFlags: { + extensionSupport: false, + }, + }, +}; + +describe('BridgeController', function () { + let bridgeController: BridgeController; + + beforeAll(function () { + bridgeController = new BridgeController(); + }); + + it('constructor should setup correctly', function () { + expect(bridgeController.store.getState()).toStrictEqual(EMPTY_INIT_STATE); + }); + + it('setBridgeFeatureFlags should set the bridge feature flags', function () { + const featureFlagsResponse = { extensionSupport: true }; + bridgeController.setBridgeFeatureFlags(featureFlagsResponse); + expect( + bridgeController.store.getState().bridgeState.bridgeFeatureFlags, + ).toStrictEqual(featureFlagsResponse); + }); +}); diff --git a/app/scripts/controllers/bridge.ts b/app/scripts/controllers/bridge.ts new file mode 100644 index 000000000000..23323371ea4c --- /dev/null +++ b/app/scripts/controllers/bridge.ts @@ -0,0 +1,36 @@ +import { ObservableStore } from '@metamask/obs-store'; + +export enum BridgeFeatureFlagsKey { + EXTENSION_SUPPORT = 'extensionSupport', +} + +export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: boolean; +}; + +const initialState = { + bridgeState: { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, + }, + }, +}; + +export default class BridgeController { + store = new ObservableStore(initialState); + + resetState = () => { + this.store.updateState({ + bridgeState: { + ...initialState.bridgeState, + }, + }); + }; + + setBridgeFeatureFlags = (bridgeFeatureFlags: BridgeFeatureFlags) => { + const { bridgeState } = this.store.getState(); + this.store.updateState({ + bridgeState: { ...bridgeState, bridgeFeatureFlags }, + }); + }; +} diff --git a/app/scripts/lib/accounts/BalancesController.test.ts b/app/scripts/lib/accounts/BalancesController.test.ts index 01ce1f88c608..02627c6aa201 100644 --- a/app/scripts/lib/accounts/BalancesController.test.ts +++ b/app/scripts/lib/accounts/BalancesController.test.ts @@ -9,9 +9,10 @@ import { createMockInternalAccount } from '../../../../test/jest/mocks'; import { BalancesController, AllowedActions, - BalancesControllerEvents, + AllowedEvents, BalancesControllerState, defaultState, + BalancesControllerMessenger, } from './BalancesController'; import { Poller } from './Poller'; @@ -46,14 +47,15 @@ const setupController = ({ } = {}) => { const controllerMessenger = new ControllerMessenger< AllowedActions, - BalancesControllerEvents + AllowedEvents >(); - const balancesControllerMessenger = controllerMessenger.getRestricted({ - name: 'BalancesController', - allowedActions: ['SnapController:handleRequest'], - allowedEvents: [], - }); + const balancesControllerMessenger: BalancesControllerMessenger = + controllerMessenger.getRestricted({ + name: 'BalancesController', + allowedActions: ['SnapController:handleRequest'], + allowedEvents: ['AccountsController:stateChange'], + }); const mockSnapHandleRequest = jest.fn(); controllerMessenger.registerActionHandler( diff --git a/app/scripts/lib/accounts/BalancesController.ts b/app/scripts/lib/accounts/BalancesController.ts index eee4ac11889a..ab1eb8c6cfe6 100644 --- a/app/scripts/lib/accounts/BalancesController.ts +++ b/app/scripts/lib/accounts/BalancesController.ts @@ -12,11 +12,17 @@ import { type Balance, type CaipAssetType, type InternalAccount, + isEvmAccountType, } from '@metamask/keyring-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import type { Draft } from 'immer'; +import type { + AccountsControllerChangeEvent, + AccountsControllerState, +} from '@metamask/accounts-controller'; +import { isBtcMainnetAddress } from '../../../../shared/lib/multichain'; import { Poller } from './Poller'; const controllerName = 'BalancesController'; @@ -81,15 +87,20 @@ export type BalancesControllerEvents = BalancesControllerStateChange; */ export type AllowedActions = HandleSnapRequest; +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerChangeEvent; + /** * Messenger type for the BalancesController. */ export type BalancesControllerMessenger = RestrictedControllerMessenger< typeof controllerName, BalancesControllerActions | AllowedActions, - BalancesControllerEvents, + BalancesControllerEvents | AllowedEvents, AllowedActions['type'], - never + AllowedEvents['type'] >; /** @@ -110,21 +121,6 @@ const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0']; const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0']; export const BTC_AVG_BLOCK_TIME = 600000; // 10 minutes in milliseconds -/** - * Returns whether an address is on the Bitcoin mainnet. - * - * This function only checks the prefix of the address to determine if it's on - * the mainnet or not. It doesn't validate the address itself, and should only - * be used as a temporary solution until this information is included in the - * account object. - * - * @param address - The address to check. - * @returns `true` if the address is on the Bitcoin mainnet, `false` otherwise. - */ -function isBtcMainnet(address: string): boolean { - return address.startsWith('bc1') || address.startsWith('1'); -} - /** * The BalancesController is responsible for fetching and caching account * balances. @@ -158,6 +154,11 @@ export class BalancesController extends BaseController< }, }); + this.messagingSystem.subscribe( + 'AccountsController:stateChange', + (newState) => this.#handleOnAccountsControllerChange(newState), + ); + this.#listMultichainAccounts = listMultichainAccounts; this.#poller = new Poller(() => this.updateBalances(), BTC_AVG_BLOCK_TIME); } @@ -203,7 +204,7 @@ export class BalancesController extends BaseController< partialState.balances[account.id] = await this.#getBalances( account.id, account.metadata.snap.id, - isBtcMainnet(account.address) + isBtcMainnetAddress(account.address) ? BTC_MAINNET_ASSETS : BTC_TESTNET_ASSETS, ); @@ -216,6 +217,21 @@ export class BalancesController extends BaseController< })); } + /** + * Handles changes in the accounts state, specifically when new non-EVM accounts are added. + * + * @param newState - The new state of the accounts controller. + */ + #handleOnAccountsControllerChange(newState: AccountsControllerState) { + // If we have any new non-EVM accounts, we just update non-EVM balances + const newNonEvmAccounts = Object.values( + newState.internalAccounts.accounts, + ).filter((account) => !isEvmAccountType(account.type)); + if (newNonEvmAccounts.length) { + this.updateBalances(); + } + } + /** * Get the balances for an account. * diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 92173fc5f72c..a6b47eef9376 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -120,6 +120,13 @@ export const SENTRY_BACKGROUND_STATE = { MultichainBalancesController: { balances: false, }, + BridgeController: { + bridgeState: { + bridgeFeatureFlags: { + extensionSupport: false, + }, + }, + }, CronjobController: { jobs: false, }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1799b36e6538..88491e2af62c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -139,6 +139,8 @@ import { } from '@metamask/snaps-utils'; ///: END:ONLY_INCLUDE_IF +import { Interface } from '@ethersproject/abi'; +import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis'; import { isEvmAccountType } from '@metamask/keyring-api'; import { methodsRequiringNetworkSwitch, @@ -207,6 +209,10 @@ import { } from '../../shared/modules/selectors'; import { createCaipStream } from '../../shared/modules/caip-stream'; import { BaseUrl } from '../../shared/constants/urls'; +import { + TOKEN_TRANSFER_LOG_TOPIC_HASH, + TRANSFER_SINFLE_LOG_TOPIC_HASH, +} from '../../shared/lib/transactions-controller-utils'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -315,6 +321,7 @@ import { createTxVerificationMiddleware } from './lib/tx-verification/tx-verific import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; +import BridgeController from './controllers/bridge'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -517,12 +524,6 @@ export default class MetamaskController extends EventEmitter { this.networkController.getProviderAndBlockTracker().blockTracker; this.deprecatedNetworkVersions = {}; - const tokenListMessenger = this.controllerMessenger.getRestricted({ - name: 'TokenListController', - allowedEvents: ['NetworkController:stateChange'], - allowedActions: ['NetworkController:getNetworkClientById'], - }); - const accountsControllerMessenger = this.controllerMessenger.getRestricted({ name: 'AccountsController', allowedEvents: [ @@ -560,6 +561,12 @@ export default class MetamaskController extends EventEmitter { networkConfigurations: this.networkController.state.networkConfigurations, }); + const tokenListMessenger = this.controllerMessenger.getRestricted({ + name: 'TokenListController', + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: ['NetworkController:stateChange'], + }); + this.tokenListController = new TokenListController({ chainId: this.networkController.state.providerConfig.chainId, preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( @@ -597,49 +604,21 @@ export default class MetamaskController extends EventEmitter { allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getNetworkClientById', + 'AccountsController:getSelectedAccount', + 'AccountsController:getAccount', ], allowedEvents: [ 'NetworkController:networkDidChange', - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'PreferencesController:stateChange', 'TokenListController:stateChange', ], }); this.tokensController = new TokensController({ + state: initState.TokensController, + provider: this.provider, messenger: tokensControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - // TODO: The tokens controller currently does not support internalAccounts. This is done to match the behavior of the previous tokens controller subscription. - onPreferencesStateChange: (listener) => - this.controllerMessenger.subscribe( - `AccountsController:selectedAccountChange`, - (newlySelectedInternalAccount) => { - listener({ selectedAddress: newlySelectedInternalAccount.address }); - }, - ), - onNetworkDidChange: (cb) => - networkControllerMessenger.subscribe( - 'NetworkController:networkDidChange', - () => { - const networkState = this.networkController.state; - return cb(networkState); - }, - ), - onTokenListStateChange: (listener) => - this.controllerMessenger.subscribe( - `${this.tokenListController.name}:stateChange`, - listener, - ), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - config: { - provider: this.provider, - selectedAddress: - initState.AccountsController?.internalAccounts?.accounts[ - initState.AccountsController?.internalAccounts?.selectedAccount - ]?.address ?? '', - }, - state: initState.TokensController, }); const nftControllerMessenger = this.controllerMessenger.getRestricted({ @@ -657,15 +636,9 @@ export default class MetamaskController extends EventEmitter { ], }); this.nftController = new NftController({ + state: initState.NftController, messenger: nftControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( this.assetsContractController, ), @@ -699,10 +672,6 @@ export default class MetamaskController extends EventEmitter { source, }, }), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - state: initState.NftController, }); this.nftController.setApiKey(process.env.OPENSEA_KEY); @@ -711,14 +680,13 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: 'NftDetectionController', allowedEvents: [ - 'PreferencesController:stateChange', 'NetworkController:stateChange', + 'PreferencesController:stateChange', ], allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getState', 'NetworkController:getNetworkClientById', - 'PreferencesController:getState', 'AccountsController:getSelectedAccount', ], }); @@ -726,21 +694,12 @@ export default class MetamaskController extends EventEmitter { this.nftDetectionController = new NftDetectionController({ messenger: nftDetectionControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - onNftsStateChange: (listener) => this.nftController.subscribe(listener), - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), getOpenSeaApiKey: () => this.nftController.openSeaApiKey, getBalancesInSingleCall: this.assetsContractController.getBalancesInSingleCall.bind( this.assetsContractController, ), addNft: this.nftController.addNft.bind(this.nftController), - getNftApi: this.nftController.getNftApi.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] disabled: @@ -748,11 +707,6 @@ export default class MetamaskController extends EventEmitter { undefined ? false // the detection is enabled by default : !this.preferencesController.store.getState().useNftDetection, - selectedAddress: - this.preferencesController.store.getState().selectedAddress, - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), }); this.metaMetricsController = new MetaMetricsController({ @@ -921,7 +875,7 @@ export default class MetamaskController extends EventEmitter { const multichainBalancesControllerMessenger = this.controllerMessenger.getRestricted({ name: 'BalancesController', - allowedEvents: [], + allowedEvents: ['AccountsController:stateChange'], allowedActions: ['SnapController:handleRequest'], }); @@ -946,55 +900,29 @@ export default class MetamaskController extends EventEmitter { fetchMultiExchangeRate, }); - const tokenRatesControllerMessenger = - this.controllerMessenger.getRestricted({ - name: 'TokenRatesController', - allowedEvents: [ - 'PreferencesController:stateChange', - 'TokensController:stateChange', - 'NetworkController:stateChange', - ], - allowedActions: [ - 'TokensController:getState', - 'NetworkController:getNetworkClientById', - 'NetworkController:getState', - 'PreferencesController:getState', - ], - }); + const tokenRatesMessenger = this.controllerMessenger.getRestricted({ + name: 'TokenRatesController', + allowedActions: [ + 'TokensController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + 'AccountsController:getAccount', + 'AccountsController:getSelectedAccount', + ], + allowedEvents: [ + 'NetworkController:stateChange', + 'AccountsController:selectedEvmAccountChange', + 'PreferencesController:stateChange', + 'TokensController:stateChange', + ], + }); // token exchange rate tracker - this.tokenRatesController = new TokenRatesController( - { - messenger: tokenRatesControllerMessenger, - chainId: this.networkController.state.providerConfig.chainId, - ticker: this.networkController.state.providerConfig.ticker, - selectedAddress: this.accountsController.getSelectedAccount().address, - onTokensStateChange: (listener) => - this.tokensController.subscribe(listener), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), - onPreferencesStateChange: (listener) => - this.controllerMessenger.subscribe( - `AccountsController:selectedAccountChange`, - (newlySelectedInternalAccount) => { - listener({ - selectedAddress: newlySelectedInternalAccount.address, - }); - }, - ), - tokenPricesService: new CodefiTokenPricesServiceV2(), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - }, - { - allTokens: this.tokensController.state.allTokens, - allDetectedTokens: this.tokensController.state.allDetectedTokens, - }, - initState.TokenRatesController, - ); + this.tokenRatesController = new TokenRatesController({ + state: initState.TokenRatesController, + messenger: tokenRatesMessenger, + tokenPricesService: new CodefiTokenPricesServiceV2(), + }); this.preferencesController.store.subscribe( previousValueComparator((prevState, currState) => { @@ -1621,6 +1549,7 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: 'TokenDetectionController', allowedActions: [ + 'AccountsController:getAccount', 'AccountsController:getSelectedAccount', 'KeyringController:getState', 'NetworkController:getNetworkClientById', @@ -1632,7 +1561,7 @@ export default class MetamaskController extends EventEmitter { 'TokensController:addDetectedTokens', ], allowedEvents: [ - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'KeyringController:lock', 'KeyringController:unlock', 'NetworkController:networkDidChange', @@ -1967,6 +1896,7 @@ export default class MetamaskController extends EventEmitter { }, initState.SwapsController, ); + this.bridgeController = new BridgeController(); this.smartTransactionsController = new SmartTransactionsController( { getNetworkClientById: this.networkController.getNetworkClientById.bind( @@ -2196,6 +2126,7 @@ export default class MetamaskController extends EventEmitter { EncryptionPublicKeyController: this.encryptionPublicKeyController, SignatureController: this.signatureController, SwapsController: this.swapsController.store, + BridgeController: this.bridgeController.store, EnsController: this.ensController, ApprovalController: this.approvalController, PPOMController: this.ppomController, @@ -3016,6 +2947,7 @@ export default class MetamaskController extends EventEmitter { appMetadataController, permissionController, preferencesController, + bridgeController, swapsController, tokensController, smartTransactionsController, @@ -3621,6 +3553,10 @@ export default class MetamaskController extends EventEmitter { setSwapsQuotesPollingLimitEnabled: swapsController.setSwapsQuotesPollingLimitEnabled.bind(swapsController), + // Bridge + setBridgeFeatureFlags: + bridgeController.setBridgeFeatureFlags.bind(bridgeController), + // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( smartTransactionsController, @@ -6344,7 +6280,7 @@ export default class MetamaskController extends EventEmitter { } await this._createTransactionNotifcation(transactionMeta); - this._updateNFTOwnership(transactionMeta); + await this._updateNFTOwnership(transactionMeta); this._trackTransactionFailure(transactionMeta); } @@ -6372,46 +6308,158 @@ export default class MetamaskController extends EventEmitter { } } - _updateNFTOwnership(transactionMeta) { + async _updateNFTOwnership(transactionMeta) { // if this is a transferFrom method generated from within the app it may be an NFT transfer transaction // in which case we will want to check and update ownership status of the transferred NFT. - const { type, txParams, chainId } = transactionMeta; + const { type, txParams, chainId, txReceipt } = transactionMeta; + const selectedAddress = + this.accountsController.getSelectedAccount().address; - if ( - type !== TransactionType.tokenMethodTransferFrom || - txParams === undefined - ) { + const { allNfts } = this.nftController.state; + const txReceiptLogs = txReceipt?.logs; + + const isContractInteractionTx = + type === TransactionType.contractInteraction && txReceiptLogs; + const isTransferFromTx = + (type === TransactionType.tokenMethodTransferFrom || + type === TransactionType.tokenMethodSafeTransferFrom) && + txParams !== undefined; + + if (!isContractInteractionTx && !isTransferFromTx) { return; } - const { data, to: contractAddress, from: userAddress } = txParams; - const transactionData = parseStandardTokenTransactionData(data); - // Sometimes the tokenId value is parsed as "_value" param. Not seeing this often any more, but still occasionally: - // i.e. call approve() on BAYC contract - https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#writeContract, and tokenId shows up as _value, - // not sure why since it doesn't match the ERC721 ABI spec we use to parse these transactions - https://github.com/MetaMask/metamask-eth-abis/blob/d0474308a288f9252597b7c93a3a8deaad19e1b2/src/abis/abiERC721.ts#L62. - const transactionDataTokenId = - getTokenIdParam(transactionData) ?? getTokenValueParam(transactionData); + if (isTransferFromTx) { + const { data, to: contractAddress, from: userAddress } = txParams; + const transactionData = parseStandardTokenTransactionData(data); + // Sometimes the tokenId value is parsed as "_value" param. Not seeing this often any more, but still occasionally: + // i.e. call approve() on BAYC contract - https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#writeContract, and tokenId shows up as _value, + // not sure why since it doesn't match the ERC721 ABI spec we use to parse these transactions - https://github.com/MetaMask/metamask-eth-abis/blob/d0474308a288f9252597b7c93a3a8deaad19e1b2/src/abis/abiERC721.ts#L62. + const transactionDataTokenId = + getTokenIdParam(transactionData) ?? getTokenValueParam(transactionData); + + // check if its a known NFT + const knownNft = allNfts?.[userAddress]?.[chainId]?.find( + ({ address, tokenId }) => + isEqualCaseInsensitive(address, contractAddress) && + tokenId === transactionDataTokenId, + ); - const { allNfts } = this.nftController.state; + // if it is we check and update ownership status. + if (knownNft) { + this.nftController.checkAndUpdateSingleNftOwnershipStatus( + knownNft, + false, + // TODO add networkClientId once it is available in the transactionMeta + // the chainId previously passed here didn't actually allow us to check for ownership on a non globally selected network + // because the check would use the provider for the globally selected network, not the chainId passed here. + { userAddress }, + ); + } + } else { + // Else if contract interaction we will parse the logs + + const allNftTransferLog = txReceiptLogs.map((txReceiptLog) => { + const isERC1155NftTransfer = + txReceiptLog.topics && + txReceiptLog.topics[0] === TRANSFER_SINFLE_LOG_TOPIC_HASH; + const isERC721NftTransfer = + txReceiptLog.topics && + txReceiptLog.topics[0] === TOKEN_TRANSFER_LOG_TOPIC_HASH; + let isTransferToSelectedAddress; + + if (isERC1155NftTransfer) { + isTransferToSelectedAddress = + txReceiptLog.topics && + txReceiptLog.topics[3] && + txReceiptLog.topics[3].match(selectedAddress?.slice(2)); + } - // check if its a known NFT - const knownNft = allNfts?.[userAddress]?.[chainId]?.find( - ({ address, tokenId }) => - isEqualCaseInsensitive(address, contractAddress) && - tokenId === transactionDataTokenId, - ); + if (isERC721NftTransfer) { + isTransferToSelectedAddress = + txReceiptLog.topics && + txReceiptLog.topics[2] && + txReceiptLog.topics[2].match(selectedAddress?.slice(2)); + } - // if it is we check and update ownership status. - if (knownNft) { - this.nftController.checkAndUpdateSingleNftOwnershipStatus( - knownNft, - false, - // TODO add networkClientId once it is available in the transactionMeta - // the chainId previously passed here didn't actually allow us to check for ownership on a non globally selected network - // because the check would use the provider for the globally selected network, not the chainId passed here. - { userAddress }, - ); + return { + isERC1155NftTransfer, + isERC721NftTransfer, + isTransferToSelectedAddress, + ...txReceiptLog, + }; + }); + if (allNftTransferLog.length !== 0) { + const allNftParsedLog = []; + allNftTransferLog.forEach((singleLog) => { + if ( + singleLog.isTransferToSelectedAddress && + (singleLog.isERC1155NftTransfer || singleLog.isERC721NftTransfer) + ) { + let iface; + if (singleLog.isERC1155NftTransfer) { + iface = new Interface(abiERC1155); + } else { + iface = new Interface(abiERC721); + } + try { + const parsedLog = iface.parseLog({ + data: singleLog.data, + topics: singleLog.topics, + }); + allNftParsedLog.push({ + contract: singleLog.address, + ...parsedLog, + }); + } catch (err) { + // ignore + } + } + }); + // Filter known nfts and new Nfts + const knownNFTs = []; + const newNFTs = []; + allNftParsedLog.forEach((single) => { + const tokenIdFromLog = getTokenIdParam(single); + const existingNft = allNfts?.[selectedAddress]?.[chainId]?.find( + ({ address, tokenId }) => { + return ( + isEqualCaseInsensitive(address, single.contract) && + tokenId === tokenIdFromLog + ); + }, + ); + if (existingNft) { + knownNFTs.push(existingNft); + } else { + newNFTs.push({ + tokenId: tokenIdFromLog, + ...single, + }); + } + }); + // For known nfts only refresh ownership + const refreshOwnershipNFts = knownNFTs.map(async (singleNft) => { + return this.nftController.checkAndUpdateSingleNftOwnershipStatus( + singleNft, + false, + // TODO add networkClientId once it is available in the transactionMeta + // the chainId previously passed here didn't actually allow us to check for ownership on a non globally selected network + // because the check would use the provider for the globally selected network, not the chainId passed here. + { selectedAddress }, + ); + }); + await Promise.allSettled(refreshOwnershipNFts); + // For new nfts, add them to state + const addNftPromises = newNFTs.map(async (singleNft) => { + return this.nftController.addNft( + singleNft.contract, + singleNft.tokenId, + ); + }); + await Promise.allSettled(addNftPromises); + } } } diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 771378568020..1ad12736fe2a 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -662,59 +662,46 @@ describe('MetaMaskController', () => { it('should clear previous identities after vault restoration', async () => { jest.spyOn(metamaskController, 'getBalance').mockResolvedValue('0x0'); - let startTime = Date.now(); await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED, ); - let endTime = Date.now(); - const firstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const firstVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); - expect( - firstVaultIdentities[TEST_ADDRESS].lastSelected >= startTime && - firstVaultIdentities[TEST_ADDRESS].lastSelected <= endTime, - ).toStrictEqual(true); - delete firstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(firstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - }); + expect(firstVaultAccounts).toHaveLength(1); + expect(firstVaultAccounts[0].address).toBe(TEST_ADDRESS); - await metamaskController.preferencesController.setAccountLabel( - TEST_ADDRESS, + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + metamaskController.accountsController.setAccountName( + selectedAccount.id, 'Account Foo', ); - const labelledFirstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const labelledFirstVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); - delete labelledFirstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(labelledFirstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, - }); - startTime = Date.now(); + expect(labelledFirstVaultAccounts[0].address).toBe(TEST_ADDRESS); + expect(labelledFirstVaultAccounts[0].metadata.name).toBe('Account Foo'); + await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED_ALT, ); - endTime = Date.now(); - const secondVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const secondVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); + + expect(secondVaultAccounts).toHaveLength(1); expect( - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected >= startTime && - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected <= endTime, - ).toStrictEqual(true); - delete secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected; - expect(secondVaultIdentities).toStrictEqual({ - [TEST_ADDRESS_ALT]: { - address: TEST_ADDRESS_ALT, - name: DEFAULT_LABEL, - }, - }); + metamaskController.accountsController.getSelectedAccount().address, + ).toBe(TEST_ADDRESS_ALT); + expect(secondVaultAccounts[0].address).toBe(TEST_ADDRESS_ALT); + expect(secondVaultAccounts[0].metadata.name).toBe(DEFAULT_LABEL); }); it('should restore any consecutive accounts with balances without extra zero balance accounts', async () => { @@ -748,29 +735,29 @@ describe('MetaMaskController', () => { allDetectedTokens: { '0x1': { [TEST_ADDRESS_2]: [{}] } }, }); - const startTime = Date.now(); await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED, ); // Expect first account to be selected - const identities = cloneDeep(metamaskController.getState().identities); - expect( - identities[TEST_ADDRESS].lastSelected >= startTime && - identities[TEST_ADDRESS].lastSelected <= Date.now(), - ).toStrictEqual(true); - - // Expect first 2 accounts to be restored - delete identities[TEST_ADDRESS].lastSelected; - expect(identities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - [TEST_ADDRESS_2]: { - address: TEST_ADDRESS_2, - name: 'Account 2', - lastSelected: expect.any(Number), - }, - }); + const accounts = cloneDeep( + metamaskController.accountsController.listAccounts(), + ); + + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + + expect(selectedAccount.address).toBe(TEST_ADDRESS); + expect(accounts).toHaveLength(2); + expect(accounts[0].address).toBe(TEST_ADDRESS); + expect(accounts[0].metadata.name).toBe(DEFAULT_LABEL); + expect(accounts[1].address).toBe(TEST_ADDRESS_2); + expect(accounts[1].metadata.name).toBe('Account 2'); + // TODO: Handle last selected in the update of the next accounts controller. + // expect(accounts[1].metadata.lastSelected).toBeGreaterThan( + // accounts[0].metadata.lastSelected, + // ); }); }); @@ -1596,14 +1583,12 @@ describe('MetaMaskController', () => { symbol: 'FOO', }; - metamaskController.tokensController.update((state) => { - state.tokens = [ - { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - ...tokenData, - }, - ]; - }); + await metamaskController.tokensController.addTokens([ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + ...tokenData, + }, + ]); metamaskController.provider = provider; const tokenDetails = diff --git a/builds.yml b/builds.yml index 2e449c687591..0e83d86303b0 100644 --- a/builds.yml +++ b/builds.yml @@ -135,6 +135,7 @@ features: # env object supports both declarations (- FOO), and definitions (- FOO: BAR). # Variables that were declared have to be defined somewhere in the load chain before usage env: + - BRIDGE_USE_DEV_APIS: false - SWAPS_USE_DEV_APIS: false - PORTFOLIO_URL: https://portfolio.metamask.io - TOKEN_ALLOWANCE_IMPROVEMENTS: false @@ -266,6 +267,8 @@ env: # Enables the notifications feature within the build: - NOTIFICATIONS: '' + - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io + ### # Meta variables ### diff --git a/development/lib/retry.js b/development/lib/retry.js index e6e5dfc040af..813a63aa44e4 100644 --- a/development/lib/retry.js +++ b/development/lib/retry.js @@ -11,12 +11,12 @@ * @param {string} [args.rejectionMessage] - The message for the rejected promise * this function will return in the event of failure. (Default: "Retry limit * reached") - * @param {boolean} [args.retryUntilFailure] - Retries until the function fails. + * @param {boolean} [args.stopAfterOneFailure] - Retries until the function fails. * @param {Function} functionToRetry - The function that is run and tested for * failure. * @returns {Promise<* | null | Error>} a promise that either resolves with one of the following: * - If successful, resolves with the return value of functionToRetry. - * - If functionToRetry fails while retryUntilFailure is true, resolves with null. + * - If functionToRetry fails while stopAfterOneFailure is true, resolves with null. * - Otherwise it is rejected with rejectionMessage. */ async function retry( @@ -24,7 +24,7 @@ async function retry( retries, delay = 0, rejectionMessage = 'Retry limit reached', - retryUntilFailure = false, + stopAfterOneFailure = false, }, functionToRetry, ) { @@ -36,7 +36,7 @@ async function retry( try { const result = await functionToRetry(); - if (!retryUntilFailure) { + if (!stopAfterOneFailure) { return result; } } catch (error) { @@ -46,18 +46,22 @@ async function retry( console.error('error caught in retry():', error); } - if (attempts < retries) { - console.log('Ready to retry() again'); + if (stopAfterOneFailure) { + throw new Error('Test failed. No more retries will be performed'); } - if (retryUntilFailure) { - return null; + if (attempts < retries) { + console.log('Ready to retry() again'); } } finally { attempts += 1; } } + if (stopAfterOneFailure) { + return null; + } + throw new Error(rejectionMessage); } diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index d17b42a08fc8..6e8a55b7124a 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -800,7 +800,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index d17b42a08fc8..6e8a55b7124a 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -800,7 +800,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index d17b42a08fc8..6e8a55b7124a 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -800,7 +800,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index b1576a7d4f79..08b8c7e19958 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -892,7 +892,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 02078b5d39fc..a01cf9c55765 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -2124,8 +2124,8 @@ }, "chokidar>anymatch": { "packages": { - "chokidar>anymatch>picomatch": true, - "chokidar>normalize-path": true + "chokidar>anymatch>normalize-path": true, + "chokidar>anymatch>picomatch": true } }, "chokidar>anymatch>picomatch": { @@ -3945,8 +3945,8 @@ }, "gulp-sourcemaps>@gulp-sourcemaps/identity-map": { "packages": { - "chokidar>normalize-path": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>acorn": true, + "gulp-sourcemaps>@gulp-sourcemaps/identity-map>normalize-path": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>postcss": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>source-map": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>through2": true @@ -3997,13 +3997,8 @@ }, "gulp-sourcemaps>@gulp-sourcemaps/map-sources": { "packages": { - "gulp-sourcemaps>@gulp-sourcemaps/map-sources>normalize-path": true, - "gulp-sourcemaps>@gulp-sourcemaps/map-sources>through2": true - } - }, - "gulp-sourcemaps>@gulp-sourcemaps/map-sources>normalize-path": { - "packages": { - "vinyl>remove-trailing-separator": true + "gulp-sourcemaps>@gulp-sourcemaps/map-sources>through2": true, + "gulp-watch>anymatch>normalize-path": true } }, "gulp-sourcemaps>@gulp-sourcemaps/map-sources>through2": { @@ -4254,10 +4249,10 @@ "setTimeout": true }, "packages": { + "chokidar": true, "eslint>glob-parent": true, "gulp-watch>ansi-colors": true, "gulp-watch>anymatch": true, - "gulp-watch>chokidar": true, "gulp-watch>fancy-log": true, "gulp-watch>path-is-absolute": true, "gulp-watch>readable-stream": true, @@ -4314,7 +4309,7 @@ "packages": { "gulp-watch>anymatch>micromatch>braces>expand-range": true, "gulp-watch>anymatch>micromatch>braces>preserve": true, - "gulp-watch>chokidar>braces>repeat-element": true + "gulp-watch>anymatch>micromatch>braces>repeat-element": true } }, "gulp-watch>anymatch>micromatch>braces>expand-range": { @@ -4327,7 +4322,7 @@ "gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>is-number": true, "gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>isobject": true, "gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic": true, - "gulp-watch>chokidar>braces>repeat-element": true, + "gulp-watch>anymatch>micromatch>braces>repeat-element": true, "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true } }, @@ -4455,8 +4450,6 @@ }, "packages": { "chokidar>normalize-path": true, - "del>is-glob": true, - "eslint>glob-parent": true, "eslint>is-glob": true, "gulp-watch>chokidar>anymatch": true, "gulp-watch>chokidar>async-each": true, @@ -4470,750 +4463,234 @@ "pumpify>inherits": true } }, - "gulp-watch>chokidar>anymatch": { + "gulp-watch>chokidar>fsevents": { "builtin": { - "path.sep": true + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "console.assert": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true }, "packages": { - "gulp-watch>chokidar>anymatch>micromatch": true, - "gulp-watch>chokidar>anymatch>normalize-path": true + "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, - "gulp-watch>chokidar>anymatch>micromatch": { + "gulp-watch>chokidar>fsevents>node-pre-gyp": { "builtin": { - "path.basename": true, - "path.sep": true, - "util.inspect": true + "events.EventEmitter": true, + "fs.existsSync": true, + "fs.readFileSync": true, + "fs.renameSync": true, + "path.dirname": true, + "path.existsSync": true, + "path.join": true, + "path.resolve": true, + "url.parse": true, + "url.resolve": true, + "util.inherits": true }, "globals": { - "process.platform": true + "__dirname": true, + "console.log": true, + "process.arch": true, + "process.cwd": true, + "process.env": true, + "process.platform": true, + "process.version.substr": true, + "process.versions": true }, "packages": { - "@babel/register>clone-deep>kind-of": true, - "gulp-watch>chokidar>anymatch>micromatch>define-property": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob": true, - "gulp-watch>chokidar>braces": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp-zip>plugin-error>arr-diff": true, - "gulp-zip>plugin-error>extend-shallow": true, - "gulp>gulp-cli>liftoff>fined>object.pick": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>nanomatch": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>define-property": { - "packages": { - "gulp>gulp-cli>isobject": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>define-property": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>extend-shallow": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>define-property": { - "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets": { - "globals": { - "__filename": true + "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": { + "builtin": { + "child_process.spawnSync": true, + "fs.readdirSync": true, + "os.platform": true }, - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>debug": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>extend-shallow": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>gulp-cli>matchdep>micromatch>extglob>expand-brackets>posix-character-classes": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true + "globals": { + "process.env": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>debug": { + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": { "builtin": { - "fs.SyncWriteStream": true, - "net.Socket": true, - "tty.WriteStream": true, - "tty.isatty": true, - "util": true + "path": true, + "stream.Stream": true, + "url": true }, "globals": { - "chrome": true, "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true + "process.argv": true, + "process.env.DEBUG_NOPT": true, + "process.env.NOPT_DEBUG": true, + "process.platform": true }, "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>debug>ms": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true + "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>nopt>abbrev": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": { + "builtin": { + "child_process.exec": true, + "path": true + }, + "globals": { + "process.env.COMPUTERNAME": true, + "process.env.ComSpec": true, + "process.env.EDITOR": true, + "process.env.HOSTNAME": true, + "process.env.PATH": true, + "process.env.PROMPT": true, + "process.env.PS1": true, + "process.env.Path": true, + "process.env.SHELL": true, + "process.env.USER": true, + "process.env.USERDOMAIN": true, + "process.env.USERNAME": true, + "process.env.VISUAL": true, + "process.env.path": true, + "process.nextTick": true, + "process.platform": true + }, "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true + "@storybook/core>@storybook/core-server>x-default-browser>default-browser-id>untildify>os-homedir": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": { + "globals": { + "process.env.SystemRoot": true, + "process.env.TEMP": true, + "process.env.TMP": true, + "process.env.TMPDIR": true, + "process.env.windir": true, + "process.platform": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>extend-shallow": { + "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": { + "builtin": { + "assert": true, + "fs": true, + "path.join": true + }, + "globals": { + "process.platform": true, + "setTimeout": true + }, "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "nyc>glob": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>extend-shallow": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": { + "globals": { + "console": true, + "process": true } }, - "gulp-watch>chokidar>anymatch>normalize-path": { + "gulp-watch>fancy-log": { + "globals": { + "console": true, + "process.argv.indexOf": true, + "process.stderr.write": true, + "process.stdout.write": true + }, "packages": { - "vinyl>remove-trailing-separator": true + "fancy-log>ansi-gray": true, + "fancy-log>color-support": true, + "fancy-log>time-stamp": true } }, - "gulp-watch>chokidar>async-each": { + "gulp-watch>path-is-absolute": { "globals": { - "define": true - } - }, - "gulp-watch>chokidar>braces": { - "packages": { - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>extend-shallow": true, - "gulp-watch>chokidar>braces>fill-range": true, - "gulp-watch>chokidar>braces>repeat-element": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>snapdragon-node": true, - "gulp-watch>chokidar>braces>split-string": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>gulp-cli>isobject": true, - "gulp>undertaker>arr-flatten": true + "process.platform": true } }, - "gulp-watch>chokidar>braces>extend-shallow": { + "gulp-watch>readable-stream": { + "builtin": { + "events.EventEmitter": true, + "stream": true, + "util": true + }, + "globals": { + "process.browser": true, + "process.env.READABLE_STREAM": true, + "process.stderr": true, + "process.stdout": true, + "process.version.slice": true, + "setImmediate": true + }, "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp-watch>readable-stream>isarray": true, + "gulp-watch>readable-stream>safe-buffer": true, + "gulp-watch>readable-stream>string_decoder": true, + "pumpify>inherits": true, + "readable-stream-2>core-util-is": true, + "readable-stream-2>process-nextick-args": true, + "readable-stream>util-deprecate": true } }, - "gulp-watch>chokidar>braces>fill-range": { + "gulp-watch>readable-stream>safe-buffer": { "builtin": { - "util.inspect": true - }, - "packages": { - "gulp-watch>chokidar>braces>fill-range>extend-shallow": true, - "gulp-watch>chokidar>braces>fill-range>is-number": true, - "gulp-watch>chokidar>braces>fill-range>to-regex-range": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + "buffer": true } }, - "gulp-watch>chokidar>braces>fill-range>extend-shallow": { + "gulp-watch>readable-stream>string_decoder": { "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp-watch>readable-stream>safe-buffer": true } }, - "gulp-watch>chokidar>braces>fill-range>is-number": { + "gulp-watch>vinyl-file": { + "builtin": { + "path.resolve": true + }, + "globals": { + "process.cwd": true + }, "packages": { - "gulp-watch>chokidar>braces>fill-range>is-number>kind-of": true + "del>graceful-fs": true, + "gh-pages>globby>pinkie-promise": true, + "gulp-watch>vinyl-file>pify": true, + "gulp-watch>vinyl-file>strip-bom": true, + "gulp-watch>vinyl-file>strip-bom-stream": true, + "gulp-watch>vinyl-file>vinyl": true } }, - "gulp-watch>chokidar>braces>fill-range>is-number>kind-of": { + "gulp-watch>vinyl-file>strip-bom": { + "globals": { + "Buffer.isBuffer": true + }, "packages": { - "browserify>insert-module-globals>is-buffer": true + "gulp>vinyl-fs>remove-bom-buffer>is-utf8": true } }, - "gulp-watch>chokidar>braces>fill-range>to-regex-range": { + "gulp-watch>vinyl-file>strip-bom-stream": { "packages": { - "gulp-watch>chokidar>braces>fill-range>is-number": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + "gulp-watch>vinyl-file>strip-bom": true, + "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": true } }, - "gulp-watch>chokidar>braces>snapdragon": { + "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": { "builtin": { - "fs.readFileSync": true, - "path.dirname": true, - "util.inspect": true + "util.inherits": true }, "globals": { - "__filename": true + "Buffer.concat": true, + "setImmediate": true }, "packages": { - "gulp-watch>chokidar>braces>snapdragon>base": true, - "gulp-watch>chokidar>braces>snapdragon>debug": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true, - "gulp-watch>chokidar>braces>snapdragon>extend-shallow": true, - "gulp-watch>chokidar>braces>snapdragon>map-cache": true, - "gulp-watch>chokidar>braces>snapdragon>source-map": true, - "gulp-watch>chokidar>braces>snapdragon>use": true, - "resolve-url-loader>rework>css>source-map-resolve": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon-node>define-property": true, - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node>define-property": { - "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base": { - "builtin": { - "util.inherits": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base": true, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils": true, - "gulp-watch>chokidar>braces>snapdragon>base>component-emitter": true, - "gulp-watch>chokidar>braces>snapdragon>base>define-property": true, - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep": true, - "gulp-watch>chokidar>braces>snapdragon>base>pascalcase": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>union-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>component-emitter": true, - "gulp>gulp-cli>array-sort>get-value": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>map-visit": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>object-visit": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>map-visit": { - "builtin": { - "util.inspect": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>object-visit": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>object-visit": { - "packages": { - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values": true, - "gulp>gulp-cli>array-sort>get-value": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value": { - "packages": { - "@babel/register>clone-deep>is-plain-object": true, - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value>extend-shallow": true, - "gulp-watch>chokidar>braces>split-string": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value>extend-shallow": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>union-value": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value": true, - "gulp-zip>plugin-error>arr-union": true, - "gulp>gulp-cli>array-sort>get-value": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>has-values": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>isobject": true, - "gulp>gulp-cli>array-sort>get-value": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>isobject": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>isobject>isarray": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils": { - "builtin": { - "util": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true, - "gulp-zip>plugin-error>arr-union": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend": { - "builtin": { - "util.inherits": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy>copy-descriptor": true, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy>kind-of": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>define-property": { - "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep>is-extendable": true, - "gulp>undertaker>object.reduce>for-own>for-in": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep>is-extendable": { - "packages": { - "@babel/register>clone-deep>is-plain-object": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>debug": { - "builtin": { - "fs.SyncWriteStream": true, - "net.Socket": true, - "tty.WriteStream": true, - "tty.isatty": true, - "util": true - }, - "globals": { - "chrome": true, - "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>debug>ms": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor": true, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-accessor-descriptor": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>extend-shallow": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>use": { - "packages": { - "@babel/register>clone-deep>kind-of": true - } - }, - "gulp-watch>chokidar>braces>split-string": { - "packages": { - "gulp-zip>plugin-error>extend-shallow": true - } - }, - "gulp-watch>chokidar>braces>to-regex": { - "packages": { - "gulp-watch>chokidar>braces>to-regex>define-property": true, - "gulp-watch>chokidar>braces>to-regex>safe-regex": true, - "gulp-zip>plugin-error>extend-shallow": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp-watch>chokidar>braces>to-regex>define-property": { - "packages": { - "gulp>gulp-cli>isobject": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>to-regex>safe-regex": { - "packages": { - "gulp-watch>chokidar>braces>to-regex>safe-regex>ret": true - } - }, - "gulp-watch>chokidar>fsevents": { - "builtin": { - "events.EventEmitter": true, - "fs.stat": true, - "path.join": true, - "util.inherits": true - }, - "globals": { - "__dirname": true, - "console.assert": true, - "process.nextTick": true, - "process.platform": true, - "setImmediate": true - }, - "packages": { - "gulp-watch>chokidar>fsevents>node-pre-gyp": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp": { - "builtin": { - "events.EventEmitter": true, - "fs.existsSync": true, - "fs.readFileSync": true, - "fs.renameSync": true, - "path.dirname": true, - "path.existsSync": true, - "path.join": true, - "path.resolve": true, - "url.parse": true, - "url.resolve": true, - "util.inherits": true - }, - "globals": { - "__dirname": true, - "console.log": true, - "process.arch": true, - "process.cwd": true, - "process.env": true, - "process.platform": true, - "process.version.substr": true, - "process.versions": true - }, - "packages": { - "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": { - "builtin": { - "child_process.spawnSync": true, - "fs.readdirSync": true, - "os.platform": true - }, - "globals": { - "process.env": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": { - "builtin": { - "path": true, - "stream.Stream": true, - "url": true - }, - "globals": { - "console": true, - "process.argv": true, - "process.env.DEBUG_NOPT": true, - "process.env.NOPT_DEBUG": true, - "process.platform": true - }, - "packages": { - "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>nopt>abbrev": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": { - "builtin": { - "child_process.exec": true, - "path": true - }, - "globals": { - "process.env.COMPUTERNAME": true, - "process.env.ComSpec": true, - "process.env.EDITOR": true, - "process.env.HOSTNAME": true, - "process.env.PATH": true, - "process.env.PROMPT": true, - "process.env.PS1": true, - "process.env.Path": true, - "process.env.SHELL": true, - "process.env.USER": true, - "process.env.USERDOMAIN": true, - "process.env.USERNAME": true, - "process.env.VISUAL": true, - "process.env.path": true, - "process.nextTick": true, - "process.platform": true - }, - "packages": { - "@storybook/core>@storybook/core-server>x-default-browser>default-browser-id>untildify>os-homedir": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": { - "globals": { - "process.env.SystemRoot": true, - "process.env.TEMP": true, - "process.env.TMP": true, - "process.env.TMPDIR": true, - "process.env.windir": true, - "process.platform": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": { - "builtin": { - "assert": true, - "fs": true, - "path.join": true - }, - "globals": { - "process.platform": true, - "setTimeout": true - }, - "packages": { - "nyc>glob": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": { - "globals": { - "console": true, - "process": true - } - }, - "gulp-watch>chokidar>is-binary-path": { - "builtin": { - "path.extname": true - }, - "packages": { - "gulp-watch>chokidar>is-binary-path>binary-extensions": true - } - }, - "gulp-watch>chokidar>readdirp": { - "builtin": { - "path.join": true, - "path.relative": true, - "util.inherits": true - }, - "globals": { - "setImmediate": true - }, - "packages": { - "del>graceful-fs": true, - "gulp-watch>chokidar>anymatch>micromatch": true, - "gulp-watch>readable-stream": true - } - }, - "gulp-watch>chokidar>upath": { - "builtin": { - "path": true - } - }, - "gulp-watch>fancy-log": { - "globals": { - "console": true, - "process.argv.indexOf": true, - "process.stderr.write": true, - "process.stdout.write": true - }, - "packages": { - "fancy-log>ansi-gray": true, - "fancy-log>color-support": true, - "fancy-log>time-stamp": true - } - }, - "gulp-watch>path-is-absolute": { - "globals": { - "process.platform": true - } - }, - "gulp-watch>readable-stream": { - "builtin": { - "events.EventEmitter": true, - "stream": true, - "util": true - }, - "globals": { - "process.browser": true, - "process.env.READABLE_STREAM": true, - "process.stderr": true, - "process.stdout": true, - "process.version.slice": true, - "setImmediate": true - }, - "packages": { - "gulp-watch>readable-stream>isarray": true, - "gulp-watch>readable-stream>safe-buffer": true, - "gulp-watch>readable-stream>string_decoder": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true - } - }, - "gulp-watch>readable-stream>safe-buffer": { - "builtin": { - "buffer": true - } - }, - "gulp-watch>readable-stream>string_decoder": { - "packages": { - "gulp-watch>readable-stream>safe-buffer": true - } - }, - "gulp-watch>vinyl-file": { - "builtin": { - "path.resolve": true - }, - "globals": { - "process.cwd": true - }, - "packages": { - "del>graceful-fs": true, - "gh-pages>globby>pinkie-promise": true, - "gulp-watch>vinyl-file>pify": true, - "gulp-watch>vinyl-file>strip-bom": true, - "gulp-watch>vinyl-file>strip-bom-stream": true, - "gulp-watch>vinyl-file>vinyl": true - } - }, - "gulp-watch>vinyl-file>strip-bom": { - "globals": { - "Buffer.isBuffer": true - }, - "packages": { - "gulp>vinyl-fs>remove-bom-buffer>is-utf8": true - } - }, - "gulp-watch>vinyl-file>strip-bom-stream": { - "packages": { - "gulp-watch>vinyl-file>strip-bom": true, - "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": true - } - }, - "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": { - "builtin": { - "util.inherits": true - }, - "globals": { - "Buffer.concat": true, - "setImmediate": true - }, - "packages": { - "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream": true + "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream": true } }, "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream": { @@ -5368,9 +4845,9 @@ }, "gulp>glob-watcher": { "packages": { + "chokidar": true, "gulp>glob-watcher>anymatch": true, "gulp>glob-watcher>async-done": true, - "gulp>glob-watcher>chokidar": true, "gulp>glob-watcher>is-negated-glob": true, "gulp>glob-watcher>just-debounce": true, "gulp>undertaker>object.defaults": true @@ -5381,390 +4858,585 @@ "path.sep": true }, "packages": { - "gulp>glob-watcher>anymatch>micromatch": true, - "gulp>glob-watcher>anymatch>normalize-path": true + "gulp-watch>anymatch>normalize-path": true, + "gulp>glob-watcher>anymatch>micromatch": true + } + }, + "gulp>glob-watcher>anymatch>micromatch": { + "builtin": { + "path.basename": true, + "path.sep": true, + "util.inspect": true + }, + "globals": { + "process.platform": true + }, + "packages": { + "@babel/register>clone-deep>kind-of": true, + "gulp-zip>plugin-error>arr-diff": true, + "gulp-zip>plugin-error>extend-shallow": true, + "gulp>glob-watcher>anymatch>micromatch>braces": true, + "gulp>glob-watcher>anymatch>micromatch>define-property": true, + "gulp>glob-watcher>anymatch>micromatch>extglob": true, + "gulp>gulp-cli>liftoff>fined>object.pick": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, + "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, + "gulp>gulp-cli>matchdep>micromatch>nanomatch": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces": { + "packages": { + "gulp-watch>anymatch>micromatch>braces>repeat-element": true, + "gulp>glob-watcher>anymatch>micromatch>braces>extend-shallow": true, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node": true, + "gulp>gulp-cli>matchdep>micromatch>braces>split-string": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true, + "gulp>undertaker>arr-flatten": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range": { + "builtin": { + "util.inspect": true + }, + "packages": { + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>extend-shallow": true, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number": true, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>to-regex-range": true, + "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>to-regex-range": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number": true, + "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>define-property": { + "packages": { + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, + "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": { + "globals": { + "__filename": true + }, + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>extglob>expand-brackets>posix-character-classes": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": { + "builtin": { + "fs.SyncWriteStream": true, + "net.Socket": true, + "tty.WriteStream": true, + "tty.isatty": true, + "util": true + }, + "globals": { + "chrome": true, + "console": true, + "document": true, + "localStorage": true, + "navigator": true, + "process": true + }, + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug>ms": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>async-done": { + "globals": { + "process.nextTick": true + }, + "packages": { + "@metamask/object-multiplex>once": true, + "duplexify>end-of-stream": true, + "gulp>glob-watcher>async-done>stream-exhaust": true, + "readable-stream-2>process-nextick-args": true + } + }, + "gulp>glob-watcher>async-done>stream-exhaust": { + "builtin": { + "stream.Writable": true, + "util.inherits": true + }, + "globals": { + "setImmediate": true + } + }, + "gulp>glob-watcher>chokidar": { + "packages": { + "gulp>glob-watcher>chokidar>fsevents": true + } + }, + "gulp>glob-watcher>chokidar>fsevents": { + "builtin": { + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "console.assert": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true + }, + "packages": { + "gulp-watch>chokidar>fsevents>node-pre-gyp": true + } + }, + "gulp>glob-watcher>just-debounce": { + "globals": { + "clearTimeout": true, + "setTimeout": true + } + }, + "gulp>gulp-cli>liftoff>fined>object.pick": { + "packages": { + "gulp>gulp-cli>isobject": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node": { + "packages": { + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>define-property": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util>kind-of": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>split-string": { + "packages": { + "gulp-zip>plugin-error>extend-shallow": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": { + "packages": { + "@babel/register>clone-deep>kind-of": true, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": { + "packages": { + "@babel/register>clone-deep>kind-of": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": { + "packages": { + "@babel/register>clone-deep>kind-of": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>fragment-cache": { + "packages": { + "gulp>gulp-cli>liftoff>fined>parse-filepath>map-cache": true } }, - "gulp>glob-watcher>anymatch>micromatch": { + "gulp>gulp-cli>matchdep>micromatch>nanomatch": { "builtin": { "path.basename": true, "path.sep": true, "util.inspect": true }, - "globals": { - "process.platform": true - }, "packages": { "@babel/register>clone-deep>kind-of": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, "gulp-zip>plugin-error>arr-diff": true, "gulp-zip>plugin-error>extend-shallow": true, - "gulp>glob-watcher>anymatch>micromatch>define-property": true, - "gulp>glob-watcher>anymatch>micromatch>extglob": true, - "gulp>glob-watcher>chokidar>braces": true, "gulp>gulp-cli>liftoff>fined>object.pick": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>nanomatch": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true + "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true, + "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": true } }, - "gulp>glob-watcher>anymatch>micromatch>define-property": { + "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": { "packages": { "gulp>gulp-cli>isobject": true, "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob": { - "packages": { - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": { + "gulp>gulp-cli>matchdep>micromatch>regex-not": { "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + "gulp-zip>plugin-error>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon": { + "builtin": { + "fs.readFileSync": true, + "path.dirname": true, + "util.inspect": true + }, "globals": { "__filename": true }, "packages": { - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": true, - "gulp>gulp-cli>matchdep>micromatch>extglob>expand-brackets>posix-character-classes": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true + "gulp>gulp-cli>liftoff>fined>parse-filepath>map-cache": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>debug": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>source-map": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>use": true, + "resolve-url-loader>rework>css>source-map-resolve": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base": { "builtin": { - "fs.SyncWriteStream": true, - "net.Socket": true, - "tty.WriteStream": true, - "tty.isatty": true, - "util": true + "util.inherits": true }, - "globals": { - "chrome": true, - "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true + "packages": { + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>component-emitter": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>pascalcase": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base": { + "packages": { + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>union-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>component-emitter": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>map-visit": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>map-visit": { + "builtin": { + "util.inspect": true }, "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug>ms": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": true + "gulp>gulp-cli>isobject": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>kind-of": true + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>kind-of": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number": { "packages": { - "browserify>insert-module-globals>is-buffer": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true + "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>kind-of": { "packages": { "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value": { "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "@babel/register>clone-deep>is-plain-object": true, + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, + "gulp>gulp-cli>matchdep>micromatch>braces>split-string": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value>extend-shallow": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value>extend-shallow": { "packages": { "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true } }, - "gulp>glob-watcher>anymatch>normalize-path": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path": { "packages": { - "vinyl>remove-trailing-separator": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path>kind-of": true } }, - "gulp>glob-watcher>async-done": { - "globals": { - "process.nextTick": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path>kind-of": { "packages": { - "@metamask/object-multiplex>once": true, - "duplexify>end-of-stream": true, - "gulp>glob-watcher>async-done>stream-exhaust": true, - "readable-stream-2>process-nextick-args": true + "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>async-done>stream-exhaust": { - "builtin": { - "stream.Writable": true, - "util.inherits": true - }, - "globals": { - "setImmediate": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>union-value": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, + "gulp-zip>plugin-error>arr-union": true, + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value": true } }, - "gulp>glob-watcher>chokidar": { - "builtin": { - "events.EventEmitter": true, - "fs": true, - "path.basename": true, - "path.dirname": true, - "path.extname": true, - "path.join": true, - "path.relative": true, - "path.resolve": true, - "path.sep": true - }, - "globals": { - "clearTimeout": true, - "console.error": true, - "process.env.CHOKIDAR_INTERVAL": true, - "process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR": true, - "process.env.CHOKIDAR_USEPOLLING": true, - "process.nextTick": true, - "process.platform": true, - "setTimeout": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value": { "packages": { - "chokidar>normalize-path": true, - "del>is-glob": true, - "eslint>glob-parent": true, - "gulp-watch>chokidar>async-each": true, - "gulp-watch>path-is-absolute": true, - "gulp>glob-watcher>anymatch": true, - "gulp>glob-watcher>chokidar>braces": true, - "gulp>glob-watcher>chokidar>fsevents": true, - "gulp>glob-watcher>chokidar>is-binary-path": true, - "gulp>glob-watcher>chokidar>readdirp": true, - "gulp>glob-watcher>chokidar>upath": true, - "pumpify>inherits": true + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value": true } }, - "gulp>glob-watcher>chokidar>braces": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value": { "packages": { - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>repeat-element": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>snapdragon-node": true, - "gulp-watch>chokidar>braces>split-string": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>glob-watcher>chokidar>braces>extend-shallow": true, - "gulp>glob-watcher>chokidar>braces>fill-range": true, - "gulp>gulp-cli>isobject": true, - "gulp>undertaker>arr-flatten": true + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>has-values": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>isobject": true } }, - "gulp>glob-watcher>chokidar>braces>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>isobject": { "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>isobject>isarray": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils": { "builtin": { - "util.inspect": true + "util": true }, "packages": { - "gulp>glob-watcher>chokidar>braces>fill-range>extend-shallow": true, - "gulp>glob-watcher>chokidar>braces>fill-range>is-number": true, - "gulp>glob-watcher>chokidar>braces>fill-range>to-regex-range": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + "gulp-zip>plugin-error>arr-union": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend": { + "builtin": { + "util.inherits": true + }, "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>is-number": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy": { "packages": { - "gulp>glob-watcher>chokidar>braces>fill-range>is-number>kind-of": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy>copy-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy>kind-of": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>is-number>kind-of": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy>kind-of": { "packages": { "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>to-regex-range": { - "packages": { - "gulp>glob-watcher>chokidar>braces>fill-range>is-number": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true - } - }, - "gulp>glob-watcher>chokidar>fsevents": { - "builtin": { - "events.EventEmitter": true, - "fs.stat": true, - "path.join": true, - "util.inherits": true - }, - "globals": { - "__dirname": true, - "console.assert": true, - "process.nextTick": true, - "process.platform": true, - "setImmediate": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>define-property": { "packages": { - "gulp-watch>chokidar>fsevents>node-pre-gyp": true + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true } }, - "gulp>glob-watcher>chokidar>is-binary-path": { - "builtin": { - "path.extname": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep": { "packages": { - "gulp>glob-watcher>chokidar>is-binary-path>binary-extensions": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep>is-extendable": true, + "gulp>undertaker>object.reduce>for-own>for-in": true } }, - "gulp>glob-watcher>chokidar>readdirp": { - "builtin": { - "path.join": true, - "path.relative": true, - "util.inherits": true - }, - "globals": { - "setImmediate": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep>is-extendable": { "packages": { - "del>graceful-fs": true, - "gulp>glob-watcher>anymatch>micromatch": true, - "gulp>glob-watcher>chokidar>readdirp>readable-stream": true + "@babel/register>clone-deep>is-plain-object": true } }, - "gulp>glob-watcher>chokidar>readdirp>readable-stream": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>debug": { "builtin": { - "events.EventEmitter": true, - "stream": true, + "fs.SyncWriteStream": true, + "net.Socket": true, + "tty.WriteStream": true, + "tty.isatty": true, "util": true }, "globals": { - "process.browser": true, - "process.env.READABLE_STREAM": true, - "process.stderr": true, - "process.stdout": true, - "process.version.slice": true, - "setImmediate": true + "chrome": true, + "console": true, + "document": true, + "localStorage": true, + "navigator": true, + "process": true }, "packages": { - "gulp>glob-watcher>chokidar>readdirp>readable-stream>isarray": true, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>safe-buffer": true, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>string_decoder": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true - } - }, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>safe-buffer": { - "builtin": { - "buffer": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>debug>ms": true } }, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>string_decoder": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": { "packages": { - "gulp>glob-watcher>chokidar>readdirp>readable-stream>safe-buffer": true - } - }, - "gulp>glob-watcher>chokidar>upath": { - "builtin": { - "path": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor": true } }, - "gulp>glob-watcher>just-debounce": { - "globals": { - "clearTimeout": true, - "setTimeout": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-accessor-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>kind-of": true } }, - "gulp>gulp-cli>liftoff>fined>object.pick": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-accessor-descriptor": { "packages": { - "gulp>gulp-cli>isobject": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true } }, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor": { "packages": { - "@babel/register>clone-deep>kind-of": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true } }, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": { "packages": { - "@babel/register>clone-deep>kind-of": true + "browserify>insert-module-globals>is-buffer": true } }, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>extend-shallow": { "packages": { - "@babel/register>clone-deep>kind-of": true + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true } }, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>use": { "packages": { - "gulp-watch>chokidar>braces>snapdragon>map-cache": true + "@babel/register>clone-deep>kind-of": true } }, - "gulp>gulp-cli>matchdep>micromatch>nanomatch": { - "builtin": { - "path.basename": true, - "path.sep": true, - "util.inspect": true - }, + "gulp>gulp-cli>matchdep>micromatch>to-regex": { "packages": { - "@babel/register>clone-deep>kind-of": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp-zip>plugin-error>arr-diff": true, "gulp-zip>plugin-error>extend-shallow": true, - "gulp>gulp-cli>liftoff>fined>object.pick": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": true, "gulp>gulp-cli>matchdep>micromatch>regex-not": true, - "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": true + "gulp>gulp-cli>matchdep>micromatch>to-regex>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex": true } }, - "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": { + "gulp>gulp-cli>matchdep>micromatch>to-regex>define-property": { "packages": { "gulp>gulp-cli>isobject": true, "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true } }, - "gulp>gulp-cli>matchdep>micromatch>regex-not": { + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex": { "packages": { - "gulp-watch>chokidar>braces>to-regex>safe-regex": true, - "gulp-zip>plugin-error>extend-shallow": true + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex>ret": true } }, "gulp>gulp-cli>replace-homedir>is-absolute": { @@ -6505,9 +6177,9 @@ "packages": { "del>graceful-fs": true, "gulp-sourcemaps>convert-source-map": true, + "gulp-watch>anymatch>normalize-path": true, "gulp>vinyl-fs>remove-bom-buffer": true, "gulp>vinyl-fs>vinyl-sourcemap>append-buffer": true, - "gulp>vinyl-fs>vinyl-sourcemap>normalize-path": true, "gulp>vinyl-fs>vinyl-sourcemap>now-and-later": true, "vinyl": true } @@ -6528,11 +6200,6 @@ "buffer.Buffer.isBuffer": true } }, - "gulp>vinyl-fs>vinyl-sourcemap>normalize-path": { - "packages": { - "vinyl>remove-trailing-separator": true - } - }, "gulp>vinyl-fs>vinyl-sourcemap>now-and-later": { "packages": { "@metamask/object-multiplex>once": true diff --git a/package.json b/package.json index f1c90da5fc8a..f9dbb8dbeb77 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "attributions:generate": "./development/generate-attributions.sh" }, "resolutions": { + "chokidar": "^3.6.0", "simple-update-notifier@^1.0.0": "^2.0.0", "@babel/core": "patch:@babel/core@npm%3A7.23.2#~/.yarn/patches/@babel-core-npm-7.23.2-b93f586907.patch", "@types/react": "^16.9.53", @@ -288,7 +289,7 @@ "@metamask/address-book-controller": "^4.0.1", "@metamask/announcement-controller": "^6.1.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "^33.0.0", + "@metamask/assets-controllers": "^34.0.0", "@metamask/base-controller": "^5.0.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", @@ -528,7 +529,7 @@ "bify-module-groups": "^2.0.0", "browserify": "^17.0.0", "chalk": "^4.1.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "concurrently": "^8.2.2", "copy-webpack-plugin": "^12.0.2", "cross-spawn": "^7.0.3", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index b45cf79a6e8f..fe6579bfab73 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -30,6 +30,8 @@ "phishing-detection.api.cx.metamask.io", "portfolio.metamask.io", "price.api.cx.metamask.io", + "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "proxy.api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index e02c992bbbba..6ac9cfd4245e 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -1,5 +1,6 @@ import { CHAIN_IDS } from './network'; +// TODO read from feature flags export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.MAINNET, CHAIN_IDS.BSC, @@ -11,3 +12,11 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.BASE, ]; + +const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; +const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; +export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS + ? BRIDGE_DEV_API_BASE_URL + : BRIDGE_PROD_API_BASE_URL; + +export const BRIDGE_CLIENT_ID = 'extension'; diff --git a/shared/constants/methods-tags.ts b/shared/constants/methods-tags.ts index a770fd733cd2..89cce5f67c8d 100644 --- a/shared/constants/methods-tags.ts +++ b/shared/constants/methods-tags.ts @@ -12,6 +12,8 @@ export const methodsRequiringNetworkSwitch = [ 'wallet_switchEthereumChain', 'wallet_addEthereumChain', 'wallet_watchAsset', + 'eth_signTypedData', + 'eth_signTypedData_v3', 'eth_signTypedData_v4', 'personal_sign', ] as const; diff --git a/shared/constants/multichain/assets.ts b/shared/constants/multichain/assets.ts index b4b2a74f6c22..988a9fbad624 100644 --- a/shared/constants/multichain/assets.ts +++ b/shared/constants/multichain/assets.ts @@ -3,3 +3,8 @@ import { MultichainNetworks } from './networks'; export const MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 = { BTC: `${MultichainNetworks.BITCOIN}/slip44:0`, } as const; + +export enum MultichainNativeAssets { + BITCOIN = `${MultichainNetworks.BITCOIN}/slip44:0`, + BITCOIN_TESTNET = `${MultichainNetworks.BITCOIN_TESTNET}/slip44:0`, +} diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index de5e50639374..7f629ca49c6b 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -1,12 +1,17 @@ import { ProviderConfig } from '@metamask/network-controller'; import { CaipChainId } from '@metamask/utils'; +import { isBtcMainnetAddress, isBtcTestnetAddress } from '../../lib/multichain'; export type ProviderConfigWithImageUrl = Omit & { rpcPrefs?: { imageUrl?: string }; }; export type MultichainProviderConfig = ProviderConfigWithImageUrl & { + nickname: string; chainId: CaipChainId; + // NOTE: For now we use a callback to check if the address is compatible with + // the given network or not + isAddressCompatible: (address: string) => boolean; }; export enum MultichainNetworks { @@ -34,5 +39,18 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< rpcPrefs: { imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], }, + isAddressCompatible: isBtcMainnetAddress, + }, + [MultichainNetworks.BITCOIN_TESTNET]: { + chainId: MultichainNetworks.BITCOIN_TESTNET, + rpcUrl: '', // not used + ticker: 'BTC', + nickname: 'Bitcoin (testnet)', + id: 'btc-testnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], + }, + isAddressCompatible: isBtcTestnetAddress, }, }; diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 452f0584ffaa..955c9b2decc2 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -66,22 +66,6 @@ export type RPCDefinition = { rpcPrefs: RPCPreferences; }; -/** - * For each chain that we support fiat onramps for, we provide a set of - * configuration options that help for initializing the connectiong to the - * onramp providers. - */ -type BuyableChainSettings = { - /** - * The native currency for the given chain - */ - nativeCurrency: CurrencySymbol | TestNetworkCurrencySymbol; - /** - * The network name or identifier - */ - network: string; -}; - /** * Throughout the extension we set the current provider by referencing its * "type", which can be any of the values in the below object. These values @@ -296,6 +280,7 @@ export const CURRENCY_SYMBOLS = { AVALANCHE: 'AVAX', BNB: 'BNB', BUSD: 'BUSD', + BTC: 'BTC', // Do we wanna mix EVM and non-EVM here? CELO: 'CELO', DAI: 'DAI', GNOSIS: 'XDAI', @@ -908,108 +893,6 @@ export const UNSUPPORTED_RPC_METHODS = new Set([ export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link'; -// The first item in transakCurrencies must be the -// default crypto currency for the network -const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum'; - -export const BUYABLE_CHAINS_MAP: { - [K in Exclude< - ChainId, - | typeof CHAIN_IDS.LOCALHOST - | typeof CHAIN_IDS.OPTIMISM_TESTNET - | typeof CHAIN_IDS.OPTIMISM_GOERLI - | typeof CHAIN_IDS.BASE_TESTNET - | typeof CHAIN_IDS.OPBNB_TESTNET - | typeof CHAIN_IDS.OPBNB - | typeof CHAIN_IDS.BSC_TESTNET - | typeof CHAIN_IDS.POLYGON_TESTNET - | typeof CHAIN_IDS.AVALANCHE_TESTNET - | typeof CHAIN_IDS.FANTOM_TESTNET - | typeof CHAIN_IDS.MOONBEAM_TESTNET - | typeof CHAIN_IDS.LINEA_GOERLI - | typeof CHAIN_IDS.LINEA_SEPOLIA - | typeof CHAIN_IDS.GOERLI - | typeof CHAIN_IDS.SEPOLIA - | typeof CHAIN_IDS.GNOSIS - | typeof CHAIN_IDS.AURORA - | typeof CHAIN_IDS.ARBITRUM_GOERLI - | typeof CHAIN_IDS.BLAST - | typeof CHAIN_IDS.FILECOIN - | typeof CHAIN_IDS.POLYGON_ZKEVM - | typeof CHAIN_IDS.SCROLL - | typeof CHAIN_IDS.SCROLL_SEPOLIA - | typeof CHAIN_IDS.WETHIO - | typeof CHAIN_IDS.CHZ - | typeof CHAIN_IDS.NUMBERS - | typeof CHAIN_IDS.SEI - >]: BuyableChainSettings; -} = { - [CHAIN_IDS.MAINNET]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME, - }, - [CHAIN_IDS.BSC]: { - nativeCurrency: CURRENCY_SYMBOLS.BNB, - network: 'bsc', - }, - [CHAIN_IDS.POLYGON]: { - nativeCurrency: CURRENCY_SYMBOLS.MATIC, - network: 'polygon', - }, - [CHAIN_IDS.AVALANCHE]: { - nativeCurrency: CURRENCY_SYMBOLS.AVALANCHE, - network: 'avaxcchain', - }, - [CHAIN_IDS.FANTOM]: { - nativeCurrency: CURRENCY_SYMBOLS.FANTOM, - network: 'fantom', - }, - [CHAIN_IDS.CELO]: { - nativeCurrency: CURRENCY_SYMBOLS.CELO, - network: 'celo', - }, - [CHAIN_IDS.OPTIMISM]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'optimism', - }, - [CHAIN_IDS.ARBITRUM]: { - nativeCurrency: CURRENCY_SYMBOLS.ARBITRUM, - network: 'arbitrum', - }, - [CHAIN_IDS.CRONOS]: { - nativeCurrency: CURRENCY_SYMBOLS.CRONOS, - network: 'cronos', - }, - [CHAIN_IDS.MOONBEAM]: { - nativeCurrency: CURRENCY_SYMBOLS.GLIMMER, - network: 'moonbeam', - }, - [CHAIN_IDS.MOONRIVER]: { - nativeCurrency: CURRENCY_SYMBOLS.MOONRIVER, - network: 'moonriver', - }, - [CHAIN_IDS.HARMONY]: { - nativeCurrency: CURRENCY_SYMBOLS.ONE, - network: 'harmony', - }, - [CHAIN_IDS.PALM]: { - nativeCurrency: CURRENCY_SYMBOLS.PALM, - network: 'palm', - }, - [CHAIN_IDS.LINEA_MAINNET]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'linea', - }, - [CHAIN_IDS.ZKSYNC_ERA]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'zksync', - }, - [CHAIN_IDS.BASE]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'base', - }, -}; - export const FEATURED_RPCS: RPCDefinition[] = [ { chainId: CHAIN_IDS.ARBITRUM, diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts new file mode 100644 index 000000000000..6c59f506e721 --- /dev/null +++ b/shared/lib/multichain.test.ts @@ -0,0 +1,49 @@ +import { isBtcMainnetAddress, isBtcTestnetAddress } from './multichain'; + +const MAINNET_ADDRESSES = [ + // P2WPKH + 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', + // P2PKH + '1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ', +]; + +const TESTNET_ADDRESSES = [ + // P2WPKH + 'tb1q6rmsq3vlfdhjdhtkxlqtuhhlr6pmj09y6w43g8', +]; + +const ETH_ADDRESSES = ['0x6431726EEE67570BF6f0Cf892aE0a3988F03903F']; + +describe('multichain', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(MAINNET_ADDRESSES)( + 'returns true if address is compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...TESTNET_ADDRESSES, ...ETH_ADDRESSES])( + 'returns false if address is not compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(false); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(TESTNET_ADDRESSES)( + 'returns true if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...MAINNET_ADDRESSES, ...ETH_ADDRESSES])( + 'returns false if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(false); + }, + ); +}); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts new file mode 100644 index 000000000000..07466e439266 --- /dev/null +++ b/shared/lib/multichain.ts @@ -0,0 +1,31 @@ +import { isEthAddress } from '../../app/scripts/lib/multichain/address'; + +/** + * Returns whether an address is on the Bitcoin mainnet. + * + * This function only checks the prefix of the address to determine if it's on + * the mainnet or not. It doesn't validate the address itself, and should only + * be used as a temporary solution until this information is included in the + * account object. + * + * @param address - The address to check. + * @returns `true` if the address is on the Bitcoin mainnet, `false` otherwise. + */ +export function isBtcMainnetAddress(address: string): boolean { + return ( + !isEthAddress(address) && + (address.startsWith('bc1') || address.startsWith('1')) + ); +} + +/** + * Returns whether an address is on the Bitcoin testnet. + * + * See {@link isBtcMainnetAddress} for implementation details. + * + * @param address - The address to check. + * @returns `true` if the address is on the Bitcoin testnet, `false` otherwise. + */ +export function isBtcTestnetAddress(address: string): boolean { + return !isEthAddress(address) && !isBtcMainnetAddress(address); +} diff --git a/shared/lib/transactions-controller-utils.js b/shared/lib/transactions-controller-utils.js index 425ff33b6f32..073ff922af67 100644 --- a/shared/lib/transactions-controller-utils.js +++ b/shared/lib/transactions-controller-utils.js @@ -9,6 +9,9 @@ export const TOKEN_TRANSFER_LOG_TOPIC_HASH = export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract'; +export const TRANSFER_SINFLE_LOG_TOPIC_HASH = + '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62'; + export const TEN_SECONDS_IN_MILLISECONDS = 10_000; export function calcGasTotal(gasLimit = '0', gasPrice = '0') { diff --git a/sonar-project.properties b/sonar-project.properties index de14094b965e..0455fa9634e2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,18 +1,14 @@ -sonar.projectKey=metamask-extension -sonar.organization=consensys +sonar.projectKey=metamask-extension-private +sonar.organization=metamask -# This is the name and version displayed in the SonarCloud UI. -sonar.projectName=MetaMask Extension -#sonar.projectVersion=1.0 +# Source +sonar.sources=app,development,offscreen,shared,types,ui +sonar.exclusions=**/*.test.**,**/*.spec.**,app/images -# Root for sonar analysis. -sonar.sources=app/ +# Tests +sonar.tests=app,test,development,offscreen,shared,types,ui +sonar.test.inclusions=**/*.test.**,**/*.spec.** +sonar.javascript.lcov.reportPaths=tests/coverage/lcov.info -# Excluded project files from analysis. -#sonar.exclusions= - -# Inclusions for test files. -sonar.test.inclusions=**.test.** - -# Encoding of the source code. Default is default system encoding -sonar.sourceEncoding=UTF-8 +# Fail CI job if quality gate failures +sonar.qualitygate.wait=false \ No newline at end of file diff --git a/test/data/confirmations/personal_sign.ts b/test/data/confirmations/personal_sign.ts index bac5b3f9838a..6ac82dfe57bb 100644 --- a/test/data/confirmations/personal_sign.ts +++ b/test/data/confirmations/personal_sign.ts @@ -35,15 +35,20 @@ export const signatureRequestSIWE = { siwe: { isSIWEMessage: true, parsedMessage: { - domain: 'metamask.github.io', address: '0x935e73edb9ff52e23bac7f7e049a1ecd06d05477', + chainId: 1, + domain: 'metamask.github.io', + expirationTime: null, + issuedAt: '2021-09-30T16:25:24.000Z', + nonce: '32891757', + notBefore: '2022-03-17T12:45:13.610Z', + requestId: 'some_id', + scheme: null, statement: 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', uri: 'https://metamask.github.io', version: '1', - chainId: 1, - nonce: '32891757', - issuedAt: '2021-09-30T16:25:24.000Z', + resources: null, }, }, }, @@ -67,17 +72,19 @@ export const SignatureRequestSIWEWithResources = { siwe: { isSIWEMessage: true, parsedMessage: { - domain: 'metamask.github.io', address: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', - statement: - 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', - uri: 'https://metamask.github.io', - version: '1', chainId: 1, - nonce: '32891757', + domain: 'metamask.github.io', + expirationTime: null, issuedAt: '2021-09-30T16:25:24.000Z', + nonce: '32891757', notBefore: '2022-03-17T12:45:13.610Z', requestId: 'some_id', + scheme: null, + statement: + 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', + uri: 'https://metamask.github.io', + version: '1', resources: [ 'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu', 'https://example.com/my-web2-claim.json', diff --git a/test/data/mock-accounts.ts b/test/data/mock-accounts.ts index ff6009ebd555..2273915f7a5f 100644 --- a/test/data/mock-accounts.ts +++ b/test/data/mock-accounts.ts @@ -40,7 +40,7 @@ export const MOCK_ACCOUNT_ERC4337: InternalAccount = { export const MOCK_ACCOUNT_BIP122_P2WPKH: InternalAccount = { id: 'ae247df6-3911-47f7-9e36-28e6a7d96078', - address: 'bc1qaabb', + address: 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', options: {}, methods: [BtcMethod.SendMany], type: BtcAccountType.P2wpkh, @@ -52,8 +52,23 @@ export const MOCK_ACCOUNT_BIP122_P2WPKH: InternalAccount = { }, }; +export const MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET: InternalAccount = { + id: 'fcdafe8b-4bdf-4e25-9051-e255b2a0af5f', + address: 'tb1q6rmsq3vlfdhjdhtkxlqtuhhlr6pmj09y6w43g8', + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, + metadata: { + name: 'Bitcoin Testnet Account', + keyring: { type: KeyringTypes.snap }, + importTime: 1691565967600, + lastSelected: 1955565967656, + }, +}; + export const MOCK_ACCOUNTS = { [MOCK_ACCOUNT_EOA.id]: MOCK_ACCOUNT_EOA, [MOCK_ACCOUNT_ERC4337.id]: MOCK_ACCOUNT_ERC4337, [MOCK_ACCOUNT_BIP122_P2WPKH.id]: MOCK_ACCOUNT_BIP122_P2WPKH, + [MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET.id]: MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, }; diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index c8604c96c055..5beee30824ce 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -1242,6 +1242,143 @@ ], "swapsState": {} }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "shortName": "Ethereum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "shortName": "Optimism", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "shortName": "Cronos", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "shortName": "BNB Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 100, + "chainName": "Gnosis Mainnet", + "shortName": "Gnosis", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "shortName": "Polygon", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "shortName": "Fantom", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 324, + "chainName": "zkSync Era Mainnet", + "shortName": "zkSync Era", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1101, + "chainName": "Polygon zkEVM", + "shortName": "Polygon zkEVM", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1284, + "chainName": "Moonbeam Mainnet", + "shortName": "Moonbeam", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "shortName": "Moonriver", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 8453, + "chainName": "Base Mainnet", + "shortName": "Base", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "shortName": "Arbitrum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "shortName": "Celo", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "shortName": "Avalanche C-Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 59144, + "chainName": "Linea", + "shortName": "Linea", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "shortName": "Aurora", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "shortName": "Harmony (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm Mainnet", + "shortName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": "1-tx", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 920533023a58..9e595ad8f0e0 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1943,6 +1943,143 @@ } } }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "shortName": "Ethereum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "shortName": "Optimism", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "shortName": "Cronos", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "shortName": "BNB Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 100, + "chainName": "Gnosis Mainnet", + "shortName": "Gnosis", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "shortName": "Polygon", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "shortName": "Fantom", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 324, + "chainName": "zkSync Era Mainnet", + "shortName": "zkSync Era", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1101, + "chainName": "Polygon zkEVM", + "shortName": "Polygon zkEVM", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1284, + "chainName": "Moonbeam Mainnet", + "shortName": "Moonbeam", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "shortName": "Moonriver", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 8453, + "chainName": "Base Mainnet", + "shortName": "Base", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "shortName": "Arbitrum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "shortName": "Celo", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "shortName": "Avalanche C-Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 59144, + "chainName": "Linea", + "shortName": "Linea", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "shortName": "Aurora", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "shortName": "Harmony (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm Mainnet", + "shortName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": null, diff --git a/test/e2e/changedFilesUtil.js b/test/e2e/changedFilesUtil.js new file mode 100644 index 000000000000..5ead76203db0 --- /dev/null +++ b/test/e2e/changedFilesUtil.js @@ -0,0 +1,44 @@ +const fs = require('fs').promises; +const path = require('path'); + +const BASE_PATH = path.resolve(__dirname, '..', '..'); +const CHANGED_FILES_PATH = path.join( + BASE_PATH, + 'changed-files', + 'changed-files.txt', +); + +/** + * Reads the list of changed files from the git diff file. + * + * @returns {Promise} An array of changed file paths. + */ +async function readChangedFiles() { + try { + const data = await fs.readFile(CHANGED_FILES_PATH, 'utf8'); + const changedFiles = data.split('\n'); + return changedFiles; + } catch (error) { + console.error('Error reading from file:', error); + return ''; + } +} + +/** + * Filters the list of changed files to include only E2E test files within the 'test/e2e/' directory. + * + * @returns {Promise} An array of filtered E2E test file paths. + */ +async function filterE2eChangedFiles() { + const changedFiles = await readChangedFiles(); + const e2eChangedFiles = changedFiles + .filter( + (file) => + file.startsWith('test/e2e/') && + (file.endsWith('.spec.js') || file.endsWith('.spec.ts')), + ) + .map((file) => `${BASE_PATH}/${file}`); + return e2eChangedFiles; +} + +module.exports = { filterE2eChangedFiles }; diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 41951f973aa9..ba32c94cabec 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -664,6 +664,7 @@ const closeSRPReveal = async (driver) => { const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; const DAPP_ONE_URL = 'http://127.0.0.1:8081'; +const DAPP_TWO_URL = 'http://127.0.0.1:8082'; const openDapp = async (driver, contract = null, dappURL = DAPP_URL) => { return contract @@ -1121,6 +1122,7 @@ module.exports = { DAPP_HOST_ADDRESS, DAPP_URL, DAPP_ONE_URL, + DAPP_TWO_URL, TEST_SEED_PHRASE, TEST_SEED_PHRASE_TWO, PRIVATE_KEY, diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index f3279377114d..50bd7633f8d3 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -629,6 +629,15 @@ async function setupMocking( return [...privacyReport].sort(); } + /** + * Excludes hosts from the privacyReport if they are refered to by the MetaMask Portfolio + * in a different tab. This is because the Portfolio is a separate application + * + * @param request + */ + const portfolioRequestsMatcher = (request) => + request.headers.referer === 'https://portfolio.metamask.io/'; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -638,7 +647,10 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { - if (request.headers.host.match(browserAPIRequestDomains) === null) { + if ( + request.headers.host.match(browserAPIRequestDomains) === null && + !portfolioRequestsMatcher(request) + ) { privacyReport.add(request.headers.host); } }); diff --git a/test/e2e/run-all.js b/test/e2e/run-all.js index 0ff043261a7b..d52a37e9afe6 100644 --- a/test/e2e/run-all.js +++ b/test/e2e/run-all.js @@ -6,6 +6,7 @@ const { hideBin } = require('yargs/helpers'); const { runInShell } = require('../../development/lib/run-command'); const { exitWithError } = require('../../development/lib/exit-with-error'); const { loadBuildTypesConfig } = require('../../development/lib/build-type'); +const { filterE2eChangedFiles } = require('./changedFilesUtil'); // These tests should only be run on Flask for now. const FLASK_ONLY_TESTS = ['test-snap-namelookup.spec.js']; @@ -30,9 +31,47 @@ const getTestPathsForTestDir = async (testDir) => { return testPaths; }; +// Quality Gate Retries +const RETRIES_FOR_NEW_OR_CHANGED_TESTS = 5; + +/** + * Runs the quality gate logic to filter and append changed or new tests if present. + * + * @param {string} fullTestList - List of test paths to be considered. + * @param {string[]} changedOrNewTests - List of changed or new test paths. + * @returns {string} The updated full test list. + */ +async function applyQualityGate(fullTestList, changedOrNewTests) { + let qualityGatedList = fullTestList; + + if (changedOrNewTests.length > 0) { + // Filter to include only the paths present in fullTestList + const filteredTests = changedOrNewTests.filter((test) => + fullTestList.includes(test), + ); + + // If there are any filtered tests, append them to fullTestList + if (filteredTests.length > 0) { + const filteredTestsString = filteredTests.join('\n'); + for (let i = 0; i < RETRIES_FOR_NEW_OR_CHANGED_TESTS; i++) { + qualityGatedList += `\n${filteredTestsString}`; + } + } + } + + return qualityGatedList; +} + // For running E2Es in parallel in CI -function runningOnCircleCI(testPaths) { - const fullTestList = testPaths.join('\n'); +async function runningOnCircleCI(testPaths) { + const changedOrNewTests = await filterE2eChangedFiles(); + console.log('Changed or new test list:', changedOrNewTests); + + const fullTestList = await applyQualityGate( + testPaths.join('\n'), + changedOrNewTests, + ); + console.log('Full test list:', fullTestList); fs.writeFileSync('test/test-results/fullTestList.txt', fullTestList); @@ -46,7 +85,7 @@ function runningOnCircleCI(testPaths) { // Report if no tests found, exit gracefully if (result.indexOf('There were no tests found') !== -1) { console.log(`run-all.js info: Skipping this node because "${result}"`); - return []; + return { fullTestList: [] }; } // If there's no text file, it means this node has no tests, so exit gracefully @@ -54,13 +93,15 @@ function runningOnCircleCI(testPaths) { console.log( 'run-all.js info: Skipping this node because there is no myTestList.txt', ); - return []; + return { fullTestList: [] }; } // take the space-delimited result and split into an array - return fs + const myTestList = fs .readFileSync('test/test-results/myTestList.txt', { encoding: 'utf8' }) .split(' '); + + return { fullTestList: myTestList, changedOrNewTests }; } async function main() { @@ -204,8 +245,10 @@ async function main() { await fs.promises.mkdir('test/test-results/e2e', { recursive: true }); let myTestList; + let changedOrNewTests; if (process.env.CIRCLECI) { - myTestList = runningOnCircleCI(testPaths); + ({ fullTestList: myTestList, changedOrNewTests = [] } = + await runningOnCircleCI(testPaths)); } else { myTestList = testPaths; } @@ -217,7 +260,12 @@ async function main() { if (testPath !== '') { testPath = testPath.replace('\n', ''); // sometimes there's a newline at the end of the testPath console.log(`\nExecuting testPath: ${testPath}\n`); - await runInShell('node', [...args, testPath]); + + const isTestChangedOrNew = changedOrNewTests?.includes(testPath); + const qualityGateArg = isTestChangedOrNew + ? ['--stop-after-one-failure'] + : []; + await runInShell('node', [...args, ...qualityGateArg, testPath]); } } } diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js index a4c0496dbda6..0acf0e571cdb 100644 --- a/test/e2e/run-e2e-test.js +++ b/test/e2e/run-e2e-test.js @@ -35,7 +35,7 @@ async function main() { 'Set how many times the test should be retried upon failure.', type: 'number', }) - .option('retry-until-failure', { + .option('stop-after-one-failure', { default: false, description: 'Retries until the test fails', type: 'boolean', @@ -73,7 +73,7 @@ async function main() { mmi, e2eTestPath, retries, - retryUntilFailure, + stopAfterOneFailure, leaveRunning, updateSnapshot, updatePrivacySnapshot, @@ -141,7 +141,7 @@ async function main() { const dir = 'test/test-results/e2e'; fs.mkdir(dir, { recursive: true }); - await retry({ retries, retryUntilFailure }, async () => { + await retry({ retries, stopAfterOneFailure }, async () => { await runInShell('yarn', [ 'mocha', `--config=${configFile}`, diff --git a/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts b/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts new file mode 100644 index 000000000000..24f0bc0fb233 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts @@ -0,0 +1,42 @@ +import { Suite } from 'mocha'; +import { withFixtures, logInWithBalanceValidation } from '../../helpers'; +import { Ganache } from '../../seeder/ganache'; +import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../webdriver/driver'; +import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; + +describe('Click bridge button from asset page @no-mmi', function (this: Suite) { + it('loads portfolio tab when flag is turned off', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle()), + async ({ + driver, + ganacheServer, + contractRegistry, + }: { + driver: Driver; + ganacheServer: Ganache; + contractRegistry: GanacheContractAddressRegistry; + }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + + // ETH + await bridgePage.loadAssetPage(contractRegistry); + await bridgePage.load('coin-overview'); + await bridgePage.verifyPortfolioTab( + 'https://portfolio.metamask.io/bridge?metametricsId=null', + ); + + await bridgePage.reloadHome(); + + // TST + await bridgePage.loadAssetPage(contractRegistry, 'TST'); + await bridgePage.load('token-overview'); + await bridgePage.verifyPortfolioTab( + 'https://portfolio.metamask.io/bridge?metametricsId=null', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts b/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts new file mode 100644 index 000000000000..0a6098e01592 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts @@ -0,0 +1,27 @@ +import { Suite } from 'mocha'; +import { withFixtures, logInWithBalanceValidation } from '../../helpers'; +import { Ganache } from '../../seeder/ganache'; +import { Driver } from '../../webdriver/driver'; +import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; + +describe('Click bridge button from wallet overview @no-mmi', function (this: Suite) { + it('loads portfolio tab when flag is turned off', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle()), + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer: Ganache; + }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + await bridgePage.load(); + await bridgePage.verifyPortfolioTab( + 'https://portfolio.metamask.io/bridge?metametricsId=null', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts new file mode 100644 index 000000000000..157876f43769 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -0,0 +1,128 @@ +import { strict as assert } from 'assert'; +import { Mockttp } from 'mockttp'; +import FixtureBuilder from '../../fixture-builder'; +import { + WINDOW_TITLES, + clickNestedButton, + generateGanacheOptions, +} from '../../helpers'; +import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../webdriver/driver'; + +export class BridgePage { + driver: Driver; + + constructor(driver: Driver) { + this.driver = driver; + } + + load = async ( + location: + | 'wallet-overview' + | 'coin-overview' + | 'token-overview' = 'wallet-overview', + ) => { + let bridgeButtonTestIdPrefix; + switch (location) { + case 'wallet-overview': + bridgeButtonTestIdPrefix = 'eth'; + break; + case 'coin-overview': // native asset page + bridgeButtonTestIdPrefix = 'coin'; + break; + case 'token-overview': + default: + bridgeButtonTestIdPrefix = 'token'; + } + await this.driver.clickElement( + `[data-testid="${bridgeButtonTestIdPrefix}-overview-bridge"]`, + ); + }; + + reloadHome = async (shouldCloseWindow = true) => { + if (shouldCloseWindow) { + await this.driver.closeWindow(); + await this.driver.delay(2000); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + } + await this.driver.navigate(); + }; + + loadAssetPage = async ( + contractRegistry: GanacheContractAddressRegistry, + symbol?: string, + ) => { + let tokenListItem; + + if (symbol) { + // Import token + const contractAddress = await contractRegistry.getContractAddress( + SMART_CONTRACTS.HST, + ); + await this.driver.clickElement({ + text: 'Import tokens', + tag: 'button', + }); + await clickNestedButton(this.driver, 'Custom token'); + await this.driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + contractAddress, + ); + await this.driver.waitForSelector( + '[data-testid="import-tokens-modal-custom-decimals"]', + ); + await this.driver.clickElement({ + text: 'Next', + tag: 'button', + }); + await this.driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); + await this.driver.delay(2000); + tokenListItem = await this.driver.findElement({ text: symbol }); + } else { + tokenListItem = await this.driver.findElement( + '[data-testid="multichain-token-list-button"]', + ); + } + await tokenListItem.click(); + assert.ok((await this.driver.getCurrentUrl()).includes('asset')); + }; + + verifyPortfolioTab = async (url: string) => { + await this.driver.delay(4000); + await this.driver.switchToWindowWithTitle('MetaMask Portfolio - Bridge'); + assert.equal(await this.driver.getCurrentUrl(), url); + }; + + verifySwapPage = async () => { + const currentUrl = await this.driver.getCurrentUrl(); + assert.ok(currentUrl.includes('cross-chain/swaps')); + }; +} + +export const getBridgeFixtures = ( + title?: string, + testSpecificMock?: (server: Mockttp) => Promise, +) => { + return { + driverOptions: { + openDevToolsForTabs: true, + }, + fixtures: new FixtureBuilder({ inputChainId: CHAIN_IDS.MAINNET }) + .withNetworkControllerOnMainnet() + .withTokensControllerERC20() + .build(), + testSpecificMock, + smartContract: SMART_CONTRACTS.HST, + ganacheOptions: generateGanacheOptions({ + hardfork: 'london', + chain: { chainId: CHAIN_IDS.MAINNET }, + }), + title, + }; +}; diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 84fef8e26a90..a7169143d03e 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -59,6 +59,13 @@ "approvalFlows": "object" }, "AuthenticationController": { "isSignedIn": "boolean" }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean" + } + } + }, "CronjobController": { "jobs": "object" }, "CurrencyController": { "currencyRates": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 1dd7775ec70f..a79a2e543573 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -2,6 +2,7 @@ "DNS": "object", "activeTab": "object", "appState": "object", + "bridge": "object", "confirm": "object", "confirmAlerts": "object", "confirmTransaction": "object", @@ -60,6 +61,11 @@ }, "connectedStatusPopoverHasBeenShown": true, "defaultHomeActiveTabName": null, + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean" + } + }, "browserEnvironment": { "os": "string", "browser": "string" }, "popupGasPollTokens": "object", "notificationGasPollTokens": "object", @@ -257,6 +263,7 @@ "encryptionKey": "string", "encryptionSalt": "string" }, + "ramps": "object", "send": "object", "swaps": "object", "unconnectedAccount": { "state": "CLOSED" } diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index 0cd18c13a9bf..fd133fa47e47 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -88,6 +88,14 @@ const selectors = { }, suggestedTicker: '[data-testid="network-form-ticker-suggestion"]', tickerWarning: '[data-testid="network-form-ticker-warning"]', + suggestedTickerForXDAI: { + css: '[data-testid="network-form-ticker-suggestion"]', + text: 'Suggested ticker symbol: XDAI', + }, + tickerWarningTokenSymbol: { + css: '[data-testid="network-form-ticker-warning"]', + text: "This token symbol doesn't match the network name or chain ID entered.", + }, tickerButton: { text: 'PETH', tag: 'button' }, networkAdded: { text: 'Network added successfully!', tag: 'h4' }, @@ -735,11 +743,11 @@ describe('Custom network', function () { await driver.fill(selectors.explorerInputField, 'https://test.com'); const suggestedTicker = await driver.isElementPresent( - selectors.suggestedTicker, + selectors.suggestedTickerForXDAI, ); const tickerWarning = await driver.isElementPresent( - selectors.tickerWarning, + selectors.tickerWarningTokenSymbol, ); assert.equal(suggestedTicker, false); diff --git a/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js new file mode 100644 index 000000000000..cd197970baea --- /dev/null +++ b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js @@ -0,0 +1,148 @@ +const FixtureBuilder = require('../../fixture-builder'); +const { + withFixtures, + openDapp, + unlockWallet, + DAPP_URL, + DAPP_ONE_URL, + regularDelayMs, + defaultGanacheOptions, + WINDOW_TITLES, +} = require('../../helpers'); + +describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { + it('should queue signTypedData tx after eth_sendTransaction confirmation and signTypedData confirmation should target the correct network after eth_sendTransaction is confirmed @no-mmi', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Open Dapp One + await openDapp(driver, undefined, DAPP_URL); + + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Open Dapp Two + await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect to dapp 2 + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.switchToWindowWithUrl(DAPP_URL); + + // switch chain for Dapp One + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x3e8' }], + }); + + // Initiate switchEthereumChain on Dapp one + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ text: 'Switch network', tag: 'button' }); + + await driver.switchToWindowWithUrl(DAPP_URL); + + // eth_sendTransaction request + await driver.clickElement('#sendButton'); + + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + + // signTypedData request + await driver.clickElement('#signTypedData'); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // Check correct network on the send confirmation. + await driver.findElement({ + css: '[data-testid="network-display"]', + text: 'Localhost 7777', + }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // Check correct network on the signTypedData confirmation. + await driver.findElement({ + css: '[data-testid="signature-request-network-display"]', + text: 'Localhost 8545', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/request-queuing/ui.spec.js b/test/e2e/tests/request-queuing/ui.spec.js index e85d87226e84..45429ad32263 100644 --- a/test/e2e/tests/request-queuing/ui.spec.js +++ b/test/e2e/tests/request-queuing/ui.spec.js @@ -1,4 +1,5 @@ const { strict: assert } = require('assert'); +const { Browser } = require('selenium-webdriver'); const FixtureBuilder = require('../../fixture-builder'); const { withFixtures, @@ -11,21 +12,33 @@ const { defaultGanacheOptions, switchToNotificationWindow, veryLargeDelayMs, + DAPP_TWO_URL, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); -async function openDappAndSwitchChain(driver, dappUrl, chainId) { - const notificationWindowIndex = chainId ? 4 : 3; +// Window handle adjustments will need to be made for Non-MV3 Firefox +// due to OffscreenDocument. Additionally Firefox continually bombs +// with a "NoSuchWindowError: Browsing context has been discarded" whenever +// we try to open a third dapp, so this test run in Firefox will +// validate two dapps instead of 3 +const IS_FIREFOX = process.env.SELENIUM_BROWSER === Browser.FIREFOX; +async function openDappAndSwitchChain( + driver, + dappUrl, + chainId, + notificationWindowIndex = 3, +) { // Open the dapp await openDapp(driver, undefined, dappUrl); - await driver.delay(regularDelayMs); // Connect to the dapp await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); await driver.delay(regularDelayMs); + await switchToNotificationWindow(driver, notificationWindowIndex); + await driver.clickElement({ text: 'Next', tag: 'button', @@ -62,39 +75,99 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { } } -async function selectDappClickSendGetNetwork(driver, dappUrl) { +async function selectDappClickSend(driver, dappUrl) { await driver.switchToWindowWithUrl(dappUrl); - // Windows: MetaMask, TestDapp1, TestDapp2 - const expectedWindowHandles = 3; - await driver.waitUntilXWindowHandles(expectedWindowHandles); - const currentWindowHandles = await driver.getAllWindowHandles(); await driver.clickElement('#sendButton'); +} - // Under mv3, we don't need to add to the current number of window handles - // because the offscreen document returned by getAllWindowHandles provides - // an extra window handle - const newWindowHandles = await driver.waitUntilXWindowHandles( - process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined - ? currentWindowHandles.length - : currentWindowHandles.length + 1, - ); - const [newNotificationWindowHandle] = newWindowHandles.filter( - (h) => !currentWindowHandles.includes(h), - ); - await driver.switchToWindow(newNotificationWindowHandle); +async function switchToNotificationPopoverValidateDetails( + driver, + expectedDetails, +) { + // Switches to the MetaMask Dialog window for confirmation + const windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog, windowHandles); + // Get UI details const networkPill = await driver.findElement( '[data-testid="network-display"]', ); const networkText = await networkPill.getText(); - await driver.clickElement({ css: 'button', text: 'Reject' }); - return networkText; + const originElement = await driver.findElement( + '.confirm-page-container-summary__origin bdi', + ); + const originText = await originElement.getText(); + + // Get state details + const notificationWindowState = await driver.executeScript(() => + window.stateHooks?.getCleanAppState?.(), + ); + const { chainId } = notificationWindowState.metamask.providerConfig; + + // Ensure accuracy + validateConfirmationDetails( + { networkText, originText, chainId }, + expectedDetails, + ); +} + +async function rejectTransaction(driver) { + await driver.clickElement({ tag: 'button', text: 'Reject' }); +} + +async function confirmTransaction(driver) { + await driver.clickElement({ tag: 'button', text: 'Confirm' }); +} + +function validateConfirmationDetails( + { chainId, networkText, originText }, + expected, +) { + assert.equal(chainId, expected.chainId); + assert.equal(networkText, expected.networkText); + assert.equal(originText, expected.originText); +} + +async function switchToNetworkByName(driver, networkName) { + await driver.clickElement('[data-testid="network-display"]'); + await driver.clickElement(`[data-testid="${networkName}"]`); +} + +async function validateBalanceAndActivity( + driver, + expectedBalance, + expectedActivityEntries = 1, +) { + // Ensure the balance changed if the the transaction was confirmed + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + text: expectedBalance, + }); + + // Ensure there's an activity entry of "Send" and "Confirmed" + if (expectedActivityEntries) { + await driver.clickElement('[data-testid="account-overview__activity-tab"]'); + assert.equal( + ( + await driver.findElements({ + css: '[data-testid="activity-list-item-action"]', + text: 'Send', + }) + ).length, + expectedActivityEntries, + ); + assert.equal( + (await driver.findElements('.transaction-status-label--confirmed')) + .length, + expectedActivityEntries, + ); + } } describe('Request-queue UI changes', function () { it('UI should show network specific to domain @no-mmi', async function () { const port = 8546; - const chainId = 1338; + const chainId = 1338; // 0x53a await withFixtures( { dapp: true, @@ -126,7 +199,7 @@ describe('Request-queue UI changes', function () { await openDappAndSwitchChain(driver, DAPP_URL); // Open the second dapp and switch chains - await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); + await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a', 4); // Go to wallet fullscreen, ensure that the global network changed to Ethereum Mainnet await driver.switchToWindowWithTitle( @@ -134,22 +207,151 @@ describe('Request-queue UI changes', function () { ); await driver.findElement({ css: '[data-testid="network-display"]', - text: 'Ethereum Mainnet', + text: 'Localhost 8546', }); // Go to the first dapp, ensure it uses localhost - const dappOneNetworkPillText = await selectDappClickSendGetNetwork( - driver, - DAPP_URL, - ); - assert.equal(dappOneNetworkPillText, 'Localhost 8545'); + await selectDappClickSend(driver, DAPP_URL); + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x539', + networkText: 'Localhost 8545', + originText: DAPP_URL, + }); + await rejectTransaction(driver); // Go to the second dapp, ensure it uses Ethereum Mainnet - const dappTwoNetworkPillText = await selectDappClickSendGetNetwork( - driver, - DAPP_ONE_URL, + await selectDappClickSend(driver, DAPP_ONE_URL); + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x53a', + networkText: 'Localhost 8546', + originText: DAPP_ONE_URL, + }); + await rejectTransaction(driver); + }, + ); + }); + + it('handles three confirmations on three confirmations concurrently @no-mmi', async function () { + const port = 8546; + const chainId = 1338; // 0x53a + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + // Ganache for network 1 + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + // Ganache for network 3 + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + dappOptions: { numberOfDapps: 3 }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Navigate to extension home screen + await driver.navigate(PAGES.HOME); + + // Open the first dapp + await openDappAndSwitchChain(driver, DAPP_URL); + + // Open the second dapp and switch chains + await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a', 4); + + if (!IS_FIREFOX) { + // Open the third dapp and switch chains + await openDappAndSwitchChain(driver, DAPP_TWO_URL, '0x3e8', 5); + } + + // Trigger a send confirmation on the first dapp, do not confirm or reject + await selectDappClickSend(driver, DAPP_URL); + + // Trigger a send confirmation on the second dapp, do not confirm or reject + await selectDappClickSend(driver, DAPP_ONE_URL); + + if (!IS_FIREFOX) { + // Trigger a send confirmation on the third dapp, do not confirm or reject + await selectDappClickSend(driver, DAPP_TWO_URL); + } + + // Switch to the Notification window, ensure first transaction still showing + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x539', + networkText: 'Localhost 8545', + originText: DAPP_URL, + }); + + // Confirm transaction, wait for first confirmation window to close, second to display + await confirmTransaction(driver); + await driver.delay(veryLargeDelayMs); + + // Switch to the new Notification window, ensure second transaction showing + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x53a', + networkText: 'Localhost 8546', + originText: DAPP_ONE_URL, + }); + + // Reject this transaction, wait for second confirmation window to close, third to display + await rejectTransaction(driver); + await driver.delay(veryLargeDelayMs); + + if (!IS_FIREFOX) { + // Switch to the new Notification window, ensure third transaction showing + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x3e8', + networkText: 'Localhost 7777', + originText: DAPP_TWO_URL, + }); + + // Confirm transaction + await confirmTransaction(driver); + } + + // With first and last confirmations confirmed, and second rejected, + // Ensure only first and last network balances were affected + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Wait for transaction to be completed on final confirmation + await driver.delay(veryLargeDelayMs); + + if (!IS_FIREFOX) { + // Start on the last joined network, whose send transaction was just confirmed + await validateBalanceAndActivity(driver, '24.9998'); + } + + // Switch to second network, ensure full balance + await switchToNetworkByName(driver, 'Localhost 8546'); + await validateBalanceAndActivity(driver, '25', 0); + + // Turn on test networks in Networks menu so Localhost 8545 is available + await driver.clickElement('[data-testid="network-display"]'); + await driver.clickElement('.mm-modal-content__dialog .toggle-button'); + await driver.clickElement( + '.mm-modal-content__dialog button[aria-label="Close"]', ); - assert.equal(dappTwoNetworkPillText, 'Ethereum Mainnet'); + + // Switch to first network, whose send transaction was just confirmed + await switchToNetworkByName(driver, 'Localhost 8545'); + await validateBalanceAndActivity(driver, '24.9998'); }, ); }); diff --git a/test/e2e/tests/swap-send/swap-send-eth.spec.ts b/test/e2e/tests/swap-send/swap-send-eth.spec.ts index 244693d513a2..e4be22ae1fa3 100644 --- a/test/e2e/tests/swap-send/swap-send-eth.spec.ts +++ b/test/e2e/tests/swap-send/swap-send-eth.spec.ts @@ -74,12 +74,6 @@ describe('Swap-Send ETH', function () { // TODO assert swap api request payload await swapSendPage.submitSwap(); - await swapSendPage.verifyHistoryEntry( - 'Send ETH as TST', - 'Pending', - '-1 ETH', - '', - ); await swapSendPage.verifyHistoryEntry( 'Send ETH as TST', 'Confirmed', diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index 37516fd716b8..a323077aa692 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -13,6 +13,73 @@ const FixtureBuilder = require('../../../fixture-builder'); describe('ERC721 NFTs testdapp interaction', function () { const smartContract = SMART_CONTRACTS.NFTS; + it('should add NFTs to state by parsing tx logs without having to click on watch NFT', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + title: this.test.fullTitle(), + }, + async ({ driver, _, contractRegistry }) => { + const contract = contractRegistry.getContractAddress(smartContract); + await unlockWallet(driver); + + // Open Dapp and wait for deployed contract + await openDapp(driver, contract); + await driver.findClickableElement('#deployButton'); + + // mint NFTs + await driver.fill('#mintAmountInput', '5'); + await driver.clickElement({ text: 'Mint', tag: 'button' }); + + // Notification + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const [extension] = windowHandles; + await driver.switchToWindowWithTitle( + WINDOW_TITLES.Dialog, + windowHandles, + ); + await driver.waitForSelector({ + css: '.confirm-page-container-summary__action__name', + text: 'Deposit', + }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + await driver.clickElement( + '[data-testid="account-overview__activity-tab"]', + ); + const transactionItem = await driver.waitForSelector({ + css: '[data-testid="activity-list-item-action"]', + text: 'Deposit', + }); + assert.equal(await transactionItem.isDisplayed(), true); + + // verify the mint transaction has finished + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + const nftsMintStatus = await driver.findElement({ + css: '#nftsStatus', + text: 'Mint completed', + }); + assert.equal(await nftsMintStatus.isDisplayed(), true); + + await driver.switchToWindow(extension); + + await clickNestedButton(driver, 'NFTs'); + await driver.findElement({ text: 'TestDappNFTs (5)' }); + const nftsListItemsFirstCheck = await driver.findElements( + '.nft-item__container', + ); + assert.equal(nftsListItemsFirstCheck.length, 5); + }, + ); + }); + it('should prompt users to add their NFTs to their wallet (one by one) @no-mmi', async function () { await withFixtures( { @@ -97,14 +164,16 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement({ text: 'Add NFTs', tag: 'button' }); await driver.switchToWindow(extension); await clickNestedButton(driver, 'NFTs'); - await driver.findElement({ text: 'TestDappNFTs (3)' }); + // Changed this check from 3 to 6, because after mint all nfts has been added to state, + await driver.findElement({ text: 'TestDappNFTs (6)' }); const nftsListItemsFirstCheck = await driver.findElements( '.nft-item__container', ); - assert.equal(nftsListItemsFirstCheck.length, 3); + assert.equal(nftsListItemsFirstCheck.length, 6); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); await driver.fill('#watchNFTInput', '4'); + await driver.clickElement({ text: 'Watch NFT', tag: 'button' }); await driver.fill('#watchNFTInput', '5'); await driver.clickElement({ text: 'Watch NFT', tag: 'button' }); diff --git a/test/e2e/tests/tokens/nft/send-nft.spec.js b/test/e2e/tests/tokens/nft/send-nft.spec.js index 7df8febcab56..a9b89a2abb9b 100644 --- a/test/e2e/tests/tokens/nft/send-nft.spec.js +++ b/test/e2e/tests/tokens/nft/send-nft.spec.js @@ -145,8 +145,6 @@ describe('Send NFT', function () { // Go back to NFTs tab and check the imported NFT is shown as previously owned await driver.clickElement('[data-testid="account-overview__nfts-tab"]'); - await driver.clickElement('[data-testid="refresh-list-button"]'); - const previouslyOwnedNft = await driver.findElement({ css: 'h5', text: 'Previously Owned', diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 365d9b00bdbd..85f617450136 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -666,3 +666,22 @@ export const createSwapsMockStore = () => { }, }; }; + +export const createBridgeMockStore = () => { + const swapsStore = createSwapsMockStore(); + return { + ...swapsStore, + bridge: { + toChain: null, + }, + metamask: { + ...swapsStore.metamask, + bridgeState: { + ...(swapsStore.metamask.bridgeState ?? {}), + bridgeFeatureFlags: { + extensionSupport: false, + }, + }, + }, + }; +}; diff --git a/test/setup.js b/test/setup.js index f96e744a917a..17f9c45249ab 100644 --- a/test/setup.js +++ b/test/setup.js @@ -7,3 +7,10 @@ window.SVGPathElement = window.SVGPathElement || { prototype: {} }; global.indexedDB = {}; // scrollIntoView is not available in JSDOM window.HTMLElement.prototype.scrollIntoView = () => undefined + +global.platform = { + // Required for: coin overviews components + openTab: () => undefined, + // Required for: settings info tab + getVersion: () => '', +}; diff --git a/ui/components/app/add-network/add-network.test.js b/ui/components/app/add-network/add-network.test.js index 3a15f4d33c3e..f3084b1db065 100644 --- a/ui/components/app/add-network/add-network.test.js +++ b/ui/components/app/add-network/add-network.test.js @@ -6,6 +6,7 @@ import mockState from '../../../../test/data/mock-state.json'; import AddNetwork from './add-network'; jest.mock('../../../selectors', () => ({ + ...jest.requireActual('../../../selectors'), getNetworkConfigurations: () => ({ networkConfigurationId: { chainId: '0x539', diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx index c9647cedf633..0c4165182b18 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx @@ -180,8 +180,8 @@ export function ConfirmAlertModal({ onCheckboxClick={handleConfirmCheckbox} label={ selectedAlert?.provider === SecurityProvider.Blockaid - ? t('confirmAlertModalAcknowledgeBlockaid') - : t('confirmAlertModalAcknowledge') + ? t('confirmAlertModalAcknowledgeSingle') + : t('confirmAlertModalAcknowledgeMultiple') } /> } diff --git a/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap b/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap index ef56b906038f..d869e2f978fa 100644 --- a/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap +++ b/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap @@ -16,7 +16,7 @@ exports[`Inline Alert renders alert with danger severity 1`] = `

- [inlineAlert] + [alert]

- [inlineAlert] + [alert]

- [inlineAlert] + [alert]

- {t('inlineAlert')} + {t('alert')} diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 82c45a27134e..8e493037da44 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -5,13 +5,9 @@ import TokenList from '../token-list'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'; import { - getSelectedAccountCachedBalance, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getShouldHideZeroBalanceTokens, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, - ///: END:ONLY_INCLUDE_IF getSelectedAccount, getPreferences, } from '../../../selectors'; @@ -22,6 +18,7 @@ import { getMultichainShouldShowFiat, getMultichainCurrencyImage, getMultichainIsMainnet, + getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -48,11 +45,11 @@ import { RAMPS_CARD_VARIANT_TYPES, RampsCard, } from '../../multichain/ramps-card/ramps-card'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF -const AssetList = ({ onClickAsset }) => { +const AssetList = ({ onClickAsset, showTokensLinks }) => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const selectedAccountBalance = useSelector(getSelectedAccountCachedBalance); const nativeCurrency = useSelector(getMultichainNativeCurrency); const showFiat = useSelector(getMultichainShouldShowFiat); const isMainnet = useSelector(getMultichainIsMainnet); @@ -67,7 +64,7 @@ const AssetList = ({ onClickAsset }) => { rpcUrl, ); const trackEvent = useContext(MetaMetricsContext); - const balance = useSelector(getSelectedAccountCachedBalance); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); const balanceIsLoading = !balance; const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( @@ -84,13 +81,13 @@ const AssetList = ({ onClickAsset }) => { } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }); const [primaryCurrencyDisplay, primaryCurrencyProperties] = - useCurrencyDisplay(selectedAccountBalance, { + useCurrencyDisplay(balance, { numberOfDecimals: primaryNumberOfDecimals, currency: primaryCurrency, }); const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = - useCurrencyDisplay(selectedAccountBalance, { + useCurrencyDisplay(balance, { numberOfDecimals: secondaryNumberOfDecimals, currency: secondaryCurrency, }); @@ -109,12 +106,16 @@ const AssetList = ({ onClickAsset }) => { }); const balanceIsZero = Number(totalFiatBalance) === 0; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF const isEvm = useSelector(getMultichainIsEvm); + // NOTE: Since we can parametrize it now, we keep the original behavior + // for EVM assets + const shouldShowTokensLinks = showTokensLinks ?? isEvm; + let isStakeable = isMainnet && isEvm; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) isStakeable = false; @@ -183,18 +184,22 @@ const AssetList = ({ onClickAsset }) => { }); }} /> - {balanceIsZero && ( - 0 ? 0 : 4} - /> + {shouldShowTokensLinks && ( + <> + {balanceIsZero && ( + 0 ? 0 : 4} + /> + )} + 0 && !balanceIsZero ? 0 : 2} + /> + )} - 0 && !balanceIsZero ? 0 : 2} - /> {showDetectedTokens && ( )} @@ -204,6 +209,7 @@ const AssetList = ({ onClickAsset }) => { AssetList.propTypes = { onClickAsset: PropTypes.func.isRequired, + showTokensLinks: PropTypes.bool, }; export default AssetList; diff --git a/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx b/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx new file mode 100644 index 000000000000..3eb9a9323048 --- /dev/null +++ b/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + BlockSize, + Display, +} from '../../../../helpers/constants/design-system'; +import { + Box, + ButtonPrimary, + ButtonPrimarySize, + ButtonSecondary, + ButtonSecondarySize, + Modal, + ModalBody, + ModalContent, + ModalHeader, + ModalOverlay, +} from '../../../component-library'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + hideModal, + setEditedNetwork, + toggleNetworkMenu, +} from '../../../../store/actions'; + +const ConfirmDeleteRpcUrlModal = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + return ( + { + dispatch(setEditedNetwork()); + dispatch(hideModal()); + }} + > + + + {t('confirmDeletion')} + + {t('confirmRpcUrlDeletionMessage')} + + { + dispatch(hideModal()); + dispatch(toggleNetworkMenu()); + }} + > + {t('back')} + + { + console.log('TODO: Delete RPc URL'); + }} + > + {t('deleteRpcUrl')} + + + + + + ); +}; + +export default ConfirmDeleteRpcUrlModal; diff --git a/ui/components/app/modals/modal.js b/ui/components/app/modals/modal.js index f3eb1a950a40..9d546e5de74d 100644 --- a/ui/components/app/modals/modal.js +++ b/ui/components/app/modals/modal.js @@ -38,6 +38,7 @@ import TransactionAlreadyConfirmed from './transaction-already-confirmed'; // Metamask Notifications import ConfirmTurnOffProfileSyncing from './confirm-turn-off-profile-syncing'; import TurnOnMetamaskNotifications from './turn-on-metamask-notifications/turn-on-metamask-notifications'; +import ConfirmDeleteRpcUrlModal from './confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal'; const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -230,6 +231,19 @@ const MODALS = { }, }, + CONFIRM_DELETE_RPC_URL: { + contents: , + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + EDIT_APPROVAL_PERMISSION: { contents: , mobileModalStyle: { diff --git a/ui/components/app/network-account-balance-header/network-account-balance-header.js b/ui/components/app/network-account-balance-header/network-account-balance-header.js index 98c91acfe81a..9137d53f1f14 100644 --- a/ui/components/app/network-account-balance-header/network-account-balance-header.js +++ b/ui/components/app/network-account-balance-header/network-account-balance-header.js @@ -68,6 +68,7 @@ export default function NetworkAccountBalanceHeader({ variant={TextVariant.bodySm} as="h6" color={TextColor.textAlternative} + data-testid="signature-request-network-display" > {networkName} diff --git a/ui/components/app/nfts-tab/nfts-tab.js b/ui/components/app/nfts-tab/nfts-tab.js index bc54ca4c9766..43a0bd1a9d64 100644 --- a/ui/components/app/nfts-tab/nfts-tab.js +++ b/ui/components/app/nfts-tab/nfts-tab.js @@ -17,7 +17,6 @@ import { useNftsCollections } from '../../../hooks/useNftsCollections'; import { getCurrentNetwork, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, getShouldHideZeroBalanceTokens, getSelectedAccount, ///: END:ONLY_INCLUDE_IF @@ -49,6 +48,7 @@ import { RampsCard, } from '../../multichain/ramps-card/ramps-card'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF import Spinner from '../../ui/spinner'; @@ -73,7 +73,7 @@ export default function NftsTab() { shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const showRampsCard = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index a48e24a661c6..ae8a3ff000db 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -52,6 +52,7 @@ jest.mock('../../../selectors', () => { return { getAccountType: mockGetAccountType, getSelectedInternalAccount: mockGetSelectedAccount, + getCurrentChainId: jest.fn(() => '0x1'), }; }); diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index b47130c5d739..20c41ef33e4d 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -10,7 +10,6 @@ import { getCurrentChainId, getSelectedAccount, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, getShouldHideZeroBalanceTokens, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; @@ -33,6 +32,7 @@ import { RAMPS_CARD_VARIANT_TYPES, RampsCard, } from '../../multichain/ramps-card/ramps-card'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF const PAGE_INCREMENT = 10; @@ -141,8 +141,7 @@ export default function TransactionList({ shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; - const isBuyableChain = useSelector(getIsBuyableChain); - + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const showRampsCard = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/wallet-overview/btc-overview.stories.tsx b/ui/components/app/wallet-overview/btc-overview.stories.tsx new file mode 100644 index 000000000000..43dff2554bef --- /dev/null +++ b/ui/components/app/wallet-overview/btc-overview.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import BtcOverview from './btc-overview'; + +export default { + title: 'Components/App/WalletOverview/BtcOverview', + component: BtcOverview, + parameters: { + docs: { + description: { + component: + 'A component that displays an overview of Bitcoin wallet information.', + }, + }, + }, +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx new file mode 100644 index 000000000000..233096f0918c --- /dev/null +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent } from '@testing-library/react'; +import thunk from 'redux-thunk'; +import { Cryptocurrency } from '@metamask/assets-controllers'; +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { MultichainNativeAssets } from '../../../../shared/constants/multichain/assets'; +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; +import { RampsMetaMaskEntry } from '../../../hooks/ramps/useRamps/useRamps'; +import BtcOverview from './btc-overview'; + +const PORTOFOLIO_URL = 'https://portfolio.test'; + +const BTC_OVERVIEW_BUY = 'coin-overview-buy'; +const BTC_OVERVIEW_BRIDGE = 'coin-overview-bridge'; +const BTC_OVERVIEW_PORTFOLIO = 'coin-overview-portfolio'; +const BTC_OVERVIEW_SWAP = 'token-overview-button-swap'; +const BTC_OVERVIEW_SEND = 'coin-overview-send'; +const BTC_OVERVIEW_PRIMARY_CURRENCY = 'coin-overview__primary-currency'; + +const mockMetaMetricsId = 'deadbeef'; +const mockNonEvmBalance = '1'; +const mockNonEvmAccount = { + address: 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', + id: '542490c8-d178-433b-9f31-f680b11f45a5', + metadata: { + name: 'Bitcoin Account', + keyring: { + type: 'Snap Keyring', + }, + snap: { + id: 'btc-snap-id', + name: 'btc-snap-name', + }, + }, + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, +}; + +function getStore(state?: Record) { + return configureMockStore([thunk])({ + metamask: { + ...mockState.metamask, + internalAccounts: { + accounts: { + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + // (Multichain) BalancesController + balances: { + [mockNonEvmAccount.id]: { + [MultichainNativeAssets.BITCOIN]: { + amount: mockNonEvmBalance, + unit: 'BTC', + }, + }, + }, + // (Multichain) RatesController + fiatCurrency: 'usd', + rates: { + [Cryptocurrency.Btc]: { + conversionRate: '1.000', + conversionDate: 0, + }, + }, + cryptocurrencies: [Cryptocurrency.Btc], + // Required, during onboarding, the extension will assume we're in an "EVM context", meaning + // most multichain selectors will not use non-EVM logic despite having a non-EVM + // selected account + completedOnboarding: true, + // Used when clicking on some buttons + metaMetricsId: mockMetaMetricsId, + // Override state if provided + ...state, + }, + }); +} + +function makePortfolioUrl(path: string, getParams: Record) { + const params = new URLSearchParams(getParams); + return `${PORTOFOLIO_URL}/${path}?${params.toString()}`; +} + +describe('BtcOverview', () => { + it('shows the primary balance', async () => { + const { queryByTestId, queryByText } = renderWithProvider( + , + getStore(), + ); + + const primaryBalance = queryByTestId(BTC_OVERVIEW_PRIMARY_CURRENCY); + expect(primaryBalance).toBeInTheDocument(); + expect(primaryBalance).toHaveTextContent(`${mockNonEvmBalance}BTC`); + // For now we consider balance to be always cached + expect(queryByText('*')).toBeInTheDocument(); + }); + + it('shows a spinner if balance is not available', async () => { + const { container } = renderWithProvider( + , + getStore({ + // The balances won't be available + balances: {}, + }), + ); + + const spinner = container.querySelector( + '.coin-overview__balance .coin-overview__primary-container .spinner', + ); + expect(spinner).toBeInTheDocument(); + }); + + it('buttons Send/Swap/Bridge are disabled', () => { + const { queryByTestId } = renderWithProvider(, getStore()); + + for (const buttonTestId of [ + BTC_OVERVIEW_SEND, + BTC_OVERVIEW_SWAP, + BTC_OVERVIEW_BRIDGE, + ]) { + const button = queryByTestId(buttonTestId); + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); + } + }); + + it('shows the "Buy & Sell" button', () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + expect(buyButton).toBeInTheDocument(); + }); + + it('opens the Portfolio "Buy & Sell" URI when clicking on "Buy & Sell" button', async () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + expect(buyButton).toBeInTheDocument(); + fireEvent.click(buyButton as HTMLElement); + + expect(openTabSpy).toHaveBeenCalledTimes(1); + expect(openTabSpy).toHaveBeenCalledWith({ + url: makePortfolioUrl('buy', { + metamaskEntry: RampsMetaMaskEntry.BuySellButton, + chainId: MultichainNetworks.BITCOIN, + metametricsId: mockMetaMetricsId, + }), + }); + }); + + it('always show the Portfolio button', () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const portfolioButton = queryByTestId(BTC_OVERVIEW_PORTFOLIO); + expect(portfolioButton).toBeInTheDocument(); + }); + + it('open the Portfolio URI when clicking on Portfolio button', async () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + + const portfolioButton = queryByTestId(BTC_OVERVIEW_PORTFOLIO); + expect(portfolioButton).toBeInTheDocument(); + fireEvent.click(portfolioButton as HTMLElement); + + expect(openTabSpy).toHaveBeenCalledTimes(1); + expect(openTabSpy).toHaveBeenCalledWith({ + url: makePortfolioUrl('', { + metamaskEntry: 'ext_portfolio_button', + metametricsId: mockMetaMetricsId, + }), + }); + }); +}); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx new file mode 100644 index 000000000000..3703252f205a --- /dev/null +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { useSelector } from 'react-redux'; +import { + getMultichainProviderConfig, + getMultichainSelectedAccountCachedBalance, +} from '../../../selectors/multichain'; +import { CoinOverview } from './coin-overview'; + +type BtcOverviewProps = { + className?: string; +}; + +const BtcOverview = ({ className }: BtcOverviewProps) => { + const { chainId } = useSelector(getMultichainProviderConfig); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + + return ( + + ); +}; + +export default BtcOverview; diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 5bba897121e4..b1774897edc4 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -6,9 +6,20 @@ import { useLocation, ///: END:ONLY_INCLUDE_IF } from 'react-router-dom'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { toHex } from '@metamask/controller-utils'; +///: END:ONLY_INCLUDE_IF +import { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + isCaipChainId, + ///: END:ONLY_INCLUDE_IF + CaipChainId, +} from '@metamask/utils'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { ChainId } from '../../../../shared/constants/network'; +///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import { CaipChainId } from '@metamask/utils'; import { getMmiPortfolioEnabled, getMmiPortfolioUrl, @@ -22,11 +33,9 @@ import { SEND_ROUTE, } from '../../../helpers/constants/routes'; import { - SwapsEthToken, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + SwapsEthToken, getCurrentKeyring, - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getMetaMetricsId, ///: END:ONLY_INCLUDE_IF getUseExternalServices, @@ -55,7 +64,8 @@ import { Box, Icon, IconName } from '../../component-library'; import IconButton from '../../ui/icon-button'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; + ///: END:ONLY_INCLUDE_IF const CoinButtons = ({ @@ -65,17 +75,23 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain, isBuyableChain, + // TODO: Remove this logic once `isNativeTokenBuyable` has been + // merged (see: https://github.com/MetaMask/metamask-extension/pull/24041) + isBuyableChainWithoutSigning = false, defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix = 'coin', }: { - classPrefix?: string; - isBuyableChain: boolean; - isSigningEnabled: boolean; + chainId: `0x${string}` | CaipChainId | number; isSwapsChain: boolean; + isSigningEnabled: boolean; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain: boolean; - chainId: `0x${string}` | CaipChainId | number; + isBuyableChain: boolean; + isBuyableChainWithoutSigning?: boolean; defaultSwapsToken?: SwapsEthToken; + ///: END:ONLY_INCLUDE_IF + classPrefix?: string; }) => { const t = useContext(I18nContext); const dispatch = useDispatch(); @@ -96,7 +112,10 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) { condition: !isBuyableChain, message: '' }, ///: END:ONLY_INCLUDE_IF - { condition: !isSigningEnabled, message: 'methodNotSupported' }, + { + condition: !(isSigningEnabled || isBuyableChainWithoutSigning), + message: 'methodNotSupported', + }, ], sendButton: [ { condition: !isSigningEnabled, message: 'methodNotSupported' }, @@ -129,6 +148,16 @@ const CoinButtons = ({ return contents; }; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const getChainId = (): CaipChainId | ChainId => { + if (isCaipChainId(chainId)) { + return chainId as CaipChainId; + } + // Otherwise we assume that's an EVM chain ID, so use the usual 0x prefix + return toHex(chainId) as ChainId; + }; + ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled); const mmiPortfolioUrl = useSelector(getMmiPortfolioUrl); @@ -247,7 +276,7 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const handleBuyAndSellOnClick = useCallback(() => { - openBuyCryptoInPdapp(); + openBuyCryptoInPdapp(getChainId()); trackEvent({ event: MetaMetricsEventName.NavBuyButtonClicked, category: MetaMetricsEventCategory.Navigation, @@ -310,7 +339,10 @@ const CoinButtons = ({ Icon={ } - disabled={!isBuyableChain || !isSigningEnabled} + disabled={ + !isBuyableChain || + !(isSigningEnabled || isBuyableChainWithoutSigning) + } data-testid={`${classPrefix}-overview-buy`} label={t('buyAndSell')} onClick={handleBuyAndSellOnClick} diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index abf1f6ca7158..6364b0231e82 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -3,15 +3,13 @@ import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { zeroAddress } from 'ethereumjs-util'; -///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { CaipChainId } from '@metamask/utils'; -///: END:ONLY_INCLUDE_IF +import type { Hex } from '@metamask/utils'; import { I18nContext } from '../../../contexts/i18n'; import Tooltip from '../../ui/tooltip'; import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { - getShouldShowFiat, getPreferences, getTokensMarketData, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -20,24 +18,28 @@ import { } from '../../../selectors'; import Spinner from '../../ui/spinner'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; import { showPrimaryCurrency } from '../../../../shared/modules/currency-display.utils'; import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; +import { + getMultichainIsEvm, + getMultichainProviderConfig, + getMultichainShouldShowFiat, +} from '../../../selectors/multichain'; import WalletOverview from './wallet-overview'; import CoinButtons from './coin-buttons'; export type CoinOverviewProps = { balance: string; balanceIsCached: boolean; - className: string; - classPrefix: string; - chainId: CaipChainId | number; - showAddress: boolean; + className?: string; + classPrefix?: string; + chainId: CaipChainId | Hex; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) // FIXME: This seems to be for Ethereum only defaultSwapsToken?: SwapsEthToken; isBridgeChain: boolean; isBuyableChain: boolean; + isBuyableChainWithoutSigning: boolean; ///: END:ONLY_INCLUDE_IF isSwapsChain: boolean; isSigningEnabled: boolean; @@ -53,6 +55,7 @@ export const CoinOverview = ({ defaultSwapsToken, isBridgeChain, isBuyableChain, + isBuyableChainWithoutSigning, ///: END:ONLY_INCLUDE_IF isSwapsChain, isSigningEnabled, @@ -65,9 +68,10 @@ export const CoinOverview = ({ ///: END:ONLY_INCLUDE_IF const t = useContext(I18nContext); - const showFiat = useSelector(getShouldShowFiat); + const isEvm = useSelector(getMultichainIsEvm); + const showFiat = useSelector(getMultichainShouldShowFiat); const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const { ticker, type, rpcUrl } = useSelector(getProviderConfig); + const { ticker, type, rpcUrl } = useSelector(getMultichainProviderConfig); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, ticker, @@ -131,9 +135,11 @@ export const CoinOverview = ({ hideTitle /> )} - + {isEvm && ( + + )} } @@ -146,6 +152,7 @@ export const CoinOverview = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain, isBuyableChain, + isBuyableChainWithoutSigning, defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix, diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 5eeec7a59cbf..684c6cffbf6b 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; - import { EthMethod } from '@metamask/keyring-api'; import { isEqual } from 'lodash'; import { @@ -13,15 +12,17 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getSwapsDefaultToken, getIsBridgeChain, - getIsBuyableChain, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; const EthOverview = ({ className }) => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); // FIXME: This causes re-renders, so use isEqual to avoid this const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 0d13bff7e1ef..0d079a32f104 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -12,27 +12,11 @@ import { import { renderWithProvider } from '../../../../test/jest/rendering'; import { KeyringType } from '../../../../shared/constants/keyring'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import { getIntlLocale } from '../../../ducks/locale/locale'; import EthOverview from './eth-overview'; -// Mock BUYABLE_CHAINS_MAP -jest.mock('../../../../shared/constants/network', () => ({ - ...jest.requireActual('../../../../shared/constants/network'), - BUYABLE_CHAINS_MAP: { - // MAINNET - '0x1': { - nativeCurrency: 'ETH', - network: 'ethereum', - }, - // POLYGON - '0x89': { - nativeCurrency: 'MATIC', - network: 'polygon', - }, - }, -})); - jest.mock('../../../hooks/useIsOriginalNativeTokenSymbol', () => { return { useIsOriginalNativeTokenSymbol: jest.fn(), @@ -138,6 +122,9 @@ describe('EthOverview', () => { }, ], }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const store = configureMockStore([thunk])(mockStore); @@ -181,6 +168,7 @@ describe('EthOverview', () => { it('should show the cached primary balance', async () => { const mockedStoreWithCachedBalance = { + ...mockStore, metamask: { ...mockStore.metamask, accounts: { @@ -267,6 +255,7 @@ describe('EthOverview', () => { it('should open the MMI PD Swaps URI when clicking on Swap button with a Custody account', async () => { const mockedStoreWithCustodyKeyring = { + ...mockStore, metamask: { ...mockStore.metamask, mmiConfiguration: { @@ -375,6 +364,7 @@ describe('EthOverview', () => { it('should have the Buy native token button disabled if chain id is not part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { @@ -399,6 +389,7 @@ describe('EthOverview', () => { it('should have the Buy native token enabled if chain id is part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { @@ -432,6 +423,7 @@ describe('EthOverview', () => { it('should open the Buy native token URI when clicking on Buy button for a buyable chain ID', async () => { const mockedStoreWithBuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { diff --git a/ui/components/app/wallet-overview/index.js b/ui/components/app/wallet-overview/index.js index 2eb058f81afd..54536007bc41 100644 --- a/ui/components/app/wallet-overview/index.js +++ b/ui/components/app/wallet-overview/index.js @@ -1 +1,2 @@ export { default as EthOverview } from './eth-overview'; +export { default as BtcOverview } from './btc-overview'; diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap index d97d0fd70fbe..0b2b13a29277 100644 --- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -32,7 +32,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam class="mm-avatar-account__jazzicon" >
- 0x0DCD5...3E7bc + bc1qn3s...5eker

{ const { container } = render({ account: mockNonEvmAccount }); expect(screen.getByText(mockAccount.metadata.name)).toBeInTheDocument(); expect( - screen.getByText( - shortenAddress(toChecksumHexAddress(mockAccount.address)), - ), + screen.getByText(shortenAddress(mockNonEvmAccount.address)), ).toBeInTheDocument(); expect( document.querySelector('[title="$100,000.00 USD"]'), diff --git a/ui/components/multichain/account-overview/account-overview-btc.stories.tsx b/ui/components/multichain/account-overview/account-overview-btc.stories.tsx new file mode 100644 index 000000000000..2afc54e22b23 --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-btc.stories.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { AccountOverviewBtc } from './account-overview-btc' +import { AccountOverviewCommonProps } from './common'; + +export default { + title: 'Components/Multichain/AccountOverviewBtc', + component: AccountOverviewBtc, +}; + +export const DefaultStory = ( + args: JSX.IntrinsicAttributes & AccountOverviewCommonProps +) => ; diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-btc.test.tsx new file mode 100644 index 000000000000..f75e906e0bce --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-btc.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import { + AccountOverviewBtc, + AccountOverviewBtcProps, +} from './account-overview-btc'; + +const defaultProps: AccountOverviewBtcProps = { + defaultHomeActiveTabName: '', + onTabClick: jest.fn(), + setBasicFunctionalityModalOpen: jest.fn(), + onSupportLinkClick: jest.fn(), +}; + +const render = (props: AccountOverviewBtcProps = defaultProps) => { + const store = configureStore({ + metamask: mockState.metamask, + }); + + return renderWithProvider(, store); +}; + +describe('AccountOverviewBtc', () => { + it('shows only Tokens and Activity tabs', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('account-overview__asset-tab')).toBeInTheDocument(); + expect(queryByTestId('account-overview__nfts-tab')).not.toBeInTheDocument(); + expect(queryByTestId('account-overview__activity-tab')).toBeInTheDocument(); + }); + + it('does not show tokens links', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('account-overview__asset-tab')).toBeInTheDocument(); + expect(queryByTestId('receive-token-button')).not.toBeInTheDocument(); + expect(queryByTestId('import-token-button')).not.toBeInTheDocument(); + // TODO: This one might be required, but we do not really handle tokens for BTC yet... + expect(queryByTestId('refresh-list-button')).not.toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-overview/account-overview-btc.tsx b/ui/components/multichain/account-overview/account-overview-btc.tsx new file mode 100644 index 000000000000..dd58b2eef414 --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-btc.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { BtcOverview } from '../../app/wallet-overview'; +import { AccountOverviewLayout } from './account-overview-layout'; +import { AccountOverviewCommonProps } from './common'; + +export type AccountOverviewBtcProps = AccountOverviewCommonProps; + +export const AccountOverviewBtc = (props: AccountOverviewBtcProps) => { + return ( + + { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) + + ///: END:ONLY_INCLUDE_IF + } + + ); +}; diff --git a/ui/components/multichain/account-overview/account-overview-tabs.tsx b/ui/components/multichain/account-overview/account-overview-tabs.tsx index 284bfb8223f3..543675d06201 100644 --- a/ui/components/multichain/account-overview/account-overview-tabs.tsx +++ b/ui/components/multichain/account-overview/account-overview-tabs.tsx @@ -41,6 +41,7 @@ import { AccountOverviewCommonProps } from './common'; export type AccountOverviewTabsProps = AccountOverviewCommonProps & { showTokens: boolean; + showTokensLinks?: boolean; showNfts: boolean; showActivity: boolean; }; @@ -52,6 +53,7 @@ export const AccountOverviewTabs = ({ ///: END:ONLY_INCLUDE_IF defaultHomeActiveTabName, showTokens, + showTokensLinks, showNfts, showActivity, }: AccountOverviewTabsProps) => { @@ -141,7 +143,8 @@ export const AccountOverviewTabs = ({ > + showTokensLinks={showTokensLinks ?? true} + onClickAsset={(asset: string) => history.push(`${ASSET_ROUTE}/${asset}`) } /> diff --git a/ui/components/multichain/account-overview/account-overview.tsx b/ui/components/multichain/account-overview/account-overview.tsx index 320b865478fc..3d6121e41471 100644 --- a/ui/components/multichain/account-overview/account-overview.tsx +++ b/ui/components/multichain/account-overview/account-overview.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { useSelector } from 'react-redux'; +import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { BannerAlert, BannerAlertSeverity } from '../../component-library'; -import { isSelectedInternalAccountEth } from '../../../selectors/accounts'; +import { getSelectedInternalAccount } from '../../../selectors'; import { AccountOverviewEth } from './account-overview-eth'; +import { AccountOverviewBtc } from './account-overview-btc'; import { AccountOverviewUnknown } from './account-overview-unknown'; import { AccountOverviewCommonProps } from './common'; @@ -13,12 +15,22 @@ export type AccountOverviewProps = AccountOverviewCommonProps & { export function AccountOverview(props: AccountOverviewProps) { const t = useI18nContext(); - - const isEth = useSelector(isSelectedInternalAccountEth); - const isUnknown = !isEth; + const account = useSelector(getSelectedInternalAccount); const { useExternalServices, setBasicFunctionalityModalOpen } = props; + const renderAccountOverviewOption = () => { + switch (account.type) { + case EthAccountType.Eoa: + case EthAccountType.Erc4337: + return ; + case BtcAccountType.P2wpkh: + return ; + default: + return ; + } + }; + return ( <> {!useExternalServices && ( @@ -33,8 +45,7 @@ export function AccountOverview(props: AccountOverviewProps) { title={t('basicConfigurationBannerTitle')} /> )} - {isEth && } - {isUnknown && } + {renderAccountOverviewOption()} ); } diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx new file mode 100644 index 000000000000..8e4269928bc8 --- /dev/null +++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { + Box, + ButtonPrimary, + ButtonPrimarySize, + FormTextField, +} from '../../../component-library'; +import { + BlockSize, + Display, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +const AddRpcUrlModal = () => { + const t = useI18nContext(); + + return ( + + + + + {t('addUrl')} + + + ); +}; + +export default AddRpcUrlModal; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index c481b163f8a0..3a1f279103d4 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { useDispatch, useSelector } from 'react-redux'; @@ -15,6 +15,7 @@ import { toggleNetworkMenu, updateNetworksList, setNetworkClientIdForDomain, + setEditedNetwork, } from '../../../store/actions'; import { FEATURED_RPCS, @@ -32,6 +33,7 @@ import { getOriginOfCurrentTab, getUseRequestQueue, getNetworkConfigurations, + getEditedNetwork, } from '../../../selectors'; import ToggleButton from '../../ui/toggle-button'; import { @@ -70,6 +72,7 @@ import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../helpers/utils/f import AddNetworkModal from '../../../pages/onboarding-flow/add-network-modal'; import PopularNetworkList from './popular-network-list/popular-network-list'; import NetworkListSearch from './network-list-search/network-list-search'; +import AddRpcUrlModal from './add-rpc-url-modal/add-rpc-url-modal'; const ACTION_MODES = { // Displays the search box and network list @@ -78,14 +81,13 @@ const ACTION_MODES = { ADD: 'add', // Displays the Edit form EDIT: 'edit', + // Displays the page for adding an additional RPC URL + ADD_RPC: 'add_rpc', }; export const NetworkListMenu = ({ onClose }) => { const t = useI18nContext(); - const [actionMode, setActionMode] = useState(ACTION_MODES.LIST); - const [modalTitle, setModalTitle] = useState(t('networkMenuHeading')); - const [networkToEdit, setNetworkToEdit] = useState(null); const nonTestNetworks = useSelector(getNonTestNetworks); const testNetworks = useSelector(getTestNetworks); const showTestNetworks = useSelector(getShowTestNetworks); @@ -114,6 +116,19 @@ export const NetworkListMenu = ({ onClose }) => { const orderedNetworksList = useSelector(getOrderedNetworksList); + const editedNetwork = useSelector(getEditedNetwork); + + const [actionMode, setActionMode] = useState( + editedNetwork ? ACTION_MODES.EDIT : ACTION_MODES.LIST, + ); + + const networkToEdit = useMemo(() => { + const network = [...nonTestNetworks, ...testNetworks].find( + (n) => n.id === editedNetwork?.networkConfigurationId, + ); + return network ? { ...network, label: network.nickname } : undefined; + }, [editedNetwork, nonTestNetworks, testNetworks]); + const networkConfigurationChainIds = Object.values(networkConfigurations).map( (net) => net.chainId, ); @@ -259,12 +274,12 @@ export const NetworkListMenu = ({ onClose }) => { const getOnEditCallback = (network) => { return () => { - const networkToUse = { - ...network, - label: network.nickname, - }; - setModalTitle(network.nickname); - setNetworkToEdit(networkToUse); + dispatch( + setEditedNetwork({ + networkConfigurationId: network.id, + nickname: network.nickname, + }), + ); setActionMode(ACTION_MODES.EDIT); }; }; @@ -518,7 +533,6 @@ export const NetworkListMenu = ({ onClose }) => { category: MetaMetricsEventCategory.Network, }); setActionMode(ACTION_MODES.ADD); - setModalTitle(t('addCustomNetwork')); }} > {t('addNetwork')} @@ -528,20 +542,38 @@ export const NetworkListMenu = ({ onClose }) => { ); } else if (actionMode === ACTION_MODES.ADD) { return ; + } else if (actionMode === ACTION_MODES.EDIT) { + return ( + setActionMode(ACTION_MODES.ADD_RPC)} + /> + ); + } else if (actionMode === ACTION_MODES.ADD_RPC) { + return ; } - return ( - - ); + return null; // Unreachable, but satisfies linter }; - const headerAdditionalProps = - actionMode === ACTION_MODES.LIST - ? {} - : { onBack: () => setActionMode(ACTION_MODES.LIST) }; + // Modal back button + let onBack; + if (actionMode === ACTION_MODES.EDIT || actionMode === ACTION_MODES.ADD) { + onBack = () => setActionMode(ACTION_MODES.LIST); + } else if (actionMode === ACTION_MODES.ADD_RPC) { + onBack = () => setActionMode(ACTION_MODES.EDIT); + } + + // Modal title + let title; + if (actionMode === ACTION_MODES.LIST) { + title = t('networkMenuHeading'); + } else if (actionMode === ACTION_MODES.ADD) { + title = t('addCustomNetwork'); + } else { + title = editedNetwork.nickname; + } return ( @@ -560,9 +592,9 @@ export const NetworkListMenu = ({ onClose }) => { paddingRight={4} paddingBottom={6} onClose={onClose} - {...headerAdditionalProps} + onBack={onBack} > - {modalTitle} + {title} {renderListNetworks()} diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index e1c61f2a35c1..c03e9e956f9a 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -21,7 +21,7 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; import useRamps, { RampsMetaMaskEntry, -} from '../../../hooks/experiences/useRamps'; +} from '../../../hooks/ramps/useRamps/useRamps'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import { getCurrentLocale } from '../../../ducks/locale/locale'; @@ -89,7 +89,7 @@ export const RampsCard = ({ variant }) => { }, [currentLocale, chainId, nickname, trackEvent]); const onClick = useCallback(() => { - openBuyCryptoInPdapp(); + openBuyCryptoInPdapp(chainId); trackEvent({ event: MetaMetricsEventName.NavBuyButtonClicked, category: MetaMetricsEventCategory.Navigation, diff --git a/ui/components/multichain/receive-token-link/receive-token-link.tsx b/ui/components/multichain/receive-token-link/receive-token-link.tsx index fa8079238b0f..41416833b5aa 100644 --- a/ui/components/multichain/receive-token-link/receive-token-link.tsx +++ b/ui/components/multichain/receive-token-link/receive-token-link.tsx @@ -53,6 +53,7 @@ export const ReceiveTokenLink: React.FC> = ({ )} { diff --git a/ui/components/multichain/token-list-item/token-list-item.js b/ui/components/multichain/token-list-item/token-list-item.js index 24fb218d83ba..a52ad8b77cad 100644 --- a/ui/components/multichain/token-list-item/token-list-item.js +++ b/ui/components/multichain/token-list-item/token-list-item.js @@ -36,14 +36,17 @@ import { import { ModalContent } from '../../component-library/modal-content/deprecated'; import { ModalHeader } from '../../component-library/modal-header/deprecated'; import { - getCurrentChainId, getMetaMetricsId, - getNativeCurrencyImage, getPreferences, getTestNetworkBackgroundColor, getTokensMarketData, } from '../../../selectors'; -import { getMultichainCurrentNetwork } from '../../../selectors/multichain'; +import { + getMultichainCurrentChainId, + getMultichainCurrentNetwork, + getMultichainIsEvm, + getMultichainNativeCurrencyImage, +} from '../../../selectors/multichain'; import Tooltip from '../../ui/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -76,10 +79,11 @@ export const TokenListItem = ({ address = null, }) => { const t = useI18nContext(); - const primaryTokenImage = useSelector(getNativeCurrencyImage); + const isEvm = useSelector(getMultichainIsEvm); + const primaryTokenImage = useSelector(getMultichainNativeCurrencyImage); const trackEvent = useContext(MetaMetricsContext); const metaMetricsId = useSelector(getMetaMetricsId); - const chainId = useSelector(getCurrentChainId); + const chainId = useSelector(getMultichainCurrentChainId); // Scam warning const showScamWarning = isNativeCurrency && !isOriginalTokenSymbol; @@ -92,15 +96,27 @@ export const TokenListItem = ({ const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; const history = useHistory(); + const getTokenTitle = () => { + if (!isOriginalTokenSymbol) { + return title; + } + // We only consider native token symbols! + switch (title) { + case CURRENCY_SYMBOLS.ETH: + return t('networkNameEthereum'); + case CURRENCY_SYMBOLS.BTC: + return t('networkNameBitcoin'); + default: + return title; + } + }; + const tokensMarketData = useSelector(getTokensMarketData); const tokenPercentageChange = tokensMarketData?.[address]?.pricePercentChange1d; - const tokenTitle = - title === CURRENCY_SYMBOLS.ETH && isOriginalTokenSymbol - ? t('networkNameEthereum') - : title; + const tokenTitle = getTokenTitle(); const stakeableTitle = ( )} - + {isEvm && ( + + )} {showScamWarning ? ( @@ -351,7 +371,7 @@ export const TokenListItem = ({ > - {showScamWarningModal ? ( + {isEvm && showScamWarningModal ? ( diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index dc5fa3ba64f6..a16508c9a45a 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -82,7 +82,13 @@ type AppState = { newNftAddedMessage: string; removeNftMessage: string; newNetworkAddedName: string; - editedNetwork: string; + editedNetwork: + | { + networkConfigurationId: string; + nickname: string; + editCompleted: boolean; + } + | undefined; newNetworkAddedConfigurationId: string; selectedNetworkConfigurationId: string; sendInputCurrencySwitched: boolean; @@ -163,7 +169,7 @@ const initialState: AppState = { newNftAddedMessage: '', removeNftMessage: '', newNetworkAddedName: '', - editedNetwork: '', + editedNetwork: undefined, newNetworkAddedConfigurationId: '', selectedNetworkConfigurationId: '', sendInputCurrencySwitched: false, @@ -489,10 +495,9 @@ export default function reduceApp( }; } case actionConstants.SET_EDIT_NETWORK: { - const { nickname } = action.payload; return { ...appState, - editedNetwork: nickname, + editedNetwork: action.payload, }; } case actionConstants.SET_NEW_TOKENS_IMPORTED: diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts new file mode 100644 index 000000000000..a9ce6f825b0f --- /dev/null +++ b/ui/ducks/bridge/actions.ts @@ -0,0 +1,9 @@ +import { swapsSlice } from '../swaps/swaps'; +import { bridgeSlice } from './bridge'; + +// Bridge actions + +// eslint-disable-next-line no-empty-pattern +const {} = swapsSlice.actions; + +export const { setToChain } = bridgeSlice.actions; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts new file mode 100644 index 000000000000..5aff92b54251 --- /dev/null +++ b/ui/ducks/bridge/bridge.test.ts @@ -0,0 +1,31 @@ +import nock from 'nock'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import bridgeReducer from './bridge'; +import { setToChain } from './actions'; + +const middleware = [thunk]; + +describe('Ducks - Bridge', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const store = configureMockStore(middleware)(createBridgeMockStore()); + + afterEach(() => { + nock.cleanAll(); + }); + + describe('setToChain', () => { + it('calls the "bridge/setToChain" action', () => { + const state = store.getState().bridge; + const actionPayload = CHAIN_IDS.BSC; + store.dispatch(setToChain(actionPayload)); + const actions = store.getActions(); + expect(actions[0].type).toBe('bridge/setToChain'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toChain).toBe(actionPayload); + }); + }); +}); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts new file mode 100644 index 000000000000..2f6b4aabd775 --- /dev/null +++ b/ui/ducks/bridge/bridge.ts @@ -0,0 +1,25 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { swapsSlice } from '../swaps/swaps'; + +// Only states that are not in swaps slice +type BridgeState = { + toChain: string | null; +}; + +const initialState: BridgeState = { + toChain: null, +}; + +const bridgeSlice = createSlice({ + name: 'bridge', + initialState: { ...initialState }, + reducers: { + ...swapsSlice.reducer, + setToChain: (state, action) => { + state.toChain = action.payload; + }, + }, +}); + +export { bridgeSlice }; +export default bridgeSlice.reducer; diff --git a/ui/ducks/index.js b/ui/ducks/index.js index 145dd708cb90..069bd385c093 100644 --- a/ui/ducks/index.js +++ b/ui/ducks/index.js @@ -10,7 +10,9 @@ import confirmTransactionReducer from './confirm-transaction/confirm-transaction import gasReducer from './gas/gas.duck'; import { invalidCustomNetwork, unconnectedAccount } from './alerts'; import swapsReducer from './swaps/swaps'; +import bridgeReducer from './bridge/bridge'; import historyReducer from './history/history'; +import rampsReducer from './ramps/ramps'; import confirmAlertsReducer from './confirm-alerts/confirm-alerts'; export default combineReducers({ @@ -26,6 +28,8 @@ export default combineReducers({ confirmAlerts: confirmAlertsReducer, confirmTransaction: confirmTransactionReducer, swaps: swapsReducer, + ramps: rampsReducer, + bridge: bridgeReducer, gas: gasReducer, localeMessages: localeMessagesReducer, }); diff --git a/ui/ducks/ramps/constants.ts b/ui/ducks/ramps/constants.ts new file mode 100644 index 000000000000..7a451658807e --- /dev/null +++ b/ui/ducks/ramps/constants.ts @@ -0,0 +1,137 @@ +import { AggregatorNetwork } from './types'; + +export const defaultBuyableChains: AggregatorNetwork[] = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + shortName: 'Ethereum', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 10, + chainName: 'Optimism Mainnet', + shortName: 'Optimism', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 25, + chainName: 'Cronos Mainnet', + shortName: 'Cronos', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 56, + chainName: 'BNB Chain Mainnet', + shortName: 'BNB Chain', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 100, + chainName: 'Gnosis Mainnet', + shortName: 'Gnosis', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 137, + chainName: 'Polygon Mainnet', + shortName: 'Polygon', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 250, + chainName: 'Fantom Mainnet', + shortName: 'Fantom', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 324, + chainName: 'zkSync Era Mainnet', + shortName: 'zkSync Era', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1101, + chainName: 'Polygon zkEVM', + shortName: 'Polygon zkEVM', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1284, + chainName: 'Moonbeam Mainnet', + shortName: 'Moonbeam', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1285, + chainName: 'Moonriver Mainnet', + shortName: 'Moonriver', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 8453, + chainName: 'Base Mainnet', + shortName: 'Base', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42161, + chainName: 'Arbitrum Mainnet', + shortName: 'Arbitrum', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42220, + chainName: 'Celo Mainnet', + shortName: 'Celo', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 43114, + chainName: 'Avalanche C-Chain Mainnet', + shortName: 'Avalanche C-Chain', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 59144, + chainName: 'Linea', + shortName: 'Linea', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1313161554, + chainName: 'Aurora Mainnet', + shortName: 'Aurora', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 1666600000, + chainName: 'Harmony Mainnet (Shard 0)', + shortName: 'Harmony (Shard 0)', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 11297108109, + chainName: 'Palm Mainnet', + shortName: 'Palm', + nativeTokenSupported: false, + }, +]; diff --git a/ui/ducks/ramps/index.ts b/ui/ducks/ramps/index.ts new file mode 100644 index 000000000000..b6f8f2473810 --- /dev/null +++ b/ui/ducks/ramps/index.ts @@ -0,0 +1 @@ +export * from './ramps'; diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts new file mode 100644 index 000000000000..c4ca4089815a --- /dev/null +++ b/ui/ducks/ramps/ramps.test.ts @@ -0,0 +1,187 @@ +import { configureStore, Store } from '@reduxjs/toolkit'; +import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; +import { getCurrentChainId, getUseExternalServices } from '../../selectors'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import rampsReducer, { + fetchBuyableChains, + getBuyableChains, + getIsNativeTokenBuyable, +} from './ramps'; +import { defaultBuyableChains } from './constants'; + +jest.mock('../../helpers/ramps/rampApi/rampAPI'); +const mockedRampAPI = RampAPI as jest.Mocked; + +jest.mock('../../selectors', () => ({ + getCurrentChainId: jest.fn(), + getUseExternalServices: jest.fn(), + getNames: jest.fn(), +})); + +describe('rampsSlice', () => { + let store: Store; + + beforeEach(() => { + store = configureStore({ + reducer: { + ramps: rampsReducer, + }, + }); + mockedRampAPI.getNetworks.mockReset(); + }); + + it('should set the initial state to defaultBuyableChains', () => { + const { ramps: rampsState } = store.getState(); + expect(rampsState).toEqual({ + buyableChains: defaultBuyableChains, + }); + }); + + describe('setBuyableChains', () => { + it('should update the buyableChains state when setBuyableChains is dispatched', () => { + const mockBuyableChains = [{ chainId: '0x1' }]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(mockBuyableChains); + }); + it('should disregard invalid array and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: 'Invalid array', + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should disregard empty array and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: [], + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should disregard array with invalid elements and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: ['some invalid', 'element'], + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + }); + + describe('getBuyableChains', () => { + it('returns buyableChains', () => { + const state = store.getState(); + expect(getBuyableChains(state)).toBe(state.ramps.buyableChains); + }); + }); + + describe('fetchBuyableChains', () => { + beforeEach(() => { + // simulate the Basic Functionality Toggle being on + const getUseExternalServicesMock = jest.mocked(getUseExternalServices); + getUseExternalServicesMock.mockReturnValue(true); + }); + + it('should call RampAPI.getNetworks when the Basic Functionality Toggle is on', async () => { + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + expect(RampAPI.getNetworks).toHaveBeenCalledTimes(1); + }); + + it('should not call RampAPI.getNetworks when the Basic Functionality Toggle is off', async () => { + const getUseExternalServicesMock = jest.mocked(getUseExternalServices); + getUseExternalServicesMock.mockReturnValue(false); + + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + + expect(RampAPI.getNetworks).not.toHaveBeenCalled(); + }); + + it('should update the state with the data that is returned', async () => { + const mockBuyableChains = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + shortName: 'Ethereum', + }, + ]; + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(mockBuyableChains); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(mockBuyableChains); + }); + it('should set state to defaultBuyableChains when returned networks are undefined', async () => { + // @ts-expect-error forcing undefined to test the behavior + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(undefined); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should set state to defaultBuyableChains when returned networks are empty', async () => { + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue([]); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should set state to defaultBuyableChains when API request fails', async () => { + jest + .spyOn(RampAPI, 'getNetworks') + .mockRejectedValue(new Error('API error')); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + }); + + describe('getIsNativeTokenBuyable', () => { + const getCurrentChainIdMock = jest.mocked(getCurrentChainId); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return true when current chain is buyable', () => { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(true); + }); + + it('should return false when current chain is not buyable', () => { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.GOERLI); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(false); + }); + + it('should return false when current chain is not a valid hex string', () => { + getCurrentChainIdMock.mockReturnValue('0x'); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(false); + }); + + it('should return false when buyable chains is a corrupted array', () => { + const mockState = { + ramps: { + buyableChains: [null, null, null], + }, + }; + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + expect(getIsNativeTokenBuyable(mockState)).toEqual(false); + }); + }); +}); diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts new file mode 100644 index 000000000000..afff609cd4d8 --- /dev/null +++ b/ui/ducks/ramps/ramps.ts @@ -0,0 +1,78 @@ +import { createSelector } from 'reselect'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { getCurrentChainId, getUseExternalServices } from '../../selectors'; +import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; +import { hexToDecimal } from '../../../shared/modules/conversion.utils'; +import { defaultBuyableChains } from './constants'; +import { AggregatorNetwork } from './types'; + +export const fetchBuyableChains = createAsyncThunk( + 'ramps/fetchBuyableChains', + async (_, { getState }) => { + const state = getState(); + const allowExternalRequests = getUseExternalServices(state); + if (!allowExternalRequests) { + return defaultBuyableChains; + } + return await RampAPI.getNetworks(); + }, +); + +const rampsSlice = createSlice({ + name: 'ramps', + initialState: { + buyableChains: defaultBuyableChains, + }, + reducers: { + setBuyableChains: (state, action) => { + if ( + Array.isArray(action.payload) && + action.payload.length > 0 && + action.payload.every((network) => network?.chainId) + ) { + state.buyableChains = action.payload; + } else { + state.buyableChains = defaultBuyableChains; + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchBuyableChains.fulfilled, (state, action) => { + const networks = action.payload; + if (networks && networks.length > 0) { + state.buyableChains = networks; + } else { + state.buyableChains = defaultBuyableChains; + } + }) + .addCase(fetchBuyableChains.rejected, (state) => { + state.buyableChains = defaultBuyableChains; + }); + }, +}); + +const { reducer } = rampsSlice; + +// Can be typed to RootState if/when the interface is defined +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getBuyableChains = (state: any) => + state.ramps?.buyableChains ?? defaultBuyableChains; + +export const getIsNativeTokenBuyable = createSelector( + [getCurrentChainId, getBuyableChains], + (currentChainId, buyableChains) => { + try { + return buyableChains + .filter(Boolean) + .some( + (network: AggregatorNetwork) => + String(network.chainId) === hexToDecimal(currentChainId), + ); + } catch (e) { + return false; + } + }, +); + +export default reducer; diff --git a/ui/ducks/ramps/types.ts b/ui/ducks/ramps/types.ts new file mode 100644 index 000000000000..6a1571715dfe --- /dev/null +++ b/ui/ducks/ramps/types.ts @@ -0,0 +1,7 @@ +export type AggregatorNetwork = { + active: boolean; + chainId: number; + chainName: string; + nativeTokenSupported: boolean; + shortName: string; +}; diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 59fadda433d5..9c15aec7435b 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -511,6 +511,7 @@ export { swapCustomGasModalLimitEdited, swapCustomGasModalClosed, setTransactionSettingsOpened, + slice as swapsSlice, }; export const navigateBackToBuildQuote = (history) => { diff --git a/ui/helpers/ramps/rampApi/rampAPI.test.ts b/ui/helpers/ramps/rampApi/rampAPI.test.ts new file mode 100644 index 000000000000..bf6e2297481d --- /dev/null +++ b/ui/helpers/ramps/rampApi/rampAPI.test.ts @@ -0,0 +1,23 @@ +import nock from 'nock'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; +import rampAPI from './rampAPI'; + +const mockedResponse = { + networks: defaultBuyableChains, +}; + +describe('rampAPI', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('should fetch networks', async () => { + nock('https://on-ramp-content.uat-api.cx.metamask.io') + .get('/regions/networks') + .query(true) + .reply(200, mockedResponse); + + const result = await rampAPI.getNetworks(); + expect(result).toStrictEqual(mockedResponse.networks); + }); +}); diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts new file mode 100644 index 000000000000..a1da6da8ef0c --- /dev/null +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -0,0 +1,25 @@ +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; +import { AggregatorNetwork } from '../../../ducks/ramps/types'; + +const fetchWithTimeout = getFetchWithTimeout(); + +const isProdEnv = process.env.NODE_ENV === 'production'; +const PROD_RAMP_API_BASE_URL = 'https://on-ramp-content.api.cx.metamask.io'; +const UAT_RAMP_API_BASE_URL = 'https://on-ramp-content.uat-api.cx.metamask.io'; + +const rampApiBaseUrl = + process.env.METAMASK_RAMP_API_CONTENT_BASE_URL || + (isProdEnv ? PROD_RAMP_API_BASE_URL : UAT_RAMP_API_BASE_URL); + +const RampAPI = { + async getNetworks(): Promise { + const url = new URL('/regions/networks', rampApiBaseUrl); + url.searchParams.set('context', 'extension'); + const response = await fetchWithTimeout(url.toString()); + + const { networks } = await response.json(); + return networks; + }, +}; + +export default RampAPI; diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts new file mode 100644 index 000000000000..9e6356d99941 --- /dev/null +++ b/ui/hooks/bridge/useBridging.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { fetchBridgeFeatureFlags } from '../../pages/bridge/bridge.util'; +import { setBridgeFeatureFlags } from '../../store/actions'; + +const useBridging = () => { + const dispatch = useDispatch(); + + useEffect(() => { + fetchBridgeFeatureFlags().then((bridgeFeatureFlags) => { + dispatch(setBridgeFeatureFlags(bridgeFeatureFlags)); + }); + }, [dispatch, setBridgeFeatureFlags]); +}; +export default useBridging; diff --git a/ui/hooks/experiences/useRamps.test.js b/ui/hooks/ramps/useRamps/useRamps.test.tsx similarity index 85% rename from ui/hooks/experiences/useRamps.test.js rename to ui/hooks/ramps/useRamps/useRamps.test.tsx index dcb1dc2eb43c..9c53eb9da247 100644 --- a/ui/hooks/experiences/useRamps.test.js +++ b/ui/hooks/ramps/useRamps/useRamps.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { FC } from 'react'; import { Provider } from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; -import configureStore from '../../store/store'; +import configureStore from '../../../store/store'; import useRamps, { RampsMetaMaskEntry } from './useRamps'; const mockedMetametricsId = '0xtestMetaMetricsId'; @@ -15,17 +15,18 @@ let mockStoreState = { }, }; -const wrapper = ({ children }) => ( +const wrapper: FC = ({ children }) => ( {children} ); describe('useRamps', () => { - beforeEach(() => { - global.platform = { openTab: jest.fn() }; - }); - - afterEach(() => { - jest.clearAllMocks(); + // mock the openTab function to test if it is called with the correct URL when opening the Pdapp + beforeAll(() => { + Object.defineProperty(global, 'platform', { + value: { + openTab: jest.fn(), + }, + }); }); it('should default the metamask entry param when opening the buy crypto URL', () => { @@ -81,9 +82,11 @@ describe('useRamps', () => { }); }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore it.each(['0x1', '0x38', '0xa'])( 'should open the buy crypto URL with the currently connected chain ID', - (mockChainId) => { + (mockChainId: string) => { mockStoreState = { ...mockStoreState, metamask: { diff --git a/ui/hooks/experiences/useRamps.ts b/ui/hooks/ramps/useRamps/useRamps.ts similarity index 62% rename from ui/hooks/experiences/useRamps.ts rename to ui/hooks/ramps/useRamps/useRamps.ts index 76bcdfe47879..aa8ccb1c76d2 100644 --- a/ui/hooks/experiences/useRamps.ts +++ b/ui/hooks/ramps/useRamps/useRamps.ts @@ -1,12 +1,12 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import type { Hex } from '@metamask/utils'; -import { ChainId } from '../../../shared/constants/network'; -import { getCurrentChainId, getMetaMetricsId } from '../../selectors'; +import { CaipChainId } from '@metamask/utils'; +import { ChainId } from '../../../../shared/constants/network'; +import { getCurrentChainId, getMetaMetricsId } from '../../../selectors'; type IUseRamps = { - openBuyCryptoInPdapp: VoidFunction; - getBuyURI: (chainId: ChainId) => string; + openBuyCryptoInPdapp: (chainId?: ChainId | CaipChainId) => void; + getBuyURI: (chainId: ChainId | CaipChainId) => string; }; export enum RampsMetaMaskEntry { @@ -24,7 +24,7 @@ const useRamps = ( const metaMetricsId = useSelector(getMetaMetricsId); const getBuyURI = useCallback( - (_chainId: Hex) => { + (_chainId: ChainId | CaipChainId) => { const params = new URLSearchParams(); params.set('metamaskEntry', metamaskEntry); params.set('chainId', _chainId); @@ -36,12 +36,15 @@ const useRamps = ( [metaMetricsId], ); - const openBuyCryptoInPdapp = useCallback(() => { - const buyUrl = getBuyURI(chainId); - global.platform.openTab({ - url: buyUrl, - }); - }, [chainId]); + const openBuyCryptoInPdapp = useCallback( + (_chainId?: ChainId | CaipChainId) => { + const buyUrl = getBuyURI(_chainId || chainId); + global.platform.openTab({ + url: buyUrl, + }); + }, + [chainId], + ); return { openBuyCryptoInPdapp, getBuyURI }; }; diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index f8c313118554..a7798d5ff10e 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -1,18 +1,18 @@ import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import BigNumber from 'bignumber.js'; import { formatCurrency } from '../helpers/utils/confirm-tx.util'; import { getMultichainCurrentCurrency, getMultichainIsEvm, getMultichainNativeCurrency, + getMultichainConversionRate, } from '../selectors/multichain'; -import { getConversionRate } from '../ducks/metamask/metamask'; import { getValueFromWeiHex } from '../../shared/modules/conversion.utils'; import { TEST_NETWORK_TICKER_MAP } from '../../shared/constants/network'; import { Numeric } from '../../shared/modules/Numeric'; import { EtherDenomination } from '../../shared/constants/common'; +import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { useMultichainSelector } from './useMultichainSelector'; // The smallest non-zero amount that can be displayed. @@ -26,6 +26,74 @@ const MIN_AMOUNT_DISPLAY = `<${MIN_AMOUNT}`; // It set to the number of decimal places in the minimum amount. export const DEFAULT_PRECISION = new BigNumber(MIN_AMOUNT).decimalPlaces(); +function formatEthCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + nativeCurrency, + inputValue, + conversionRate, + denomination, + numberOfDecimals, +}) { + if (isNativeCurrency || (!isUserPreferredCurrency && !nativeCurrency)) { + const ethDisplayValue = new Numeric(inputValue, 16, EtherDenomination.WEI) + .toDenomination(denomination || EtherDenomination.ETH) + .round(numberOfDecimals || DEFAULT_PRECISION) + .toBase(10) + .toString(); + + return ethDisplayValue === '0' && inputValue && Number(inputValue) !== 0 + ? MIN_AMOUNT_DISPLAY + : ethDisplayValue; + } else if (isUserPreferredCurrency && conversionRate) { + return formatCurrency( + getValueFromWeiHex({ + value: inputValue, + fromCurrency: nativeCurrency, + toCurrency: currency, + conversionRate, + numberOfDecimals: numberOfDecimals || 2, + toDenomination: denomination, + }), + currency, + ); + } + return null; +} + +function formatBtcCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + currentCurrency, + nativeCurrency, + inputValue, + conversionRate, +}) { + if (isNativeCurrency || (!isUserPreferredCurrency && !nativeCurrency)) { + // NOTE: We use the value coming from the BalancesController here (and thus, the non-EVM + // account Snap). + // We use `Numeric` here, so we handle those amount the same way than for EVMs (it's worth + // noting that if `inputValue` is not properly defined, the amount will be set to '0', see + // `Numeric` constructor for that) + return new Numeric(inputValue, 10).toString(); // BTC usually uses 10 digits + } else if (isUserPreferredCurrency && conversionRate) { + const amount = + getTokenFiatAmount( + 1, // coin to native conversion rate is 1:1 + Number(conversionRate), // native to fiat conversion rate + currentCurrency, + inputValue, + 'BTC', + false, + false, + ) ?? '0'; // if the conversion fails, return 0 + return formatCurrency(amount, currency); + } + return null; +} + /** * Defines the shape of the options parameter for useCurrencyDisplay * @@ -79,60 +147,53 @@ export function useCurrencyDisplay( getMultichainNativeCurrency, account, ); - const conversionRate = useSelector(getConversionRate); + const conversionRate = useMultichainSelector( + getMultichainConversionRate, + account, + ); const isUserPreferredCurrency = currency === currentCurrency; + const isNativeCurrency = currency === nativeCurrency; const value = useMemo(() => { if (displayValue) { return displayValue; } - if (isEvm) { - if ( - currency === nativeCurrency || - (!isUserPreferredCurrency && !nativeCurrency) - ) { - const ethDisplayValue = new Numeric( - inputValue, - 16, - EtherDenomination.WEI, - ) - .toDenomination(denomination || EtherDenomination.ETH) - .round(numberOfDecimals || DEFAULT_PRECISION) - .toBase(10) - .toString(); - - return ethDisplayValue === '0' && inputValue && Number(inputValue) !== 0 - ? MIN_AMOUNT_DISPLAY - : ethDisplayValue; - } else if (isUserPreferredCurrency && conversionRate) { - return formatCurrency( - getValueFromWeiHex({ - value: inputValue, - fromCurrency: nativeCurrency, - toCurrency: currency, - conversionRate, - numberOfDecimals: numberOfDecimals || 2, - toDenomination: denomination, - }), - currency, - ); - } - } else { - // For non-EVM we assume the input value can be formatted "as-is" - return formatCurrency(inputValue, currency); + if (!isEvm) { + // TODO: We would need to update this for other non-EVM coins + return formatBtcCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + currentCurrency, + nativeCurrency, + inputValue, + conversionRate, + }); } - return null; + + return formatEthCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + nativeCurrency, + inputValue, + conversionRate, + denomination, + numberOfDecimals, + }); }, [ - inputValue, + displayValue, + isEvm, + isNativeCurrency, + isUserPreferredCurrency, + currency, nativeCurrency, + inputValue, conversionRate, - displayValue, - numberOfDecimals, denomination, - currency, - isUserPreferredCurrency, - isEvm, + numberOfDecimals, + currentCurrency, ]); let suffix; diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.js b/ui/hooks/useIsOriginalNativeTokenSymbol.js index 47035c3fbb6e..06dc380d861d 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.js @@ -7,13 +7,17 @@ import { } from '../../shared/constants/network'; import { DAY } from '../../shared/constants/time'; import { useSafeChainsListValidationSelector } from '../selectors'; +import { + getMultichainIsEvm, + getMultichainCurrentNetwork, +} from '../selectors/multichain'; import { getValidUrl } from '../../app/scripts/lib/util'; export function useIsOriginalNativeTokenSymbol( chainId, ticker, type, - rpcUrl = null, + rpcUrl = '', ) { const [isOriginalNativeSymbol, setIsOriginalNativeSymbol] = useState(false); const useSafeChainsListValidation = useSelector( @@ -29,8 +33,16 @@ export function useIsOriginalNativeTokenSymbol( ); }; + const isEvm = useSelector(getMultichainIsEvm); + const providerConfig = useSelector(getMultichainCurrentNetwork); + useEffect(() => { async function getNativeTokenSymbol(networkId) { + if (!isEvm) { + setIsOriginalNativeSymbol(ticker === providerConfig?.ticker); + return; + } + try { if (!useSafeChainsListValidation) { setIsOriginalNativeSymbol(true); diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.test.js b/ui/hooks/useIsOriginalNativeTokenSymbol.test.js index c28070e47077..1b34c4b1ef81 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.test.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.test.js @@ -2,6 +2,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useSelector } from 'react-redux'; import * as fetchWithCacheModule from '../../shared/lib/fetch-with-cache'; import { useSafeChainsListValidationSelector } from '../selectors'; +import { getMultichainIsEvm } from '../selectors/multichain'; import { useIsOriginalNativeTokenSymbol } from './useIsOriginalNativeTokenSymbol'; // Adjust the import path accordingly jest.mock('react-redux', () => { @@ -14,13 +15,18 @@ jest.mock('react-redux', () => { }); const generateUseSelectorRouter = (opts) => (selector) => { + if (selector === getMultichainIsEvm) { + // If we consider testing non-EVM here, we would need to also mock those: + // - getMultichainCurrentNetwork + return true; + } if (selector === useSafeChainsListValidationSelector) { return opts; } return undefined; }; -describe('useNativeTokenFiatAmount', () => { +describe('useIsOriginalNativeTokenSymbol', () => { afterEach(() => { jest.clearAllMocks(); }); diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx index b1a0cb85b92d..a2f7cc108e4a 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx @@ -47,7 +47,7 @@ const mockAccount = createMockInternalAccount({ const mockNonEvmAccount = { ...mockAccount, id: 'b7893c59-e376-4cc0-93ad-05ddaab574a6', - addres: 'bc1qn3stuu6g37rpxk3jfxr4h4zmj68g0lwxx5eker', + address: 'bc1qn3stuu6g37rpxk3jfxr4h4zmj68g0lwxx5eker', type: BtcAccountType.P2wpkh, }; @@ -199,7 +199,7 @@ describe('useMultichainAccountTotalFiatBalance', () => { ], tokensWithBalances: [], totalFiatBalance: '100000', - totalWeiBalance: '', + totalBalance: '1.00000000', }); }); }); diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.ts b/ui/hooks/useMultichainAccountTotalFiatBalance.ts index 062014430928..9e807be41ea5 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.ts +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.ts @@ -35,7 +35,8 @@ export const useMultichainAccountTotalFiatBalance = ( isERC721: boolean; image: string; }[]; - totalWeiBalance: string; + totalWeiBalance?: string; + totalBalance?: string; loading: boolean; orderedTokenList: { iconUrl: string; symbol: string; fiatBalance: string }[]; } => { @@ -70,19 +71,14 @@ export const useMultichainAccountTotalFiatBalance = ( // BalancesController might not have updated it yet! return EMPTY_VALUES; } - const { amount } = - balances[account.id][ - MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19[ - ticker as keyof typeof MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 - ] - ]; + const { amount: balance } = balances[account.id][asset]; const totalFiatBalance = getTokenFiatAmount( 1, // coin to native conversion rate is 1:1 Number(conversionRate), // native to fiat conversion rate currentCurrency, - amount, + balance, ticker, false, false, @@ -101,7 +97,7 @@ export const useMultichainAccountTotalFiatBalance = ( return { formattedFiat, totalFiatBalance, - totalWeiBalance: '', // Not supported + totalBalance: balance, tokensWithBalances: [], // TODO: support tokens loading: false, // TODO: support tokens orderedTokenList: [nativeTokenValues], // TODO: support tokens diff --git a/ui/hooks/useTheme.test.ts b/ui/hooks/useTheme.test.ts index 8b0f2edd66ac..5b44d2c918eb 100644 --- a/ui/hooks/useTheme.test.ts +++ b/ui/hooks/useTheme.test.ts @@ -3,6 +3,7 @@ import { renderHookWithProvider } from '../../test/lib/render-helpers'; import { useTheme } from './useTheme'; jest.mock('../selectors', () => ({ + ...jest.requireActual('../selectors'), getTheme: jest.fn(), })); diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index d83f6cfe24f8..38ea46e475f3 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -7,7 +7,6 @@ import { isEqual } from 'lodash'; import { getCurrentCurrency, getIsBridgeChain, - getIsBuyableChain, getIsSwapsChain, getSelectedInternalAccount, getSwapsDefaultToken, @@ -42,6 +41,7 @@ import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getConversionRate } from '../../../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import CoinButtons from '../../../components/app/wallet-overview/coin-buttons'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; @@ -102,7 +102,7 @@ const AssetPage = ({ const conversionRate = useSelector(getConversionRate); const allMarketData = useSelector(getTokensMarketData); const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const account = useSelector(getSelectedInternalAccount, isEqual); const isSwapsChain = useSelector(getIsSwapsChain); diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index f0e39f046e24..6cd78fab7693 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -13,7 +13,7 @@ import { startNewDraftTransaction } from '../../../ducks/send'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -28,11 +28,9 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getIsBridgeChain, getCurrentKeyring, - getIsBuyableChain, getMetaMetricsId, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; - import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; import { showModal } from '../../../store/actions'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -42,7 +40,6 @@ import { MetaMetricsSwapsEventSource, } from '../../../../shared/constants/metametrics'; import { AssetType } from '../../../../shared/constants/transaction'; - import { Display, IconColor, @@ -50,6 +47,9 @@ import { } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; import { Box, Icon, IconName } from '../../../components/component-library'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { Asset } from './asset-page'; const TokenButtons = ({ @@ -71,7 +71,7 @@ const TokenButtons = ({ const isSwapsChain = useSelector(getIsSwapsChain); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const metaMetricsId = useSelector(getMetaMetricsId); const { openBuyCryptoInPdapp } = useRamps(); ///: END:ONLY_INCLUDE_IF diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts new file mode 100644 index 000000000000..d693f92dc956 --- /dev/null +++ b/ui/pages/bridge/bridge.util.test.ts @@ -0,0 +1,59 @@ +import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +import { fetchBridgeFeatureFlags } from './bridge.util'; + +jest.mock('../../../shared/lib/fetch-with-cache'); + +describe('Bridge utils', () => { + it('should fetch bridge feature flags successfully', async () => { + const mockResponse = { + 'extension-support': true, + }; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags(); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeFeatureFlags', + }); + + expect(result).toEqual({ extensionSupport: true }); + }); + + it('should use fallback bridge feature flags if response is unexpected', async () => { + const mockResponse = { + flag1: true, + flag2: false, + }; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags(); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeFeatureFlags', + }); + + expect(result).toEqual({ extensionSupport: false }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (fetchWithCache as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeFeatureFlags()).rejects.toThrowError(mockError); + }); +}); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts new file mode 100644 index 000000000000..d7fae190e28f --- /dev/null +++ b/ui/pages/bridge/bridge.util.ts @@ -0,0 +1,72 @@ +import { + BridgeFeatureFlagsKey, + BridgeFeatureFlags, +} from '../../../app/scripts/controllers/bridge'; +import { + BRIDGE_API_BASE_URL, + BRIDGE_CLIENT_ID, +} from '../../../shared/constants/bridge'; +import { MINUTE } from '../../../shared/constants/time'; +import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +import { validateData } from '../../../shared/lib/swaps-utils'; + +const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; +const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; + +// Types copied from Metabridge API +enum BridgeFlag { + EXTENSION_SUPPORT = 'extension-support', +} + +type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_SUPPORT]: boolean; +}; +// End of copied types + +type Validator = { + property: keyof T; + type: string; + validator: (value: unknown) => boolean; +}; + +const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, +): data is T => { + return validateData(validators, data, urlUsed); +}; + +export async function fetchBridgeFeatureFlags(): Promise { + const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; + const rawFeatureFlags = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeFeatureFlags', + }); + + if ( + validateResponse( + [ + { + property: BridgeFlag.EXTENSION_SUPPORT, + type: 'boolean', + validator: (v) => typeof v === 'boolean', + }, + ], + rawFeatureFlags, + url, + ) + ) { + return { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: + rawFeatureFlags[BridgeFlag.EXTENSION_SUPPORT], + }; + } + + return { + // TODO set default to true once bridging is live + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, + }; +} diff --git a/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap b/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap index eca476209a9c..908df23e73fa 100644 --- a/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap +++ b/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap @@ -83,6 +83,7 @@ exports[`ConfirmAddSuggestedNFT Component should match snapshot 1`] = ` >
Ethereum Mainnet
@@ -295,6 +296,7 @@ exports[`ConfirmAddSuggestedNFT Component should match snapshot 1`] = ` >
Ethereum Mainnet
diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js index d177bc6b8df2..d4a92172aed6 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js @@ -43,12 +43,11 @@ import { getAccountName, getAddressBookEntry, getInternalAccounts, - getIsBuyableChain, getMetadataContractName, getNetworkIdentifier, getSwapsDefaultToken, } from '../../../../selectors'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { @@ -58,6 +57,7 @@ import { ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -118,7 +118,7 @@ const ConfirmPageContainer = (props) => { const [collectionBalance, setCollectionBalance] = useState('0'); const [isShowingTxInsightWarnings, setIsShowingTxInsightWarnings] = useState(false); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const contact = useSelector((state) => getAddressBookEntry(state, toAddress)); const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); @@ -131,10 +131,6 @@ const ConfirmPageContainer = (props) => { getMetadataContractName(state, toAddress), ); - // TODO: Move useRamps hook to the confirm-transaction-base parent component. - // TODO: openBuyCryptoInPdapp should be passed to this component as a custom prop. - // We try to keep this component for layout purpose only, we need to move this hook to the confirm-transaction-base parent - // component once it is converted to a functional component const { openBuyCryptoInPdapp } = useRamps(); const isSetApproveForAll = diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js index be3ba7793eaf..db07ad4117e7 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { getAddressBookEntry, - getIsBuyableChain, getNetworkIdentifier, getSwapsDefaultToken, getMetadataContractName, @@ -12,7 +11,6 @@ import ConfirmPageContainer from './confirm-page-container.component'; function mapStateToProps(state, ownProps) { const to = ownProps.toAddress; - const isBuyableChain = getIsBuyableChain(state); const contact = getAddressBookEntry(state, to); const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); @@ -23,7 +21,6 @@ function mapStateToProps(state, ownProps) { const toMetadataName = getMetadataContractName(state, to); return { - isBuyableChain, contact, toName, toMetadataName, diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index 6d48f42ae420..a3d3e1bbc71d 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -65,17 +65,29 @@ const ConfirmButton = ({ onSubmit={onSubmit} /> )} - + {hasDangerAlerts ? ( + + ) : ( + + )} ); }; diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap index 1e62756cf223..d6b27a2629ad 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap @@ -241,6 +241,30 @@ exports[`SIWESignInfo renders correctly for SIWE signature request 1`] = `

+
+
+

+ Request ID +

+
+
+

+ some_id +

+
+
`; diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx index 94389b253450..8c00c16f9c02 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx @@ -44,7 +44,7 @@ const SIWESignInfo: React.FC = () => { return ( <> - + diff --git a/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap b/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap index 005650a69853..c2df41b7f906 100644 --- a/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap @@ -71,6 +71,7 @@ exports[`SignatureRequestHeader should match snapshot 1`] = ` >
goerli
diff --git a/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap b/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap index 9ed331fefe28..c9d4d342a59b 100644 --- a/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap @@ -147,6 +147,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = ` >
goerli
diff --git a/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap b/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap index a1219a561ba7..5d6a208872d2 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap @@ -144,6 +144,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`] >
goerli
diff --git a/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap b/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap index 5aabba67b7d2..30011e96a607 100644 --- a/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap +++ b/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap @@ -144,6 +144,7 @@ exports[`Signature Request Component render should match snapshot when we are us >
Localhost 8545
@@ -916,6 +917,7 @@ exports[`Signature Request Component render should match snapshot when we want t >
Localhost 8545
diff --git a/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js b/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js index a80bd4b6a159..0edd91ad42e0 100644 --- a/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js +++ b/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js @@ -29,6 +29,7 @@ export default function SimulationErrorMessage({ ], }, }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return userAcknowledgedGasMissing === true ? ( diff --git a/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap b/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap index 1e600744a876..a10be0a7a2b2 100644 --- a/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap +++ b/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap @@ -144,6 +144,7 @@ exports[`Confirm Signature Request Component render should match snapshot 1`] = >
Goerli test network
diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index abe09d6f63dc..dc225367090c 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -47,7 +47,6 @@ import { getPreferences, doesAddressRequireLedgerHidConnection, getTokenList, - getIsBuyableChain, getEnsResolutionByAddress, getUnapprovedTransaction, getFullTxData, @@ -109,6 +108,7 @@ import { showCustodyConfirmLink } from '../../../store/institutional/institution ///: END:ONLY_INCLUDE_IF import { calcGasTotal } from '../../../../shared/lib/transactions-controller-utils'; import { subtractHexes } from '../../../../shared/modules/conversion.utils'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import ConfirmTransactionBase from './confirm-transaction-base.component'; let customNonceValue = ''; @@ -164,7 +164,7 @@ const mapStateToProps = (state, ownProps) => { const isGasEstimatesLoading = getIsGasEstimatesLoading(state); const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); - const isBuyableChain = getIsBuyableChain(state); + const isBuyableChain = getIsNativeTokenBuyable(state); const { confirmTransaction, metamask } = state; const conversionRate = getConversionRate(state); const { addressBook, nextNonce } = metamask; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index 918c24ed6681..bfa23af0af75 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -27,6 +27,7 @@ import { BlockaidReason, BlockaidResultType, } from '../../../../shared/constants/security-provider'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import ConfirmTransactionBase from './confirm-transaction-base.container'; @@ -203,6 +204,9 @@ const baseStore = { appState: { sendInputCurrencySwitched: false, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const mockedStoreWithConfirmTxParams = ( @@ -497,6 +501,7 @@ describe('Confirm Transaction Base', () => { it('handleMMISubmit calls sendTransaction correctly and then showCustodianDeepLink', async () => { const state = { + ...baseStore, appState: { ...baseStore.appState, gasLoadingAnimationIsShowing: false, diff --git a/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts new file mode 100644 index 000000000000..bfb63ee8496f --- /dev/null +++ b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts @@ -0,0 +1,135 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { Severity } from '../../../../../helpers/constants/design-system'; +import { renderHookWithProvider } from '../../../../../../test/lib/render-helpers'; +import mockState from '../../../../../../test/data/mock-state.json'; +import useDomainMismatchAlert from './useDomainMismatchAlerts'; + +const MOCK_ORIGIN = 'https://example-dapp.example'; +const MOCK_SUSPICIOUS_DOMAIN = 'http://suspicious.example'; +const MOCK_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + +const mockSiwe = { + isSIWEMessage: true, + parsedMessage: { + domain: MOCK_SUSPICIOUS_DOMAIN, + address: MOCK_ADDRESS, + statement: + 'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos', + uri: 'http://localhost:8080', + version: '1', + nonce: 'STMt6KQMwwdOXE306', + chainId: 1, + issuedAt: '2023-03-18T21:40:40.823Z', + resources: [ + 'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu', + 'https://example.com/my-web2-claim.json', + ], + }, +}; + +const mockCurrentConfirmation = { + id: '1', + status: 'unapproved', + time: new Date().getTime(), + type: ApprovalType.PersonalSign, + msgParams: { + from: MOCK_ADDRESS, + data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', + origin: MOCK_ORIGIN, + siwe: mockSiwe, + }, +}; + +const mockExpectedState = { + ...mockState, + metamask: { + ...mockState.metamask, + unapprovedPersonalMsgs: { + '1': { ...mockCurrentConfirmation }, + }, + pendingApprovals: { + '1': { + ...mockCurrentConfirmation, + // origin: MOCK_ORIGIN, + requestData: {}, + requestState: null, + expectsResult: false, + }, + }, + preferences: { redesignedConfirmationsEnabled: true }, + }, + confirm: { currentConfirmation: mockCurrentConfirmation }, +}; + +describe('useDomainMismatchAlert', () => { + beforeAll(() => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'true'; + }); + + afterAll(() => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'false'; + }); + + describe('returns an empty array', () => { + it('when there is no current confirmation', () => { + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + mockState, + ); + expect(result.current).toEqual([]); + }); + + it('when the current confirmation is not a SIWE request', () => { + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + { + ...mockExpectedState, + confirm: { + currentConfirmation: { + ...mockCurrentConfirmation, + msgParams: { + ...mockCurrentConfirmation.msgParams, + siwe: { + isSIWEMessage: false, + parsedMessage: mockSiwe.parsedMessage, + }, + }, + }, + }, + }, + ); + expect(result.current).toEqual([]); + }); + + it('when the SIWE domain matches origin', () => { + const originalDomain = mockSiwe.parsedMessage.domain; + mockSiwe.parsedMessage.domain = MOCK_ORIGIN; + + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + mockExpectedState, + ); + expect(result.current).toEqual([]); + + mockSiwe.parsedMessage.domain = originalDomain; + }); + }); + + it('returns an alert when the SIWE domain does not match the origin', () => { + const alertResponseExpected = { + field: 'requestFrom', + key: 'requestFrom', + message: + 'The site making the request is not the site you’re signing into. This could be an attempt to steal your login credentials.', + reason: 'Suspicious sign-in request', + severity: Severity.Danger, + }; + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + mockExpectedState, + ); + + expect(result.current).toHaveLength(1); + expect(result.current[0]).toStrictEqual(alertResponseExpected); + }); +}); diff --git a/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts new file mode 100644 index 000000000000..f75aa657ec2c --- /dev/null +++ b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { + isValidSIWEOrigin, + WrappedSIWERequest, +} from '@metamask/controller-utils'; + +import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; +import { Severity } from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { currentConfirmationSelector } from '../../../../../selectors'; + +import { SignatureRequestType } from '../../../types/confirm'; +import { isSIWESignatureRequest } from '../../../utils'; + +export default function useDomainMismatchAlerts(): Alert[] { + const t = useI18nContext(); + + const currentConfirmation = useSelector( + currentConfirmationSelector, + ) as SignatureRequestType; + const { msgParams } = currentConfirmation || {}; + + const isSIWE = isSIWESignatureRequest(currentConfirmation); + const isInvalidSIWEDomain = + isSIWE && !isValidSIWEOrigin(msgParams as WrappedSIWERequest); + + const alerts = useMemo(() => { + if (!isInvalidSIWEDomain) { + return []; + } + + return [ + { + field: 'requestFrom', + key: 'requestFrom', + message: t('alertMessageSignInDomainMismatch'), + reason: t('alertReasonSignIn'), + severity: Severity.Danger, + }, + ] as Alert[]; + }, [isInvalidSIWEDomain, t]); + + return alerts; +} diff --git a/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts b/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts index fae80bc53316..b02ef8f809a6 100644 --- a/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts +++ b/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { AlertActionKey } from '../../../components/app/confirm/info/row/constants'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { useTransactionModalContext } from '../../../contexts/transaction-modal'; const useConfirmationAlertActions = () => { diff --git a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts index 5bbfe2a721e2..9b93b285b871 100644 --- a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts +++ b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { Alert } from '../../../ducks/confirm-alerts/confirm-alerts'; import useBlockaidAlerts from './alerts/useBlockaidAlerts'; +import useDomainMismatchAlerts from './alerts/signatures/useDomainMismatchAlerts'; import { useInsufficientBalanceAlerts } from './alerts/transactions/useInsufficientBalanceAlerts'; import { useGasEstimateFailedAlerts } from './alerts/transactions/useGasEstimateFailedAlerts'; import { usePendingTransactionAlerts } from './alerts/transactions/usePendingTransactionAlerts'; @@ -10,6 +11,12 @@ import { useGasTooLowAlerts } from './alerts/transactions/useGasTooLowAlerts'; import { useNoGasPriceAlerts } from './alerts/transactions/useNoGasPriceAlerts'; import { useNetworkBusyAlerts } from './alerts/transactions/useNetworkBusyAlerts'; +function useSignatureAlerts(): Alert[] { + const domainMismatchAlerts = useDomainMismatchAlerts(); + + return useMemo(() => [...domainMismatchAlerts], [domainMismatchAlerts]); +} + function useTransactionAlerts(): Alert[] { const gasEstimateFailedAlerts = useGasEstimateFailedAlerts(); const gasFeeLowAlerts = useGasFeeLowAlerts(); @@ -46,10 +53,11 @@ function useTransactionAlerts(): Alert[] { export default function useConfirmationAlerts(): Alert[] { const blockaidAlerts = useBlockaidAlerts(); + const signatureAlerts = useSignatureAlerts(); const transactionAlerts = useTransactionAlerts(); return useMemo( - () => [...blockaidAlerts, ...transactionAlerts], - [blockaidAlerts, transactionAlerts], + () => [...blockaidAlerts, ...signatureAlerts, ...transactionAlerts], + [blockaidAlerts, signatureAlerts, transactionAlerts], ); } diff --git a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts index 77356e848cde..880eedca7399 100644 --- a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts +++ b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts @@ -63,6 +63,10 @@ const useCurrentConfirmation = () => { if ( !redesignedConfirmationsEnabled || (!isCorrectTransactionType && !isCorrectApprovalType) || + /** + * @todo remove isSIWE check when we want to enable SIWE in redesigned confirmations + * @see {@link https://github.com/MetaMask/metamask-extension/issues/24617} + */ isSIWE ) { return { currentConfirmation: undefined }; diff --git a/ui/pages/confirmations/hooks/useTransactionFunction.test.js b/ui/pages/confirmations/hooks/useTransactionFunction.test.js index cc4e69863892..22984ba71a37 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunction.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunction.test.js @@ -22,6 +22,7 @@ useGasEstimates.mockImplementation(() => FEE_MARKET_ESTIMATE_RETURN_VALUE); jest.mock('../../../selectors', () => ({ checkNetworkAndAccountSupports1559: () => true, + getCurrentChainId: jest.fn().mockReturnValue('0x1'), })); const wrapper = ({ children }) => ( diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index e07c5473bdb9..5fbad8445cd6 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -23,7 +23,6 @@ import TransactionDetail from '../../components/transaction-detail'; import ActionableMessage from '../../../../components/ui/actionable-message'; import { getPreferences, - getIsBuyableChain, transactionFeeSelector, getIsTestnet, getUseCurrencyRateCheck, @@ -46,7 +45,8 @@ import { MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); @@ -58,7 +58,7 @@ export default function GasDisplay({ gasError }) { const providerConfig = useSelector(getProviderConfig); const isTestnet = useSelector(getIsTestnet); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const { showFiatInTestnets, useNativeCurrencyAsPrimaryCurrency } = diff --git a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap index d8ff7862a6da..91ac3c735f84 100644 --- a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap +++ b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap @@ -163,6 +163,7 @@ exports[`TokenAllowancePage when mounted should match snapshot 1`] = ` >
mainnet
diff --git a/ui/pages/confirmations/types/confirm.ts b/ui/pages/confirmations/types/confirm.ts index 2050aae32c05..36a1b6a31397 100644 --- a/ui/pages/confirmations/types/confirm.ts +++ b/ui/pages/confirmations/types/confirm.ts @@ -1,4 +1,6 @@ import { ApprovalControllerState } from '@metamask/approval-controller'; +import { SIWEMessage } from '@metamask/controller-utils'; + import { TransactionMeta, TransactionType, @@ -27,21 +29,7 @@ export type SignatureRequestType = { data: string | TypedSignDataV1Type; version?: string; signatureMethod?: string; - siwe?: { - isSIWEMessage: boolean; - parsedMessage: null | { - domain: string; - address: string; - statement: string; - uri: string; - version: string; - chainId: number; - nonce: string; - issuedAt: string; - requestId?: string; - resources?: string[]; - }; - }; + siwe?: SIWEMessage; }; type: TransactionType; custodyId?: string; diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 3325f8dd81d1..73fa4450d3f5 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -46,7 +46,7 @@ export const parseSanitizeTypedDataMessage = (dataToParse: string) => { }; export const isSIWESignatureRequest = (request: SignatureRequestType) => - request.msgParams?.siwe?.isSIWEMessage; + Boolean(request?.msgParams?.siwe?.isSIWEMessage); export const isPermitSignatureRequest = (request: SignatureRequestType) => { if ( diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 3a3dfc7ed753..33ca2b67a181 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -25,8 +25,8 @@ import Popover from '../../components/ui/popover'; import ConnectedSites from '../connected-sites'; import ConnectedAccounts from '../connected-accounts'; import { isMv3ButOffscreenDocIsMissing } from '../../../shared/modules/mv3.utils'; - import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; + import { FontWeight, Display, @@ -80,6 +80,7 @@ import { ///: END:ONLY_INCLUDE_IF } from '../../../shared/lib/ui-utils'; import { AccountOverview } from '../../components/multichain/account-overview'; +import { setEditedNetwork } from '../../store/actions'; ///: BEGIN:ONLY_INCLUDE_IF(build-beta) import BetaHomeFooter from './beta/beta-home-footer.component'; ///: END:ONLY_INCLUDE_IF @@ -185,7 +186,7 @@ export default class Home extends PureComponent { showOutdatedBrowserWarning: PropTypes.bool.isRequired, setOutdatedBrowserWarningLastShown: PropTypes.func.isRequired, newNetworkAddedName: PropTypes.string, - editedNetwork: PropTypes.string, + editedNetwork: PropTypes.object, // This prop is used in the `shouldCloseNotificationPopup` function // eslint-disable-next-line react/no-unused-prop-types isSigningQRHardwareTransaction: PropTypes.bool.isRequired, @@ -220,6 +221,7 @@ export default class Home extends PureComponent { custodianDeepLink: PropTypes.object, accountType: PropTypes.string, ///: END:ONLY_INCLUDE_IF + fetchBuyableChains: PropTypes.func.isRequired, }; state = { @@ -362,6 +364,8 @@ export default class Home extends PureComponent { setWaitForConfirmDeepLinkDialog(false); }); ///: END:ONLY_INCLUDE_IF + + this.props.fetchBuyableChains(); } static getDerivedStateFromProps(props) { @@ -496,7 +500,7 @@ export default class Home extends PureComponent { setRemoveNftMessage(''); setNewTokensImported(''); // Added this so we dnt see the notif if user does not close it setNewTokensImportedError(''); - clearEditedNetwork({}); + setEditedNetwork(); }; const autoHideDelay = 5 * SECOND; @@ -603,7 +607,7 @@ export default class Home extends PureComponent { } /> ) : null} - {editedNetwork ? ( + {editedNetwork?.editCompleted ? ( - {t('newNetworkEdited', [editedNetwork])} + {t('newNetworkEdited', [editedNetwork.nickname])} { dispatch(setNewNetworkAdded({})); }, clearEditedNetwork: () => { - dispatch(setEditedNetwork({})); + dispatch(setEditedNetwork()); }, setActiveNetwork: (networkConfigurationId) => { dispatch(setActiveNetwork(networkConfigurationId)); @@ -319,6 +320,7 @@ const mapDispatchToProps = (dispatch) => { ///: END:ONLY_INCLUDE_IF setBasicFunctionalityModalOpen: () => dispatch(openBasicFunctionalityModal()), + fetchBuyableChains: () => dispatch(fetchBuyableChains()), }; }; diff --git a/ui/pages/onboarding-flow/add-network-modal/index.js b/ui/pages/onboarding-flow/add-network-modal/index.js index c031739b68b6..b35785e4f2ac 100644 --- a/ui/pages/onboarding-flow/add-network-modal/index.js +++ b/ui/pages/onboarding-flow/add-network-modal/index.js @@ -19,6 +19,7 @@ export default function AddNetworkModal({ isNewNetworkFlow = false, addNewNetwork = true, networkToEdit = null, + onRpcUrlAdd, }) { const dispatch = useDispatch(); const t = useI18nContext(); @@ -50,6 +51,7 @@ export default function AddNetworkModal({ networksToRender={[]} cancelCallback={closeCallback} submitCallback={closeCallback} + onRpcUrlAdd={onRpcUrlAdd} isNewNetworkFlow={isNewNetworkFlow} {...additionalProps} /> @@ -62,6 +64,7 @@ AddNetworkModal.propTypes = { isNewNetworkFlow: PropTypes.bool, addNewNetwork: PropTypes.bool, networkToEdit: PropTypes.object, + onRpcUrlAdd: PropTypes.func, }; AddNetworkModal.defaultProps = { diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index bec55f91a15e..9fe51d046eec 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -209,6 +209,7 @@ export default class Routes extends Component { newPrivacyPolicyToastShownDate: PropTypes.number, setSurveyLinkLastClickedOrClosed: PropTypes.func.isRequired, setNewPrivacyPolicyToastShownDate: PropTypes.func.isRequired, + clearEditedNetwork: PropTypes.func.isRequired, setNewPrivacyPolicyToastClickedOrClosed: PropTypes.func.isRequired, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal: PropTypes.bool.isRequired, @@ -804,6 +805,7 @@ export default class Routes extends Component { switchedNetworkDetails, clearSwitchedNetworkDetails, networkMenuRedesign, + clearEditedNetwork, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal, hideShowKeyringSnapRemovalResultModal, @@ -886,7 +888,12 @@ export default class Routes extends Component { toggleAccountMenu()} /> ) : null} {isNetworkMenuOpen ? ( - toggleNetworkMenu()} /> + { + toggleNetworkMenu(); + clearEditedNetwork(); + }} + /> ) : null} {networkMenuRedesign ? : null} {accountDetailsAddress ? ( diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index 8c7d127c88e1..3024ea436e35 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -1,7 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { act } from '@testing-library/react'; - +import thunk from 'redux-thunk'; import { SEND_STAGES } from '../../ducks/send'; import { CONFIRMATION_V_NEXT_ROUTE, @@ -14,6 +14,8 @@ import mockState from '../../../test/data/mock-state.json'; import { useIsOriginalNativeTokenSymbol } from '../../hooks/useIsOriginalNativeTokenSymbol'; import Routes from '.'; +const middlewares = [thunk]; + const mockShowNetworkDropdown = jest.fn(); const mockHideNetworkDropdown = jest.fn(); @@ -74,7 +76,7 @@ jest.mock('../../helpers/utils/feature-flags', () => ({ })); const render = async (route, state) => { - const store = configureMockStore()({ + const store = configureMockStore(middlewares)({ ...mockSendState, ...state, }); diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index d4334e00b4de..da6d62636d5f 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -52,6 +52,7 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hideKeyringRemovalResultModal, ///: END:ONLY_INCLUDE_IF + setEditedNetwork, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -176,6 +177,7 @@ function mapDispatchToProps(dispatch) { dispatch(setNewPrivacyPolicyToastClickedOrClosed()), setNewPrivacyPolicyToastShownDate: (date) => dispatch(setNewPrivacyPolicyToastShownDate(date)), + clearEditedNetwork: () => dispatch(setEditedNetwork()), ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hideShowKeyringSnapRemovalResultModal: () => dispatch(hideKeyringRemovalResultModal()), diff --git a/ui/pages/settings/networks-tab/index.scss b/ui/pages/settings/networks-tab/index.scss index 99a4840aead0..86e792d988c4 100644 --- a/ui/pages/settings/networks-tab/index.scss +++ b/ui/pages/settings/networks-tab/index.scss @@ -7,10 +7,12 @@ &__rpc-dropdown { cursor: pointer; + word-break: break-all; } &__rpc-item { position: relative; + word-break: break-all; } &__rpc-item:hover { diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js index 298a063f98b6..c5bdaeeb1fc5 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -120,6 +120,7 @@ const NetworksForm = ({ selectedNetwork, cancelCallback, submitCallback, + onRpcUrlAdd, }) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -795,7 +796,13 @@ const NetworksForm = ({ }, }); if (networkMenuRedesign) { - dispatch(setEditedNetwork({ nickname: networkName })); + dispatch( + setEditedNetwork({ + networkConfigurationId, + nickname: networkName, + editCompleted: true, + }), + ); } } @@ -925,8 +932,12 @@ const NetworksForm = ({ ))} ) : null} + {networkMenuRedesign ? ( - + ) : ( { +import { showModal, toggleNetworkMenu } from '../../../../store/actions'; + +export const RpcUrlEditor = ({ + currentRpcUrl, + onRpcUrlAdd, +}: { + currentRpcUrl: string; + onRpcUrlAdd: () => void; +}) => { // TODO: real endpoints const dummyRpcUrls = [ currentRpcUrl, - 'https://dummy.mainnet.public.blastapi.io', - 'https://dummy.io/v3/blockchain/node/dummy', + 'https://mainnet.public.blastapi.io', + 'https://infura.foo.bar.baz/123456789', ]; const t = useI18nContext(); + const dispatch = useDispatch(); const rpcDropdown = useRef(null); - const [isOpen, setIsOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [currentRpcEndpoint, setCurrentRpcEndpoint] = useState(currentRpcUrl); return ( @@ -48,7 +58,7 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { {t('defaultRpcUrl')} setIsOpen(!isOpen)} + onClick={() => setIsDropdownOpen(!isDropdownOpen)} className="networks-tab__rpc-dropdown" display={Display.Flex} justifyContent={JustifyContent.spaceBetween} @@ -60,7 +70,7 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { > {currentRpcEndpoint} @@ -69,19 +79,24 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { paddingTop={2} paddingBottom={2} paddingLeft={0} + matchWidth={true} paddingRight={0} className="networks-tab__rpc-popover" referenceElement={rpcDropdown.current} position={PopoverPosition.Bottom} - isOpen={isOpen} + isOpen={isDropdownOpen} > {dummyRpcUrls.map((rpcEndpoint) => ( setCurrentRpcEndpoint(rpcEndpoint)} + onClick={() => { + setCurrentRpcEndpoint(rpcEndpoint); + setIsDropdownOpen(false); + }} className={classnames('networks-tab__rpc-item', { 'networks-tab__rpc-item--selected': rpcEndpoint === currentRpcEndpoint, @@ -103,19 +118,25 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { {rpcEndpoint} alert('TODO: delete confirmation modal')} + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + dispatch(toggleNetworkMenu()); + dispatch( + showModal({ + name: 'CONFIRM_DELETE_RPC_URL', + }), + ); + }} /> ))} alert('TODO: add RPC modal')} + onClick={onRpcUrlAdd} padding={4} display={Display.Flex} alignItems={AlignItems.center} diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 5fb2badd8bcb..0dc3f89009cb 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -141,7 +141,7 @@ import { import { GAS_FEES_LEARN_MORE_URL } from '../../../../shared/lib/ui-utils'; import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; diff --git a/ui/selectors/multichain.test.ts b/ui/selectors/multichain.test.ts index fd87387a22e3..fadbfe08a08a 100644 --- a/ui/selectors/multichain.test.ts +++ b/ui/selectors/multichain.test.ts @@ -1,4 +1,5 @@ import { Cryptocurrency } from '@metamask/assets-controllers'; +import { InternalAccount } from '@metamask/keyring-api'; import { getNativeCurrency } from '../ducks/metamask/metamask'; import { MULTICHAIN_PROVIDER_CONFIGS, @@ -9,8 +10,10 @@ import { MOCK_ACCOUNTS, MOCK_ACCOUNT_EOA, MOCK_ACCOUNT_BIP122_P2WPKH, + MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, } from '../../test/data/mock-accounts'; import { CHAIN_IDS } from '../../shared/constants/network'; +import { MultichainNativeAssets } from '../../shared/constants/multichain/assets'; import { AccountsState } from './accounts'; import { MultichainState, @@ -23,14 +26,21 @@ import { getMultichainNetwork, getMultichainNetworkProviders, getMultichainProviderConfig, + getMultichainSelectedAccountCachedBalance, getMultichainShouldShowFiat, } from './multichain'; -import { getCurrentCurrency, getCurrentNetwork, getShouldShowFiat } from '.'; +import { + getCurrentCurrency, + getCurrentNetwork, + getSelectedAccountCachedBalance, + getShouldShowFiat, +} from '.'; type TestState = MultichainState & AccountsState & { metamask: { preferences: { showFiatInTestnets: boolean }; + accountsByChainId: Record>; providerConfig: { type: string; ticker: string; chainId: string }; currentCurrency: string; currencyRates: Record; @@ -60,13 +70,26 @@ function getEvmState(): TestState { selectedAccount: MOCK_ACCOUNT_EOA.id, accounts: MOCK_ACCOUNTS, }, + accountsByChainId: { + '0x1': { + [MOCK_ACCOUNT_EOA.address]: { + balance: '3', + }, + }, + }, balances: { [MOCK_ACCOUNT_BIP122_P2WPKH.id]: { - 'bip122:000000000019d6689c085ae165831e93/slip44:0': { + [MultichainNativeAssets.BITCOIN]: { amount: '1.00000000', unit: 'BTC', }, }, + [MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET.id]: { + [MultichainNativeAssets.BITCOIN_TESTNET]: { + amount: '2.00000000', + unit: 'BTC', + }, + }, }, fiatCurrency: 'usd', cryptocurrencies: [Cryptocurrency.Btc], @@ -80,12 +103,12 @@ function getEvmState(): TestState { }; } -function getNonEvmState(): TestState { +function getNonEvmState(account = MOCK_ACCOUNT_BIP122_P2WPKH): TestState { return { metamask: { ...getEvmState().metamask, internalAccounts: { - selectedAccount: MOCK_ACCOUNT_BIP122_P2WPKH.id, + selectedAccount: account.id, accounts: MOCK_ACCOUNTS, }, }, @@ -287,12 +310,62 @@ describe('Multichain Selectors', () => { expect(getMultichainIsMainnet(state)).toBe(false); }); - it('returns current chain ID if account is non-EVM (bip122:)', () => { - const state = getNonEvmState(); + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { isMainnet: true, account: MOCK_ACCOUNT_BIP122_P2WPKH }, + { isMainnet: false, account: MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET }, + ])( + 'returns $isMainnet if non-EVM account address "$account.address" is compatible with mainnet', + ({ + isMainnet, + account, + }: { + isMainnet: boolean; + account: InternalAccount; + }) => { + const state = getNonEvmState(account); + + expect(getMultichainIsMainnet(state)).toBe(isMainnet); + }, + ); + }); - expect(getMultichainIsMainnet(state)).toBe(true); + describe('getMultichainSelectedAccountCachedBalance', () => { + it('returns cached balance if account is EVM', () => { + const state = getEvmState(); + + expect(getMultichainSelectedAccountCachedBalance(state)).toBe( + getSelectedAccountCachedBalance(state), + ); }); - // No test for testnet with non-EVM for now, as we only support mainnet network providers! + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { + network: 'mainnet', + account: MOCK_ACCOUNT_BIP122_P2WPKH, + asset: MultichainNativeAssets.BITCOIN, + }, + { + network: 'testnet', + account: MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, + asset: MultichainNativeAssets.BITCOIN_TESTNET, + }, + ])( + 'returns cached balance if account is non-EVM: $network', + ({ + account, + asset, + }: { + account: InternalAccount; + asset: MultichainNativeAssets; + }) => { + const state = getNonEvmState(account); + const balance = state.metamask.balances[account.id][asset].amount; + + state.metamask.internalAccounts.selectedAccount = account.id; + expect(getMultichainSelectedAccountCachedBalance(state)).toBe(balance); + }, + ); }); }); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1fd7c00b9f34..eba3987a21db 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -1,11 +1,7 @@ import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { ProviderConfig } from '@metamask/network-controller'; import type { RatesControllerState } from '@metamask/assets-controllers'; -import { - CaipChainId, - KnownCaipNamespace, - parseCaipChainId, -} from '@metamask/utils'; +import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; import { ChainId } from '@metamask/controller-utils'; import { MultichainProviderConfig, @@ -19,6 +15,7 @@ import { getProviderConfig, } from '../ducks/metamask/metamask'; import { BalancesControllerState } from '../../app/scripts/lib/accounts/BalancesController'; +import { MultichainNativeAssets } from '../../shared/constants/multichain/assets'; import { AccountsState } from './accounts'; import { getAllNetworks, @@ -27,6 +24,7 @@ import { getIsMainnet, getMaybeSelectedInternalAccount, getNativeCurrencyImage, + getSelectedAccountCachedBalance, getSelectedInternalAccount, getShouldShowFiat, } from '.'; @@ -91,8 +89,7 @@ export function getMultichainNetwork( const selectedAccount = account ?? getSelectedInternalAccount(state); const nonEvmNetworks = getMultichainNetworkProviders(state); const nonEvmNetwork = nonEvmNetworks.find((provider) => { - const { namespace } = parseCaipChainId(provider.chainId); - return selectedAccount.type.startsWith(namespace); + return provider.isAddressCompatible(selectedAccount.address); }); if (!nonEvmNetwork) { @@ -103,8 +100,7 @@ export function getMultichainNetwork( return { // TODO: Adapt this for other non-EVM networks - // TODO: We need to have a way of setting nicknames of other non-EVM networks - nickname: 'Bitcoin', + nickname: nonEvmNetwork.nickname, isEvmNetwork: false, // FIXME: We should use CAIP-2 chain ID here, and not only the reference part chainId: nonEvmNetwork?.chainId, @@ -209,11 +205,14 @@ export function getMultichainShouldShowFiat( true; } -export function getMultichainDefaultToken(state: MultichainState) { - const symbol = getMultichainIsEvm(state) +export function getMultichainDefaultToken( + state: MultichainState, + account?: InternalAccount, +) { + const symbol = getMultichainIsEvm(state, account) ? // We fallback to 'ETH' to keep original behavior of `getSwapsDefaultToken` getProviderConfig(state).ticker ?? 'ETH' - : getMultichainProviderConfig(state).ticker; + : getMultichainProviderConfig(state, account).ticker; return { symbol }; } @@ -223,16 +222,22 @@ export function getMultichainCurrentChainId(state: MultichainState) { return chainId; } -export function getMultichainIsMainnet(state: MultichainState) { - const chainId = getMultichainCurrentChainId(state); +export function getMultichainIsMainnet( + state: MultichainState, + account?: InternalAccount, +) { + const selectedAccount = account ?? getSelectedInternalAccount(state); + const providerConfig = getMultichainProviderConfig(state, selectedAccount); return getMultichainIsEvm(state) ? getIsMainnet(state) - : // TODO: For now we only check for bitcoin mainnet, but we will need to + : // TODO: For now we only check for bitcoin, but we will need to // update this for other non-EVM networks later! - chainId === MultichainNetworks.BITCOIN; + providerConfig.chainId === MultichainNetworks.BITCOIN; } -export function getMultichainBalances(state: MultichainState) { +export function getMultichainBalances( + state: MultichainState, +): BalancesState['metamask']['balances'] { return state.metamask.balances; } @@ -240,6 +245,26 @@ export const getMultichainCoinRates = (state: MultichainState) => { return state.metamask.rates; }; +function getBtcCachedBalance(state: MultichainState) { + const balances = getMultichainBalances(state); + const account = getSelectedInternalAccount(state); + const asset = getMultichainIsMainnet(state) + ? MultichainNativeAssets.BITCOIN + : MultichainNativeAssets.BITCOIN_TESTNET; + + return balances?.[account.id]?.[asset]?.amount; +} + +// This selector is not compatible with `useMultichainSelector` since it uses the selected +// account implicitly! +export function getMultichainSelectedAccountCachedBalance( + state: MultichainState, +) { + return getMultichainIsEvm(state) + ? getSelectedAccountCachedBalance(state) + : getBtcCachedBalance(state); +} + export function getMultichainConversionRate( state: MultichainState, account?: InternalAccount, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index f613015efb7c..faec9242d616 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -13,7 +13,6 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import { addHexPrefix, getEnvironmentType } from '../../app/scripts/lib/util'; import { TEST_CHAINS, - BUYABLE_CHAINS_MAP, MAINNET_DISPLAY_NAME, BSC_DISPLAY_NAME, POLYGON_DISPLAY_NAME, @@ -1307,11 +1306,6 @@ export function getIsBridgeChain(state) { const chainId = getCurrentChainId(state); return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId); } - -export function getIsBuyableChain(state) { - const chainId = getCurrentChainId(state); - return Object.keys(BUYABLE_CHAINS_MAP).includes(chainId); -} export function getNativeCurrencyImage(state) { const chainId = getCurrentChainId(state); return CHAIN_ID_TOKEN_IMAGE_MAP[chainId]; @@ -2032,6 +2026,10 @@ export function getNewNetworkAdded(state) { return state.appState.newNetworkAddedName; } +/** + * @param state + * @returns {{ networkConfigurationId: string; nickname: string; editCompleted: boolean} | undefined} + */ export function getEditedNetwork(state) { return state.appState.editedNetwork; } diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 813153d33ca3..255d0b26adce 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1395,6 +1395,8 @@ describe('Actions', () => { const newNetworkAddedDetails = { nickname: 'test-chain', + networkConfigurationId: 'testNetworkConfigurationId', + editCompleted: true, }; store.dispatch(actions.setEditedNetwork(newNetworkAddedDetails)); @@ -2688,4 +2690,25 @@ describe('Actions', () => { expect(store.getActions()).toStrictEqual(expectedActions); }); }); + + describe('#setBridgeFeatureFlags', () => { + it('calls setBridgeFeatureFlags in the background', async () => { + const store = mockStore(); + background.setBridgeFeatureFlags = sinon + .stub() + .callsFake((_, cb) => cb()); + setBackgroundConnection(background); + + await store.dispatch( + actions.setBridgeFeatureFlags({ extensionSupport: true }), + ); + + expect(background.setBridgeFeatureFlags.callCount).toStrictEqual(1); + expect(background.setBridgeFeatureFlags.getCall(0).args[0]).toStrictEqual( + { + extensionSupport: true, + }, + ); + }); + }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 4a75d4828212..1bc6a437f121 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -113,6 +113,7 @@ import { import { ThemeType } from '../../shared/constants/preferences'; import { FirstTimeFlowType } from '../../shared/constants/onboarding'; import type { MarkAsReadNotificationsParam } from '../../app/scripts/controllers/metamask-notifications/types/notification/notification'; +import { BridgeFeatureFlags } from '../../app/scripts/controllers/bridge'; import * as actionConstants from './actionConstants'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { updateCustodyState } from './institutional/institution-actions'; @@ -3889,6 +3890,16 @@ export function setInitialGasEstimate( }; } +// Bridge +export function setBridgeFeatureFlags( + featureFlags: BridgeFeatureFlags, +): ThunkAction { + return async (dispatch: MetaMaskReduxDispatch) => { + await submitRequestToBackground('setBridgeFeatureFlags', [featureFlags]); + await forceUpdateMetamaskState(dispatch); + }; +} + // Permissions export function requestAccountsPermissionWithId( @@ -4139,16 +4150,16 @@ export function setNewNetworkAdded({ }; } -export function setEditedNetwork({ - nickname, -}: { - networkConfigurationId: string; - nickname: string; -}): PayloadAction { - return { - type: actionConstants.SET_EDIT_NETWORK, - payload: { nickname }, - }; +export function setEditedNetwork( + payload: + | { + networkConfigurationId: string; + nickname: string; + editCompleted: boolean; + } + | undefined = undefined, +): PayloadAction { + return { type: actionConstants.SET_EDIT_NETWORK, payload }; } export function setNewNftAddedMessage( diff --git a/yarn.lock b/yarn.lock index 9ef60d6f70e3..1b0c258c8e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4751,14 +4751,15 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^17.0.0": - version: 17.0.0 - resolution: "@metamask/accounts-controller@npm:17.0.0" +"@metamask/accounts-controller@npm:^17.0.0, @metamask/accounts-controller@npm:^17.1.0": + version: 17.1.1 + resolution: "@metamask/accounts-controller@npm:17.1.1" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/base-controller": "npm:^6.0.0" "@metamask/eth-snap-keyring": "npm:^4.3.1" "@metamask/keyring-api": "npm:^8.0.0" + "@metamask/keyring-controller": "npm:^17.1.0" "@metamask/snaps-sdk": "npm:^4.2.0" "@metamask/snaps-utils": "npm:^7.4.0" "@metamask/utils": "npm:^8.3.0" @@ -4769,7 +4770,7 @@ __metadata: peerDependencies: "@metamask/keyring-controller": ^17.0.0 "@metamask/snaps-controllers": ^8.1.1 - checksum: 10/49ff64d252a463e00d0ad1baad6ac1c2fea9660899c7519c4ce3bc52dcf856d62094b141aaa5ae358b2f26b58d919db4820317c72b66a221656e35a86e55d579 + checksum: 10/79c74f1e219d616ffa5754418b27e2a6f2704ed0690201b46fdcffba5b25297decee735587e373b65b31f6f1817ad1d0b3072525335f67c2da1b4ff25b077b9c languageName: node linkType: hard @@ -4824,9 +4825,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^33.0.0": - version: 33.0.0 - resolution: "@metamask/assets-controllers@npm:33.0.0" +"@metamask/assets-controllers@npm:^34.0.0": + version: 34.0.0 + resolution: "@metamask/assets-controllers@npm:34.0.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4834,7 +4835,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.2" - "@metamask/accounts-controller": "npm:^17.0.0" + "@metamask/accounts-controller": "npm:^17.1.0" "@metamask/approval-controller": "npm:^7.0.0" "@metamask/base-controller": "npm:^6.0.0" "@metamask/contract-metadata": "npm:^2.4.0" @@ -4862,7 +4863,7 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^19.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/cb35e1a170c10f64df023938108593df3c5686e92070593f230c1146cd78d5ed4fbff9027cb18920c11a1ac2dc1090ce718ed22ba61dfd337fe68c18f4c06164 + checksum: 10/181cdfbcceb71ffa6d9126d70ebe97cee43ddcc1a50554594cea073d127a3a9ddc0666615b462563e33700d32b9b405cecc8a44fbcd95c84eb3b6053546ab480 languageName: node linkType: hard @@ -11499,13 +11500,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 - languageName: node - linkType: hard - "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -12116,13 +12110,6 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3": - version: 1.2.0 - resolution: "aproba@npm:1.2.0" - checksum: 10/48def777330afca699880126b555273cd9912525500edc5866b527da6fd6c54badd3ae6cc6039081e5bc22e9b349d8e65fd70f8499beb090f86aa6261e4242dd - languageName: node - linkType: hard - "archy@npm:^1.0.0": version: 1.0.0 resolution: "archy@npm:1.0.0" @@ -12137,16 +12124,6 @@ __metadata: languageName: node linkType: hard -"are-we-there-yet@npm:~1.1.2": - version: 1.1.4 - resolution: "are-we-there-yet@npm:1.1.4" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^2.0.6" - checksum: 10/86947c3ec0c8be746a0a8eb0871033bd89d8619ddbf6d51aae2d4a3a816fc06741df01307db8c0b44b9e9b20d46725fd023f1766bf586ccf1d4636cb1441452f - languageName: node - linkType: hard - "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -12495,13 +12472,6 @@ __metadata: languageName: node linkType: hard -"async-each@npm:^1.0.1": - version: 1.0.1 - resolution: "async-each@npm:1.0.1" - checksum: 10/9421203743e3379ce70defb94a78308c828e4e56d8e8bf4ba90b4c788b90a9d0759aabd327831e5a97bb6e484eccfee2f5496c1c2b239bd15f082544d919c60d - languageName: node - linkType: hard - "async-eventemitter@npm:0.2.4": version: 0.2.4 resolution: "async-eventemitter@npm:0.2.4" @@ -13092,13 +13062,6 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^1.0.0": - version: 1.11.0 - resolution: "binary-extensions@npm:1.11.0" - checksum: 10/839f346e4a1e5d43a96c5e5b3cdc608840e97b178f27a1b2fdc7b670598a23fdec42ab70d736d03db752bef44e18a57c5471e7d3ac11163deff459d6bd580e6a - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.0.0 resolution: "binary-extensions@npm:2.0.0" @@ -13372,7 +13335,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^2.3.1, braces@npm:^2.3.2": +"braces@npm:^2.3.1": version: 2.3.2 resolution: "braces@npm:2.3.2" dependencies: @@ -14219,26 +14182,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3": - version: 3.5.3 - resolution: "chokidar@npm:3.5.3" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/863e3ff78ee7a4a24513d2a416856e84c8e4f5e60efbe03e8ab791af1a183f569b62fc6f6b8044e2804966cb81277ddbbc1dc374fba3265bd609ea8efd62f5b3 - languageName: node - linkType: hard - -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.0, chokidar@npm:^3.5.3": +"chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -14257,30 +14201,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^2.0.0": - version: 2.1.8 - resolution: "chokidar@npm:2.1.8" - dependencies: - anymatch: "npm:^2.0.0" - async-each: "npm:^1.0.1" - braces: "npm:^2.3.2" - fsevents: "npm:^1.2.7" - glob-parent: "npm:^3.1.0" - inherits: "npm:^2.0.3" - is-binary-path: "npm:^1.0.0" - is-glob: "npm:^4.0.0" - normalize-path: "npm:^3.0.0" - path-is-absolute: "npm:^1.0.0" - readdirp: "npm:^2.2.1" - upath: "npm:^1.1.1" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/567c319dd2a9078fddb5a64df46163d87b104857c1b50c2ef6f9b41b3ab28867c48dbc5f0c6ddaafd3c338b147ea33a6498eb9b906c71006cba1e486a0e9350d - languageName: node - linkType: hard - -"chownr@npm:^1.1.1, chownr@npm:^1.1.4": +"chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d @@ -14970,13 +14891,6 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 10/27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb - languageName: node - linkType: hard - "consolidate@npm:^0.16.0": version: 0.16.0 resolution: "consolidate@npm:0.16.0" @@ -15702,7 +15616,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:3.X, debug@npm:^3.1.0, debug@npm:^3.1.1, debug@npm:^3.2.6, debug@npm:^3.2.7": +"debug@npm:3.X, debug@npm:^3.1.0, debug@npm:^3.1.1, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -16196,15 +16110,6 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^1.0.2": - version: 1.0.3 - resolution: "detect-libc@npm:1.0.3" - bin: - detect-libc: ./bin/detect-libc.js - checksum: 10/3849fe7720feb153e4ac9407086956e073f1ce1704488290ef0ca8aab9430a8d48c8a9f8351889e7cdc64e5b1128589501e4fef48f3a4a49ba92cd6d112d0757 - languageName: node - linkType: hard - "detect-newline@npm:^2.0.0": version: 2.1.0 resolution: "detect-newline@npm:2.1.0" @@ -19387,15 +19292,6 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^1.2.7": - version: 1.2.7 - resolution: "fs-minipass@npm:1.2.7" - dependencies: - minipass: "npm:^2.6.0" - checksum: 10/6a2d39963eaad748164530ffab49606d0f3462c7867748521af3b7039d13689be533636d50a04e8ba6bd327d4d2e899d0907f8830d1161fe2db467d59cc46dc3 - languageName: node - linkType: hard - "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -19448,17 +19344,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^1.2.7": - version: 1.2.9 - resolution: "fsevents@npm:1.2.9" - dependencies: - nan: "npm:^2.12.1" - node-pre-gyp: "npm:^0.12.0" - checksum: 10/1d8cb504a54837816badbbe7178fae84d82d2b50731571591404f13881ace67d395cc9cadda8106716f69e362b9888295fa1c3eee58c1041c2c43fc87204da36 - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -19478,16 +19363,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^1.2.7#optional!builtin": - version: 1.2.9 - resolution: "fsevents@patch:fsevents@npm%3A1.2.9#optional!builtin::version=1.2.9&hash=d11327" - dependencies: - nan: "npm:^2.12.1" - node-pre-gyp: "npm:^0.12.0" - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -19590,22 +19465,6 @@ __metadata: languageName: node linkType: hard -"gauge@npm:~2.7.3": - version: 2.7.4 - resolution: "gauge@npm:2.7.4" - dependencies: - aproba: "npm:^1.0.3" - console-control-strings: "npm:^1.0.0" - has-unicode: "npm:^2.0.0" - object-assign: "npm:^4.1.0" - signal-exit: "npm:^3.0.0" - string-width: "npm:^1.0.1" - strip-ansi: "npm:^3.0.1" - wide-align: "npm:^1.1.0" - checksum: 10/0db20a7def238f0e8eab50226247e1f94f1446ab24700eab0a56e5ccf23ce85ccf8f0c0c462112b89beb964431b1edabd3f7b31f1f6d5f62294c453594523993 - languageName: node - linkType: hard - "generic-names@npm:^2.0.1": version: 2.0.1 resolution: "generic-names@npm:2.0.1" @@ -20619,13 +20478,6 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:^2.0.0": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 10/041b4293ad6bf391e21c5d85ed03f412506d6623786b801c4ab39e4e6ca54993f13201bceb544d92963f9e0024e6e7fbf0cb1d84c9d6b31cb9c79c8c990d13d8 - languageName: node - linkType: hard - "has-value@npm:^0.3.1": version: 0.3.1 resolution: "has-value@npm:0.3.1" @@ -21162,7 +21014,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.4": +"iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -21219,15 +21071,6 @@ __metadata: languageName: node linkType: hard -"ignore-walk@npm:^3.0.1": - version: 3.0.1 - resolution: "ignore-walk@npm:3.0.1" - dependencies: - minimatch: "npm:^3.0.4" - checksum: 10/93f871229dfd2f240ad8c9eaef2496675cc31a96f6dbd15a35ddebb0f7ae16b7ca2116f1f48ae3f749badc461103b5198ae2e65206a064bba31c32f681d38571 - languageName: node - linkType: hard - "ignore@npm:^5.1.1, ignore@npm:^5.1.8, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -21575,15 +21418,6 @@ __metadata: languageName: node linkType: hard -"is-binary-path@npm:^1.0.0": - version: 1.0.1 - resolution: "is-binary-path@npm:1.0.1" - dependencies: - binary-extensions: "npm:^1.0.0" - checksum: 10/a803c99e9d898170c3b44a86fbdc0736d3d7fcbe737345433fb78e810b9fe30c982657782ad0e676644ba4693ddf05601a7423b5611423218663d6b533341ac9 - languageName: node - linkType: hard - "is-binary-path@npm:~2.1.0": version: 2.1.0 resolution: "is-binary-path@npm:2.1.0" @@ -25337,7 +25171,7 @@ __metadata: "@metamask/announcement-controller": "npm:^6.1.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "npm:^33.0.0" + "@metamask/assets-controllers": "npm:^34.0.0" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^5.0.1" "@metamask/browser-passworder": "npm:^4.3.0" @@ -25495,7 +25329,7 @@ __metadata: browserify: "npm:^17.0.0" chalk: "npm:^4.1.2" chart.js: "npm:^4.4.1" - chokidar: "npm:^3.5.3" + chokidar: "npm:^3.6.0" classnames: "npm:^2.2.6" concurrently: "npm:^8.2.2" contentful: "npm:^10.8.7" @@ -26045,7 +25879,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^3.0.4, micromatch@npm:^3.1.10, micromatch@npm:^3.1.4": +"micromatch@npm:^3.0.4, micromatch@npm:^3.1.4": version: 3.1.10 resolution: "micromatch@npm:3.1.10" dependencies: @@ -26308,16 +26142,6 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^2.6.0, minipass@npm:^2.9.0": - version: 2.9.0 - resolution: "minipass@npm:2.9.0" - dependencies: - safe-buffer: "npm:^5.1.2" - yallist: "npm:^3.0.0" - checksum: 10/fdd1a77996c184991f8d2ce7c5b3979bec624e2a3225e2e1e140c4038fd65873d7eb90fb29779f8733735a8827b2686f283871a0c74c908f4f7694c56fa8dadf - languageName: node - linkType: hard - "minipass@npm:^3.0.0": version: 3.3.5 resolution: "minipass@npm:3.3.5" @@ -26341,15 +26165,6 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^1.3.3": - version: 1.3.3 - resolution: "minizlib@npm:1.3.3" - dependencies: - minipass: "npm:^2.9.0" - checksum: 10/9c2c47e5687d7f896431a9b5585988ef72f848b56c6a974c9489534e8f619388d500d986ef82e1c13aedd46f3a0e81b6a88110cb1b27de7524cc8dabe8885e17 - languageName: node - linkType: hard - "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -26712,7 +26527,7 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.12.1, nan@npm:^2.13.2": +"nan@npm:^2.13.2": version: 2.15.0 resolution: "nan@npm:2.15.0" dependencies: @@ -26795,19 +26610,6 @@ __metadata: languageName: node linkType: hard -"needle@npm:^2.2.1": - version: 2.4.0 - resolution: "needle@npm:2.4.0" - dependencies: - debug: "npm:^3.2.6" - iconv-lite: "npm:^0.4.4" - sax: "npm:^1.2.4" - bin: - needle: ./bin/needle - checksum: 10/0f2de9406d07f05f89cae241594a2aa66ff0a371ead42c013942c4684f60c830d57066663a740ea6b524e7d031ec2fc8ebd9bf687c51b8936af12c5dc4f7e526 - languageName: node - linkType: hard - "negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" @@ -27024,26 +26826,6 @@ __metadata: languageName: node linkType: hard -"node-pre-gyp@npm:^0.12.0": - version: 0.12.0 - resolution: "node-pre-gyp@npm:0.12.0" - dependencies: - detect-libc: "npm:^1.0.2" - mkdirp: "npm:^0.5.1" - needle: "npm:^2.2.1" - nopt: "npm:^4.0.1" - npm-packlist: "npm:^1.1.6" - npmlog: "npm:^4.0.2" - rc: "npm:^1.2.7" - rimraf: "npm:^2.6.1" - semver: "npm:^5.3.0" - tar: "npm:^4" - bin: - node-pre-gyp: ./bin/node-pre-gyp - checksum: 10/15a25f5e4e8f9dad86b1e72ad939311923c18d270b6eec9122c81d61205227dbd6cc7dc9f0bd97101b8e6b5c88c2c154d741b9c8b017e414ae0f480782c597d0 - languageName: node - linkType: hard - "node-preload@npm:^0.2.1": version: 0.2.1 resolution: "node-preload@npm:0.2.1" @@ -27069,18 +26851,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^4.0.1": - version: 4.0.1 - resolution: "nopt@npm:4.0.1" - dependencies: - abbrev: "npm:1" - osenv: "npm:^0.1.4" - bin: - nopt: ./bin/nopt.js - checksum: 10/7e9bc2a1224fd41ec604ef8d420a5f855dd6612fa6362615f8e7e8fc44e985fb117c9acfa5cfbfd5efb081c9fb6380a4b1cf579c49d5d1cd98dd425c9c8be78b - languageName: node - linkType: hard - "nopt@npm:^7.0.0": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -27162,13 +26932,6 @@ __metadata: languageName: node linkType: hard -"npm-bundled@npm:^1.0.1": - version: 1.0.6 - resolution: "npm-bundled@npm:1.0.6" - checksum: 10/db96b54c3e27aff51055748a2e900a9808f5687037bb7247a86f9e91cbe2070426414688bf684feda24e2f20e223899fc9f754ac9c882eea9bfa32a482d589b0 - languageName: node - linkType: hard - "npm-install-checks@npm:^6.0.0": version: 6.3.0 resolution: "npm-install-checks@npm:6.3.0" @@ -27204,16 +26967,6 @@ __metadata: languageName: node linkType: hard -"npm-packlist@npm:^1.1.6": - version: 1.4.1 - resolution: "npm-packlist@npm:1.4.1" - dependencies: - ignore-walk: "npm:^3.0.1" - npm-bundled: "npm:^1.0.1" - checksum: 10/a8891f330760517152dd0b9f82fe017028e32d925985cd54672fc80c3294d4438226347b09b7307fa43dfc022d0b87117de073a4a92f5a423297d88f71af23d6 - languageName: node - linkType: hard - "npm-pick-manifest@npm:^9.0.0": version: 9.0.1 resolution: "npm-pick-manifest@npm:9.0.1" @@ -27244,18 +26997,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^4.0.2": - version: 4.1.2 - resolution: "npmlog@npm:4.1.2" - dependencies: - are-we-there-yet: "npm:~1.1.2" - console-control-strings: "npm:~1.1.0" - gauge: "npm:~2.7.3" - set-blocking: "npm:~2.0.0" - checksum: 10/b6b85c9f33da8f600f72564b6ec71136b1641b8b235fca7cc543d1041acb74c2d989d97fe443a0e65754f438d9a974a2fe1b4ff8723c78ef3f9b7a6d74b02079 - languageName: node - linkType: hard - "nth-check@npm:^2.0.1": version: 2.0.1 resolution: "nth-check@npm:2.0.1" @@ -27649,7 +27390,7 @@ __metadata: languageName: node linkType: hard -"os-homedir@npm:^1.0.0, os-homedir@npm:^1.0.1": +"os-homedir@npm:^1.0.1": version: 1.0.2 resolution: "os-homedir@npm:1.0.2" checksum: 10/af609f5a7ab72de2f6ca9be6d6b91a599777afc122ac5cad47e126c1f67c176fe9b52516b9eeca1ff6ca0ab8587fe66208bc85e40a3940125f03cdb91408e9d2 @@ -27676,23 +27417,6 @@ __metadata: languageName: node linkType: hard -"os-tmpdir@npm:^1.0.0": - version: 1.0.2 - resolution: "os-tmpdir@npm:1.0.2" - checksum: 10/5666560f7b9f10182548bf7013883265be33620b1c1b4a4d405c25be2636f970c5488ff3e6c48de75b55d02bde037249fe5dbfbb4c0fb7714953d56aed062e6d - languageName: node - linkType: hard - -"osenv@npm:^0.1.4": - version: 0.1.5 - resolution: "osenv@npm:0.1.5" - dependencies: - os-homedir: "npm:^1.0.0" - os-tmpdir: "npm:^1.0.0" - checksum: 10/779d261920f2a13e5e18cf02446484f12747d3f2ff82280912f52b213162d43d312647a40c332373cbccd5e3fb8126915d3bfea8dde4827f70f82da76e52d359 - languageName: node - linkType: hard - "outpipe@npm:^1.1.0": version: 1.1.1 resolution: "outpipe@npm:1.1.1" @@ -29552,7 +29276,7 @@ __metadata: languageName: node linkType: hard -"rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.7, rc@npm:^1.2.8": +"rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" dependencies: @@ -30258,7 +29982,7 @@ __metadata: languageName: node linkType: hard -"readable-stream-2@npm:readable-stream@^2.3.3, readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.4, readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.1.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": +"readable-stream-2@npm:readable-stream@^2.3.3, readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.4, readable-stream@npm:^2.0.5, readable-stream@npm:^2.1.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": version: 2.3.7 resolution: "readable-stream@npm:2.3.7" dependencies: @@ -30318,17 +30042,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:^2.2.1": - version: 2.2.1 - resolution: "readdirp@npm:2.2.1" - dependencies: - graceful-fs: "npm:^4.1.11" - micromatch: "npm:^3.1.10" - readable-stream: "npm:^2.0.2" - checksum: 10/14af3408ac2afa4e72e72a27e2c800d80c03e80bdef7ae4bd4b7907e98dddbeaa1ba37d4788959d9ce1131fc262cc823ce41ca9f024a91d80538241eea112c3c - languageName: node - linkType: hard - "readdirp@npm:^3.5.0, readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -31390,7 +31103,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 @@ -31686,7 +31399,7 @@ __metadata: languageName: node linkType: hard -"sax@npm:>=0.6.0, sax@npm:^1.2.4": +"sax@npm:>=0.6.0": version: 1.3.0 resolution: "sax@npm:1.3.0" checksum: 10/bb571b31d30ecb0353c2ff5f87b117a03e5fb9eb4c1519141854c1a8fbee0a77ddbe8045f413259e711833aa03da210887df8527d19cdc55f299822dbf4b34de @@ -31864,7 +31577,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.3.0, semver@npm:^5.6.0": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" bin: @@ -31987,7 +31700,7 @@ __metadata: languageName: node linkType: hard -"set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": +"set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef @@ -32872,7 +32585,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -33603,21 +33316,6 @@ __metadata: languageName: node linkType: hard -"tar@npm:^4": - version: 4.4.19 - resolution: "tar@npm:4.4.19" - dependencies: - chownr: "npm:^1.1.4" - fs-minipass: "npm:^1.2.7" - minipass: "npm:^2.9.0" - minizlib: "npm:^1.3.3" - mkdirp: "npm:^0.5.5" - safe-buffer: "npm:^5.2.1" - yallist: "npm:^3.1.1" - checksum: 10/2715b5964578424ba5164632905a85e5a98c8dffeba657860aafa3a771b2602e6fd2a350bca891d78b8bda8cab5c53134c683ed2269b9925533477a24722e73b - languageName: node - linkType: hard - "tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -34949,13 +34647,6 @@ __metadata: languageName: node linkType: hard -"upath@npm:^1.1.1": - version: 1.2.0 - resolution: "upath@npm:1.2.0" - checksum: 10/ac07351d9e913eb7bc9bc0a17ed7d033a52575f0f2959e19726956c3e96f5d4d75aa6a7a777c4c9506e72372f58e06215e581f8dbff35611fc0a7b68ab4a6ddb - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.0.13": version: 1.0.13 resolution: "update-browserslist-db@npm:1.0.13" @@ -36080,15 +35771,6 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.0": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: "npm:^1.0.2 || 2 || 3 || 4" - checksum: 10/d5f8027b9a8255a493a94e4ec1b74a27bff6679d5ffe29316a3215e4712945c84ef73ca4045c7e20ae7d0c72f5f57f296e04a4928e773d4276a2f1222e4c2e99 - languageName: node - linkType: hard - "widest-line@npm:^2.0.0": version: 2.0.1 resolution: "widest-line@npm:2.0.1" @@ -36400,7 +36082,7 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^3.0.0, yallist@npm:^3.0.2, yallist@npm:^3.1.1": +"yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" checksum: 10/9af0a4329c3c6b779ac4736c69fae4190ac03029fa27c1aef4e6bcc92119b73dea6fe5db5fe881fb0ce2a0e9539a42cdf60c7c21eda04d1a0b8c082e38509efb