diff --git a/.github/workflows/stale-issues-pr.yml b/.github/workflows/stale-issues-pr.yml new file mode 100644 index 000000000..941a689d1 --- /dev/null +++ b/.github/workflows/stale-issues-pr.yml @@ -0,0 +1,28 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '0 12 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@72afbce2b0dbd1d903bb142cebe2d15dc307ae57 + with: + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 45 days. Thank you for your contributions.' + stale-issue-label: 'stale' + only-issue-labels: 'type-bug' + exempt-issue-labels: 'type-security, type-pinned, feature-request, awaiting-metamask' + stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity in the last 60 days. It will be closed in 14 days. Thank you for your contributions.' + stale-pr-label: 'stale' + exempt-pr-labels: 'work-in-progress' + close-issue-message: 'This issue was closed because there has been no follow up activity in the last 7 days. If you feel this was closed in error, please reopen and provide evidence on the latest release of the extension. Thank you for your contributions.' + close-pr-message: 'This PR was closed because there has been no follow up activity in the last 7 days. Thank you for your contributions.' + days-before-issue-stale: 90 + days-before-pr-stale: 60 + days-before-issue-close: 45 + days-before-pr-close: 14 + operations-per-run: 1 diff --git a/.github/workflows/update-lavamoat-policies.yml b/.github/workflows/update-lavamoat-policies.yml index 5f2814e97..b05221889 100644 --- a/.github/workflows/update-lavamoat-policies.yml +++ b/.github/workflows/update-lavamoat-policies.yml @@ -223,3 +223,37 @@ jobs: HAS_CHANGES: ${{ steps.policy-changes.outputs.HAS_CHANGES }} GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} + + check-status: + name: Check whether the policy update succeeded + runs-on: ubuntu-latest + needs: + - commit-updated-policies + outputs: + PASSED: ${{ steps.set-output.outputs.PASSED }} + steps: + - name: Set PASSED output + id: set-output + run: echo "PASSED=true" >> "$GITHUB_OUTPUT" + + failure-comment: + name: Comment about the policy update failure + if: ${{ always() && needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + runs-on: ubuntu-latest + needs: + - is-fork-pull-request + - check-status + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} + - name: Post comment if the update failed + run: | + passed="${{ needs.check-status.outputs.PASSED }}" + if [[ $passed != "true" ]]; then + gh pr comment "${PR_NUMBER}" --body "Policy update failed. You can [review the logs or retry the policy update here](${ACTION_RUN_URL})" + fi + env: + GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + ACTION_RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.iyarc b/.iyarc index 0bb3e981f..9e16de044 100644 --- a/.iyarc +++ b/.iyarc @@ -3,6 +3,9 @@ GHSA-257v-vj4p-3w2h # request library is subject to SSRF. # addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch -# NOTE: Disabled for now since this library seems to have been removed from the -# dependency tree — we can re-enable this line later if it appears again -#GHSA-p8p7-x288-28g6 +GHSA-p8p7-x288-28g6 + +# Prototype pollution +# Not easily patched +# Minimal risk to us because we're using lockdown which also prevents this case of prototype pollution +GHSA-h755-8qp9-cq85 diff --git a/.yarn/patches/@metamask-signature-controller-npm-4.0.1-013e64c9fd.patch b/.yarn/patches/@metamask-signature-controller-npm-4.0.1-013e64c9fd.patch new file mode 100644 index 000000000..ef3f75e9e --- /dev/null +++ b/.yarn/patches/@metamask-signature-controller-npm-4.0.1-013e64c9fd.patch @@ -0,0 +1,17 @@ +diff --git a/dist/SignatureController.js b/dist/SignatureController.js +index b58b27e84aa84393afb366d4585c084d0380d21d..0629bcf517db744ccfa40e4d7d8f2829fa95559e 100644 +--- a/dist/SignatureController.js ++++ b/dist/SignatureController.js +@@ -237,8 +237,11 @@ _SignatureController_keyringController = new WeakMap(), _SignatureController_isE + yield __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_requestApproval).call(this, messageParamsWithId, approvalType); + } + catch (error) { ++ signaturePromise.catch(() => { ++ // Expecting reject error but throwing manually rather than waiting ++ }); + __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_cancelAbstractMessage).call(this, messageManager, messageId); +- throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the request.'); ++ throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest(`MetaMask ${messageName} Signature: User denied message signature.`); + } + yield signMessage(messageParamsWithId, version, signingOpts); + return signaturePromise; diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc5fce18..914787298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.33.1] +### Fixed +- Fix to bug causing users to see an infinite spinner when signing typed messages. ([#19894](https://github.com/MetaMask/metamask-extension/pull/19894)) + ## [10.33.0] ### Added - UI Upgrade ([#18903](https://github.com/MetaMask/metamask-extension/pull/18903)) @@ -3825,7 +3829,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.33.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.33.1...HEAD +[10.33.1]: https://github.com/MetaMask/metamask-extension/compare/v10.33.0...v10.33.1 [10.33.0]: https://github.com/MetaMask/metamask-extension/compare/v10.32.0...v10.33.0 [10.32.0]: https://github.com/MetaMask/metamask-extension/compare/v10.31.1...v10.32.0 [10.31.1]: https://github.com/MetaMask/metamask-extension/compare/v10.31.0...v10.31.1 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 3da5e212e..70ca5538a 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Seed-Phrase anzeigen" }, - "reviewSpendingCap": { - "message": "Überprüfen SIe Ihre Ausgabegrenze" - }, "revokeAllTokensTitle": { "message": "Erlaubnis zum Zugriff auf alle Ihre $1 sowie deren Übertragung entziehen?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "$1 ohne Ausgabenlimit genehmigen", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Eine Ausgabegrenze für Ihr $1 einrichten", - "description": "$1 is a token symbol" - }, "settings": { "message": "Einstellungen" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "Diese URL wird derzeit vom $1-Netzwerk verwendet." }, - "useDefault": { - "message": "Standardeinstellungen verwenden" - }, "useMultiAccountBalanceChecker": { "message": "Kontoguthaben-Anfragen sammeln" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 7bbe75c6b..ce06907b2 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Αποκάλυψη φράσης ανάκτησης" }, - "reviewSpendingCap": { - "message": "Επανεξετάστε το όριο δαπανών σας" - }, "revokeAllTokensTitle": { "message": "Ανάκληση άδειας πρόσβασης σε όλα σας τα $1;", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3137,10 +3134,6 @@ "message": "Έγκριση $1 χωρίς όριο δαπανών", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Ορίστε ένα ανώτατο όριο δαπανών για το $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Ρυθμίσεις" }, @@ -4164,9 +4157,6 @@ "urlExistsErrorMsg": { "message": "Αυτό το URL χρησιμοποιείται επί του παρόντος από το δίκτυο $1." }, - "useDefault": { - "message": "Χρήση προκαθορισμένου" - }, "useMultiAccountBalanceChecker": { "message": "Μαζικά αιτήματα υπολοίπου λογαριασμού" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5d9b0ba06..328441d1e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -366,6 +366,9 @@ "allowThisSiteTo": { "message": "Allow this site to:" }, + "allowThisSnapTo": { + "message": "Allow this snap to:" + }, "allowWithdrawAndSpend": { "message": "Allow $1 to withdraw and spend up to the following amount:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -1052,6 +1055,9 @@ "customerSupport": { "message": "customer support" }, + "dappRequestedSpendingCap": { + "message": "Site requested spending cap" + }, "dappSuggested": { "message": "Site suggested" }, @@ -3541,9 +3547,6 @@ "revealTheSeedPhrase": { "message": "Reveal seed phrase" }, - "reviewSpendingCap": { - "message": "Review the spending cap for your" - }, "revokeAllTokensTitle": { "message": "Revoke permission to access and transfer all of your $1?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3560,6 +3563,9 @@ "message": "This revokes the permission for a third party to access and transfer all of your NFTs from $1 without further notice.", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" }, + "revokePermission": { + "message": "Revoke permission" + }, "revokeSpendingCap": { "message": "Revoke spending cap for your $1", "description": "$1 is a token symbol" @@ -3681,6 +3687,9 @@ "selectAccounts": { "message": "Select the account(s) to use on this site" }, + "selectAccountsForSnap": { + "message": "Select the account(s) to use with this snap" + }, "selectAll": { "message": "Select all" }, @@ -3757,10 +3766,6 @@ "message": "Approve $1 with no spend limit", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Set a spending cap for your $1", - "description": "$1 is a token symbol" - }, "settingAddSnapAccount": { "message": "Add snap account" }, @@ -3770,10 +3775,6 @@ "settingsSearchMatchingNotFound": { "message": "No matching results found." }, - "shortVersion": { - "message": "v$1", - "description": "$1 is the version number to show" - }, "show": { "message": "Show" }, @@ -4087,6 +4088,9 @@ "message": "Only enter a number that you're comfortable with $1 accessing now or in the future. You can always increase the token limit later.", "description": "$1 is origin of the site requesting the token limit" }, + "spendingCapRequest": { + "message": "Spending cap request for your $1" + }, "srpInputNumberOfWords": { "message": "I have a $1-word phrase", "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." @@ -5136,9 +5140,6 @@ "urlExistsErrorMsg": { "message": "This URL is currently used by the $1 network." }, - "useDefault": { - "message": "Use default" - }, "useMultiAccountBalanceChecker": { "message": "Batch account balance requests" }, @@ -5169,6 +5170,9 @@ "usePhishingDetectionDescription": { "message": "Display a warning for phishing domains targeting Ethereum users" }, + "useSiteSuggestion": { + "message": "Use site suggestion" + }, "useTokenDetectionPrivacyDesc": { "message": "Automatically displaying tokens sent to your account involves communication with third party servers to fetch token’s images. Those serves will have access to your IP address." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index bc3dfb601..71a4b61ec 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Revelar frase semilla" }, - "reviewSpendingCap": { - "message": "Revise su límite de gasto" - }, "revokeAllTokensTitle": { "message": "¿Revocar el permiso para acceder y transferir todos sus $1?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Aprobar $1 sin límite preestablecido", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Establecer un límite de gasto para su $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Configuración" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "En este momento, la red $1 está utilizando esta dirección URL." }, - "useDefault": { - "message": "Uso por defecto" - }, "useMultiAccountBalanceChecker": { "message": "Solicitudes de saldo de cuenta por lotes" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index aafeeead8..ca36fd57d 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Révéler la phrase mnémonique" }, - "reviewSpendingCap": { - "message": "Modifiez votre plafond de dépenses" - }, "revokeAllTokensTitle": { "message": "Révoquer l’autorisation d’accès et de transfert de tous vos $1 ?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Approuver $1 sans limite de dépenses", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Fixez un plafond de dépenses pour vos $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Paramètres" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "Cette URL est actuellement utilisée par le réseau $1." }, - "useDefault": { - "message": "Utiliser par défaut" - }, "useMultiAccountBalanceChecker": { "message": "Demandes d’informations concernant le solde de plusieurs comptes" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 2fcc7b43c..446767290 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "सीड फ़्रेज़ दिखाएं" }, - "reviewSpendingCap": { - "message": "अपनी खर्च के सीमा की समीक्षा करें" - }, "revokeAllTokensTitle": { "message": "आपके सभी $1 को एक्सेस और स्थानांतरित करने की अनुमति निरस्त करें?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "बिना किसी खर्च सीमा के $1 स्वीकृत करें", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "अपने $1 के लिए खर्च की सीमा को निर्धारित करें", - "description": "$1 is a token symbol" - }, "settings": { "message": "सेटिंग" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "यह URL वर्तमान में $1 नेटवर्क द्वारा उपयोग किया जाता है।" }, - "useDefault": { - "message": "डिफॉल्ट का उपयोग करें" - }, "useMultiAccountBalanceChecker": { "message": "खाता के शेष राशि के अनुरोधों को बैच करें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index d03338321..65fe2d24f 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Ungkap frasa seed" }, - "reviewSpendingCap": { - "message": "Tinjau batas pengeluaran Anda" - }, "revokeAllTokensTitle": { "message": "Cabut izin untuk mengakses dan mentransfer seluruh $1 Anda?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Setujui $1 tanpa batas penggunaan", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Tetapkan batas pengeluaran untuk $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Pengaturan" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "URL ini saat ini digunakan oleh jaringan $1." }, - "useDefault": { - "message": "Gunakan default" - }, "useMultiAccountBalanceChecker": { "message": "Kelompokkan permintaan saldo akun" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 18cd136db..715248ca8 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "シードフレーズを表示" }, - "reviewSpendingCap": { - "message": "使用上限を確認してください" - }, "revokeAllTokensTitle": { "message": "すべての $1 へのアクセスおよび転送許可を取り消しますか?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "使用限度額なしで $1 を承認", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "$1 の使用上限を設定", - "description": "$1 is a token symbol" - }, "settings": { "message": "設定" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "このURLは現在$1ネットワークで使用されています。" }, - "useDefault": { - "message": "既定値を使用" - }, "useMultiAccountBalanceChecker": { "message": "アカウント残高の一括リクエスト" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 2926560e3..a214b7805 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "시드 구문 보기" }, - "reviewSpendingCap": { - "message": "지출 한도 검토" - }, "revokeAllTokensTitle": { "message": "모든 $1에 액세스할 수 있는 권한을 철회할까요?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "$1 무제한 지출 승인", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "$1에 대한 지출 한도 설정", - "description": "$1 is a token symbol" - }, "settings": { "message": "설정" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "이 URL은 현재 $1 네트워크에서 사용됩니다." }, - "useDefault": { - "message": "기본값 사용" - }, "useMultiAccountBalanceChecker": { "message": "일괄 계정 잔액 요청" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 39997d6ff..00c39a066 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Revelar a frase de recuperação" }, - "reviewSpendingCap": { - "message": "Revise seu limite de gastos" - }, "revokeAllTokensTitle": { "message": "Revogar permissão de acesso e transferência de todos os seus $1?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Aprovar $1 sem limite de gastos", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Definir um limite de gastos para seu $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Definições" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "O ID da cadeia está sendo usado pela rede $1." }, - "useDefault": { - "message": "Usar padrão" - }, "useMultiAccountBalanceChecker": { "message": "Agrupar solicitações de saldo de contas" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 1921b112b..fb8a915ef 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Показать сид-фразу" }, - "reviewSpendingCap": { - "message": "Проверьте свой лимит расходов" - }, "revokeAllTokensTitle": { "message": "Отозвать разрешение на доступ ко всем вашим $1 и их перевод?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Одобрить $1 без ограничений по расходам", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Установить верхний лимит расходов для вашего $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Настройки" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "Это URL-адрес в настоящее время используется сетью $1." }, - "useDefault": { - "message": "Использовать значения по умолчанию" - }, "useMultiAccountBalanceChecker": { "message": "Пакетные запросы баланса счета" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 2ca598754..f4877ea06 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Ipakita ang seed phrase" }, - "reviewSpendingCap": { - "message": "I-review ang iyong limitasyon sa paggastos" - }, "revokeAllTokensTitle": { "message": "Bawiin ang pahintulot na i-access at ilipat ang lahat ng iyong $1?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Aprubahan ang $1 nang walang limitasyon sa paggastos", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Magtakda ng limitasyon sa paggastos para sa iyong $1", - "description": "$1 is a token symbol" - }, "settings": { "message": "Mga Setting" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "Nasa kasalukuyang listahan ng mga network na ang URL." }, - "useDefault": { - "message": "Gamitin ang default" - }, "useMultiAccountBalanceChecker": { "message": "Mga kahilingan sa balanse ng batch account" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 9c9ee1ec8..842dc5096 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Tohum ifadesini ortaya çıkar" }, - "reviewSpendingCap": { - "message": "Harcama üst limitinize göz atın" - }, "revokeAllTokensTitle": { "message": "Tüm $1 için izin erişim ve transfer izni geri çekilsin mi?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "$1 için harcama limiti olmadan onay ver", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "$1 için harcama üst limiti belirle", - "description": "$1 is a token symbol" - }, "settings": { "message": "Ayarlar" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "Bu URL şu anda $1 ağı tarafından kullanılıyor." }, - "useDefault": { - "message": "Varsayılanı kullan" - }, "useMultiAccountBalanceChecker": { "message": "Toplu hesap bakiyesi talepleri" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 7c8851b15..5c11445e5 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "Hiện cụm từ khôi phục bí mật" }, - "reviewSpendingCap": { - "message": "Xem lại hạn mức chi tiêu của bạn" - }, "revokeAllTokensTitle": { "message": "Thu hồi quyền truy cập và chuyển tất cả $1 của bạn?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "Phê duyệt $1 không có giới hạn chi tiêu", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "Đặt hạn mức chi tiêu cho $1 của bạn", - "description": "$1 is a token symbol" - }, "settings": { "message": "Cài đặt" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "Mạng $1 hiện đang sử dụng URL này." }, - "useDefault": { - "message": "Sử dụng mặc định" - }, "useMultiAccountBalanceChecker": { "message": "Xử lý hàng loạt yêu cầu số dư tài khoản" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 9090ca86a..58f7ffa97 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -2956,9 +2956,6 @@ "revealTheSeedPhrase": { "message": "显示助记词" }, - "reviewSpendingCap": { - "message": "检查您的支出上限" - }, "revokeAllTokensTitle": { "message": "撤销访问和转移您的所有 $1 的权限?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -3140,10 +3137,6 @@ "message": "批准$1,且无消费限制", "description": "The token symbol that is being approved" }, - "setSpendingCap": { - "message": "为 $1 设置支出上限", - "description": "$1 is a token symbol" - }, "settings": { "message": "设置" }, @@ -4167,9 +4160,6 @@ "urlExistsErrorMsg": { "message": "此 URL 目前已被 $1 网络使用。" }, - "useDefault": { - "message": "使用默认值" - }, "useMultiAccountBalanceChecker": { "message": "账户余额分批请求" }, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 052f218a3..9be792c49 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -526,7 +526,8 @@ export default class PreferencesController { async updateSnapRegistry() { let snapRegistry; try { - snapRegistry = await fetch(KEYRING_SNAPS_REGISTRY_URL); + const response = await fetch(KEYRING_SNAPS_REGISTRY_URL); + snapRegistry = await response.json(); } catch (error) { console.error(`Failed to fetch registry: `, error); snapRegistry = {}; diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index cc8fc02c6..37de2830e 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1305,6 +1305,8 @@ export default class TransactionController extends EventEmitter { txMeta.custodyId, fromAddress, ); + + return null; } ///: END:ONLY_INCLUDE_IN diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7c40d960a..66326a010 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -315,31 +315,42 @@ export default class MetamaskController extends EventEmitter { ], }); - let initialProviderConfig; - if (process.env.IN_TEST) { - initialProviderConfig = { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - nickname: 'Localhost 8545', - ticker: 'ETH', + let initialNetworkControllerState = {}; + if (initState.NetworkController) { + initialNetworkControllerState = initState.NetworkController; + } else if (process.env.IN_TEST) { + initialNetworkControllerState = { + providerConfig: { + chainId: CHAIN_IDS.LOCALHOST, + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + type: 'rpc', + }, + networkConfigurations: { + networkConfigurationId: { + chainId: CHAIN_IDS.LOCALHOST, + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + networkConfigurationId: 'networkConfigurationId', + }, + }, }; } else if ( process.env.METAMASK_DEBUG || process.env.METAMASK_ENVIRONMENT === 'test' ) { - initialProviderConfig = { - type: NETWORK_TYPES.GOERLI, - chainId: CHAIN_IDS.GOERLI, - ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], + initialNetworkControllerState = { + providerConfig: { + type: NETWORK_TYPES.GOERLI, + chainId: CHAIN_IDS.GOERLI, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], + }, }; } - const initialNetworkControllerState = initialProviderConfig - ? { - providerConfig: initialProviderConfig, - ...initState.NetworkController, - } - : initState.NetworkController; this.networkController = new NetworkController({ messenger: networkControllerMessenger, state: initialNetworkControllerState, @@ -568,8 +579,12 @@ export default class MetamaskController extends EventEmitter { ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(this), + legacyAPIEndpoint: `${gasApiBaseUrl}/networks//gasPrices`, EIP1559APIEndpoint: `${gasApiBaseUrl}/networks//suggestedGasFees`, - getCurrentNetworkLegacyGasAPICompatibility: () => false, + getCurrentNetworkLegacyGasAPICompatibility: () => { + const { chainId } = this.networkController.state.providerConfig; + return chainId === CHAIN_IDS.BSC; + }, getChainId: () => this.networkController.state.providerConfig.chainId, }); @@ -1467,6 +1482,18 @@ export default class MetamaskController extends EventEmitter { this.signatureController.newUnsignedPersonalMessage.bind( this.signatureController, ), + + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + setTypedMessageInProgress: + this.signatureController.setTypedMessageInProgress.bind( + this.signatureController, + ), + setPersonalMessageInProgress: + this.signatureController.setPersonalMessageInProgress.bind( + this.signatureController, + ), + ///: END:ONLY_INCLUDE_IN + processEncryptionPublicKey: this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind( this.encryptionPublicKeyController, @@ -2468,6 +2495,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:handleRequest', ), + revokeDynamicSnapPermissions: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:revokeDynamicPermissions', + ), dismissNotifications: this.dismissNotifications.bind(this), markNotificationsAsRead: this.markNotificationsAsRead.bind(this), ///: END:ONLY_INCLUDE_IN diff --git a/builds.yml b/builds.yml index 019da9378..fe4d755d3 100644 --- a/builds.yml +++ b/builds.yml @@ -52,7 +52,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.35.2-flask.1/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.36.1-flask.1/index.html - SUPPORT_LINK: https://metamask-flask.zendesk.com/hc - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -71,7 +71,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.35.2-flask.1/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.36.1-flask.1/index.html - SUPPORT_LINK: https://metamask-flask.zendesk.com/hc - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -89,7 +89,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - SUPPORT_LINK: https://mmi-support.zendesk.com/hc/en-us - SUPPORT_REQUEST_LINK: https://mmi-support.zendesk.com/hc/en-us/requests/new - - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v1/configuration/default + - MMI_CONFIGURATION_SERVICE_URL # For some reason, MMI uses this type of versioning # Leaving it on for backwards compatibility isPrerelease: true diff --git a/development/lib/retry.js b/development/lib/retry.js index 204795142..ea469958b 100644 --- a/development/lib/retry.js +++ b/development/lib/retry.js @@ -11,13 +11,19 @@ * @param {string} args.rejectionMessage - The message for the rejected promise * this function will return in the event of failure. (Default: "Retry limit * reached") + * @param {string} args.retryUntilFailure - Retries until the function fails. * @param {Function} functionToRetry - The function that is run and tested for * failure. * @returns {Promise} a promise that either resolves to null if * the function is successful or is rejected with rejectionMessage otherwise. */ async function retry( - { retries, delay = 0, rejectionMessage = 'Retry limit reached' }, + { + retries, + delay = 0, + rejectionMessage = 'Retry limit reached', + retryUntilFailure = false, + }, functionToRetry, ) { let attempts = 0; @@ -28,9 +34,14 @@ async function retry( try { await functionToRetry(); - return; + if (!retryUntilFailure) { + return; + } } catch (error) { console.error(error); + if (retryUntilFailure) { + return; + } } finally { attempts += 1; } diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index aa3b955d2..e9b8572ae 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -1967,7 +1967,6 @@ "@metamask/rpc-methods-flask>@metamask/snaps-ui": true, "@metamask/rpc-methods-flask>@metamask/snaps-utils": true, "@metamask/rpc-methods-flask>@metamask/utils": true, - "@metamask/rpc-methods-flask>nanoid": true, "eth-rpc-errors": true, "superstruct": true } @@ -2173,7 +2172,6 @@ "@metamask/snaps-controllers-flask>@metamask/snaps-utils": true, "@metamask/snaps-controllers-flask>@metamask/snaps-utils>@metamask/snaps-ui": true, "@metamask/snaps-controllers-flask>@metamask/utils": true, - "@metamask/snaps-controllers-flask>nanoid": true, "eth-rpc-errors": true, "superstruct": true } diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index aa3b955d2..e9b8572ae 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1967,7 +1967,6 @@ "@metamask/rpc-methods-flask>@metamask/snaps-ui": true, "@metamask/rpc-methods-flask>@metamask/snaps-utils": true, "@metamask/rpc-methods-flask>@metamask/utils": true, - "@metamask/rpc-methods-flask>nanoid": true, "eth-rpc-errors": true, "superstruct": true } @@ -2173,7 +2172,6 @@ "@metamask/snaps-controllers-flask>@metamask/snaps-utils": true, "@metamask/snaps-controllers-flask>@metamask/snaps-utils>@metamask/snaps-ui": true, "@metamask/snaps-controllers-flask>@metamask/utils": true, - "@metamask/snaps-controllers-flask>nanoid": true, "eth-rpc-errors": true, "superstruct": true } diff --git a/package.json b/package.json index eb8e18f1b..fdb303c67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "10.33.0", + "version": "10.33.1", "private": true, "repository": { "type": "git", @@ -195,7 +195,8 @@ "fast-json-patch@^3.1.1": "patch:fast-json-patch@npm%3A3.1.1#./.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch", "request@^2.83.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", "request@^2.88.2": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", - "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch" + "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", + "@metamask/signature-controller@^4.0.1": "patch:@metamask/signature-controller@npm%3A4.0.1#./.yarn/patches/@metamask-signature-controller-npm-4.0.1-013e64c9fd.patch" }, "dependencies": { "@babel/runtime": "^7.18.9", @@ -229,7 +230,7 @@ "@metamask/base-controller": "^3.0.0", "@metamask/browser-passworder": "^4.1.0", "@metamask/contract-metadata": "^2.3.1", - "@metamask/controller-utils": "^4.0.1", + "@metamask/controller-utils": "^4.1.0", "@metamask/design-tokens": "^1.12.0", "@metamask/desktop": "^0.3.0", "@metamask/eth-json-rpc-middleware": "^11.0.0", @@ -239,13 +240,13 @@ "@metamask/eth-token-tracker": "^4.0.0", "@metamask/eth-trezor-keyring": "^1.0.0", "@metamask/etherscan-link": "^2.2.0", - "@metamask/gas-fee-controller": "^6.0.0", + "@metamask/gas-fee-controller": "^6.0.1", "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", - "@metamask/message-manager": "^7.0.0", + "@metamask/message-manager": "^7.0.2", "@metamask/metamask-eth-abis": "^3.0.0", - "@metamask/network-controller": "^10.1.0", + "@metamask/network-controller": "^10.3.0", "@metamask/notification-controller": "^3.0.0", "@metamask/obs-store": "^8.1.0", "@metamask/permission-controller": "^4.0.0", @@ -254,18 +255,18 @@ "@metamask/providers": "^11.1.0", "@metamask/rate-limit-controller": "^3.0.0", "@metamask/rpc-methods": "^1.0.0-prerelease.1", - "@metamask/rpc-methods-flask": "npm:@metamask/rpc-methods@0.35.2-flask.1", + "@metamask/rpc-methods-flask": "npm:@metamask/rpc-methods@0.36.1-flask.1", "@metamask/safe-event-emitter": "^2.0.0", "@metamask/scure-bip39": "^2.0.3", "@metamask/signature-controller": "^4.0.1", "@metamask/slip44": "^3.0.0", "@metamask/smart-transactions-controller": "^3.1.0", "@metamask/snaps-controllers": "^1.0.0-prerelease.1", - "@metamask/snaps-controllers-flask": "npm:@metamask/snaps-controllers@0.35.2-flask.1", + "@metamask/snaps-controllers-flask": "npm:@metamask/snaps-controllers@0.36.1-flask.1", "@metamask/snaps-ui": "^1.0.0-prerelease.1", - "@metamask/snaps-ui-flask": "npm:@metamask/snaps-ui@0.35.2-flask.1", + "@metamask/snaps-ui-flask": "npm:@metamask/snaps-ui@0.36.1-flask.1", "@metamask/snaps-utils": "^1.0.0-prerelease.1", - "@metamask/snaps-utils-flask": "npm:@metamask/snaps-utils@0.35.2-flask.1", + "@metamask/snaps-utils-flask": "npm:@metamask/snaps-utils@0.36.1-flask.1", "@metamask/subject-metadata-controller": "^2.0.0", "@metamask/utils": "^5.0.0", "@ngraveio/bc-ur": "^1.1.6", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index b25353dbe..c3eb9dc1d 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -174,8 +174,7 @@ export const BSC_DISPLAY_NAME = 'Binance Smart Chain'; export const POLYGON_DISPLAY_NAME = 'Polygon'; export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain'; export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One'; -export const BNB_DISPLAY_NAME = - 'BNB Smart Chain (previously Binance Smart Chain Mainnet)'; +export const BNB_DISPLAY_NAME = 'BNB Chain'; export const OPTIMISM_DISPLAY_NAME = 'Optimism'; export const FANTOM_DISPLAY_NAME = 'Fantom Opera'; export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0'; @@ -549,7 +548,6 @@ export const BUYABLE_CHAINS_MAP: { | typeof CHAIN_IDS.FANTOM_TESTNET | typeof CHAIN_IDS.MOONBEAM_TESTNET | typeof CHAIN_IDS.LINEA_GOERLI - | typeof CHAIN_IDS.LINEA_MAINNET | typeof CHAIN_IDS.GOERLI >]: BuyableChainSettings; } = { @@ -613,6 +611,10 @@ export const BUYABLE_CHAINS_MAP: { nativeCurrency: CURRENCY_SYMBOLS.PALM, network: 'palm', }, + [CHAIN_IDS.LINEA_MAINNET]: { + nativeCurrency: CURRENCY_SYMBOLS.ETH, + network: 'linea', + }, }; export const FEATURED_RPCS: RPCDefinition[] = [ diff --git a/shared/constants/snaps/permissions.ts b/shared/constants/snaps/permissions.ts index 360c4a379..be3590ef4 100644 --- a/shared/constants/snaps/permissions.ts +++ b/shared/constants/snaps/permissions.ts @@ -29,3 +29,5 @@ export const ExcludedSnapEndowments = Object.freeze({ 'endowment:long-running is deprecated. For more information please see https://github.com/MetaMask/snaps-monorepo/issues/945.', ///: END:ONLY_INCLUDE_IN }); + +export const DynamicSnapPermissions = Object.freeze(['eth_accounts']); diff --git a/shared/modules/transaction.utils.js b/shared/modules/transaction.utils.js index b07d1f869..44a681519 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -176,7 +176,7 @@ export async function determineTransactionType(txParams, query) { contractCode = resultCode; if (isContractAddress) { - const hasValue = txParams.value && txParams.value !== '0x0'; + const hasValue = txParams.value && Number(txParams.value) !== 0; const tokenMethodName = [ TransactionType.tokenMethodApprove, diff --git a/shared/modules/transaction.utils.test.js b/shared/modules/transaction.utils.test.js index 615fda2e3..242d10007 100644 --- a/shared/modules/transaction.utils.test.js +++ b/shared/modules/transaction.utils.test.js @@ -186,6 +186,20 @@ describe('Transaction.utils', function () { getCodeResponse: '0xab', }); + const resultWithEmptyValue2 = await determineTransactionType( + { + value: '0x0000', + to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', + data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', + }, + new EthQuery(_provider), + ); + + expect(resultWithEmptyValue2).toMatchObject({ + type: TransactionType.tokenMethodTransfer, + getCodeResponse: '0xab', + }); + const resultWithValue = await determineTransactionType( { value: '0x12345', diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index d6a4bb2ab..8c0ffb76a 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -105,7 +105,9 @@ async function withFixtures(options, testSuite) { }); } } - const mockedEndpoint = await setupMocking(mockServer, testSpecificMock); + const mockedEndpoint = await setupMocking(mockServer, testSpecificMock, { + chainId: ganacheOptions?.chainId || 1337, + }); await mockServer.start(8000); driver = (await buildWebDriver(driverOptions)).driver; diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index e62958513..b52f6d938 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -291,13 +291,12 @@ describe('MetaMask', function () { it('transitions to the confirm screen', async function () { // Continue to next screen - await driver.delay(largeDelayMs); + await driver.waitForElementNotPresent('.loading-overlay'); await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.delay(largeDelayMs); }); it('displays the token transfer data', async function () { - await driver.delay(largeDelayMs); + await driver.waitForElementNotPresent('.loading-overlay'); await driver.clickElement({ text: 'Hex', tag: 'button' }); await driver.delay(regularDelayMs); @@ -481,7 +480,7 @@ describe('MetaMask', function () { }); it('submits the transaction', async function () { - await driver.delay(largeDelayMs * 2); + await driver.waitForElementNotPresent('.loading-overlay'); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(largeDelayMs * 2); }); diff --git a/test/e2e/metrics/transaction-finalized.spec.js b/test/e2e/metrics/transaction-finalized.spec.js new file mode 100644 index 000000000..32c245255 --- /dev/null +++ b/test/e2e/metrics/transaction-finalized.spec.js @@ -0,0 +1,326 @@ +/* eslint-disable no-useless-escape */ +const { strict: assert } = require('assert'); +const { + defaultGanacheOptions, + withFixtures, + unlockWallet, + sendTransaction, + getEventPayloads, +} = require('../helpers'); +const FixtureBuilder = require('../fixture-builder'); + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Transaction Submitted' and 'Transaction Finalized'. In addition on the + * first event of each series we require a field that should only appear in the + * anonymized events so that we can guarantee order of seenRequests and can + * properly make assertions. Do not use the constants from the metrics + * constants files, because if these change we want a strong indicator to our + * data team that the shape of data will change. + * + * @param {import('mockttp').Mockttp} mockServer + * @returns {Promise[]} + */ +async function mockSegment(mockServer) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { + type: 'track', + event: 'Transaction Submitted', + properties: { status: 'submitted' }, + }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { + type: 'track', + event: 'Transaction Submitted', + }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { + type: 'track', + event: 'Transaction Finalized', + properties: { status: 'confirmed' }, + }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { + type: 'track', + event: 'Transaction Finalized', + }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} + +const RECIPIENT = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3'; + +describe('Transaction Finalized Event', function () { + it('Successfully tracked when sending a transaction', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.title, + testSpecificMock: mockSegment, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + await driver.navigate(); + await unlockWallet(driver); + + await sendTransaction(driver, RECIPIENT, '2.0'); + + const events = await getEventPayloads(driver, mockedEndpoints); + // The order of these events is ensured by the mockSegment function + // which requires that the first, and third (0, 2 indexes) have the + // status property, which will only appear on the anonymous events. + const transactionSubmittedNoMMId = events[0]; + const transactionSubmittedWithMMId = events[1]; + const transactionFinalizedNoMMId = events[2]; + const transactionFinalizedWithMMId = events[3]; + + // Assert doesn't have generic matchers so we delete these timestamp related properties. + // If we switch to a different assertion library we can use generic matchers in the future. + delete transactionSubmittedNoMMId.properties.first_seen; + delete transactionFinalizedNoMMId.properties.first_seen; + delete transactionFinalizedNoMMId.properties.completion_time; + + // Assert that the event names begin with the appropriate prefixes. Even finalized events begin with transaction-submitted + // because they start as event fragments created when the transaction is submitted. + assert.ok( + transactionSubmittedNoMMId.messageId.startsWith( + 'transaction-submitted', + ), + `Transaction Submitted event with sensitive properties has messageId \"${transactionSubmittedNoMMId.messageId}\" does not have a messageId beginning with \"transaction-submitted\"`, + ); + + assert.ok( + transactionFinalizedNoMMId.messageId.startsWith( + 'transaction-submitted', + ), + `Transaction Finalized event with sensitive properties has messageId \"${transactionFinalizedNoMMId.messageId}\" that does not begin with \"transaction-submitted\"`, + ); + + assert.ok( + transactionSubmittedWithMMId.messageId.startsWith( + 'transaction-submitted', + ), + `Transaction Submitted event has messageId \"${transactionSubmittedWithMMId.messageId}\" that does not begin with \"transaction-submitted\"`, + ); + + assert.ok( + transactionFinalizedWithMMId.messageId.startsWith( + 'transaction-submitted', + ), + `Transaction Finalized event has messageID \"${transactionFinalizedWithMMId.messageId}\" that does not begin with \"transaction-submitted\"`, + ); + + // Assert that the events with sensitive properties should have messageIds ending in 0x000 + // This is important because otherwise the events are seen as duplicates in segment + + assert.ok( + transactionSubmittedNoMMId.messageId.endsWith('0x000'), + `Transaction Submitted event with sensitive properties has messageId \"${transactionSubmittedNoMMId.messageId}\" that does not end in \"0x000\" to differentiate it from the event that does not include sensitive data.`, + ); + + assert.ok( + transactionFinalizedNoMMId.messageId.endsWith('0x000'), + `Transaction Finalized event with sensitive properties has messageID \"${transactionFinalizedNoMMId.messageId}\" that does not end in \"0x000\" to differentiate it from the event that does not include sensitive data.`, + ); + + // Assert that transaction finalized events contain '-success-' in their messageId + assert.ok( + transactionFinalizedWithMMId.messageId.includes('-success-'), + `Transaction Finalized event has messageId \"${transactionFinalizedWithMMId.messageId}\" that does not contain "-success-"`, + ); + + assert.ok( + transactionFinalizedNoMMId.messageId.includes('-success-'), + `Transaction Finalized event with sensitive properties has messageID \"${transactionFinalizedNoMMId.messageId}\" that does not contain "-success-"`, + ); + + // Assert that the events with sensitive data do not contain a userId (the random anonymous id generated when a user opts into metametrics) + assert.ok( + typeof transactionSubmittedNoMMId.userId === 'undefined', + 'Transaction Submitted event with sensitive properties has a userId supplied when it should only have the anonymousId', + ); + assert.ok( + typeof transactionFinalizedNoMMId.userId === 'undefined', + 'Transaction Finalized event with sensitive properties has a userId supplied when it should only have the anonymousId', + ); + + // Assert that the events with sensitive data have anonymousId set to 0x0000000000000000 which is our universal anonymous record + assert.ok( + transactionSubmittedNoMMId.anonymousId === '0x0000000000000000', + 'Transaction Submitted event with sensitive properties has an anonymousId that does not match our universal anonymous id of 0x0000000000000000', + ); + assert.ok( + transactionFinalizedNoMMId.anonymousId === '0x0000000000000000', + 'Transaction Finalized event with sensitive properties has an anonymousId that does not match our universal anonymous id of 0x0000000000000000', + ); + + // Assert that our events without sensitive data have a userId but no anonymousId + assert.ok( + typeof transactionSubmittedWithMMId.userId === 'string' && + typeof transactionSubmittedWithMMId.anonymousId === 'undefined', + 'Transaction Submitted event without sensitive properties should only have a userId specified, and no anonymousId', + ); + assert.ok( + typeof transactionFinalizedWithMMId.userId === 'string' && + typeof transactionFinalizedWithMMId.anonymousId === 'undefined', + 'Transaction Finalized event without sensitive properties should only have a userId specified, and no anonymousId', + ); + + // Assert on the properties + + assert.deepStrictEqual( + transactionSubmittedNoMMId.properties, + { + status: 'submitted', + transaction_envelope_type: 'legacy', + gas_limit: '0x5208', + gas_price: '2', + default_gas: '0.000021', + default_gas_price: '2', + chain_id: '0x539', + referrer: 'metamask', + source: 'user', + network: '1337', + eip_1559_version: '0', + gas_edit_type: 'none', + gas_edit_attempted: 'none', + account_type: 'MetaMask', + device_model: 'N/A', + asset_type: 'NATIVE', + token_standard: 'NONE', + transaction_type: 'simpleSend', + transaction_speed_up: false, + ui_customizations: null, + category: 'Transactions', + locale: 'en', + environment_type: 'background', + }, + 'Transaction Submitted event with sensitive properties does not match the expected payload', + ); + assert.deepStrictEqual( + transactionSubmittedWithMMId.properties, + { + chain_id: '0x539', + referrer: 'metamask', + source: 'user', + network: '1337', + eip_1559_version: '0', + gas_edit_type: 'none', + gas_edit_attempted: 'none', + account_type: 'MetaMask', + device_model: 'N/A', + asset_type: 'NATIVE', + token_standard: 'NONE', + transaction_type: 'simpleSend', + transaction_speed_up: false, + ui_customizations: null, + category: 'Transactions', + locale: 'en', + environment_type: 'background', + }, + 'Transaction Submitted event without sensitive properties does not match the expected payload', + ); + + assert.deepStrictEqual( + transactionFinalizedNoMMId.properties, + { + status: 'confirmed', + transaction_envelope_type: 'legacy', + gas_limit: '0x5208', + gas_price: '2', + default_gas: '0.000021', + default_gas_price: '2', + chain_id: '0x539', + referrer: 'metamask', + source: 'user', + network: '1337', + eip_1559_version: '0', + gas_edit_type: 'none', + gas_edit_attempted: 'none', + account_type: 'MetaMask', + device_model: 'N/A', + asset_type: 'NATIVE', + token_standard: 'NONE', + transaction_type: 'simpleSend', + transaction_speed_up: false, + ui_customizations: null, + gas_used: '5208', + category: 'Transactions', + locale: 'en', + environment_type: 'background', + }, + 'Transaction Finalized event with sensitive properties does not match the expected payload', + ); + assert.deepStrictEqual( + transactionFinalizedWithMMId.properties, + { + chain_id: '0x539', + referrer: 'metamask', + source: 'user', + network: '1337', + eip_1559_version: '0', + gas_edit_type: 'none', + gas_edit_attempted: 'none', + account_type: 'MetaMask', + device_model: 'N/A', + asset_type: 'NATIVE', + token_standard: 'NONE', + transaction_type: 'simpleSend', + transaction_speed_up: false, + ui_customizations: null, + category: 'Transactions', + locale: 'en', + environment_type: 'background', + }, + 'Transaction Finalized event without sensitive properties does not match the expected payload', + ); + }, + ); + }); +}); diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 29035f46c..a3e48208d 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -20,7 +20,16 @@ const emptyStalelist = { lastUpdated: 0, }; -async function setupMocking(server, testSpecificMock) { +/** + * Setup E2E network mocks. + * + * @param {object} server - The mock server used for network mocks. + * @param {Function} testSpecificMock - A function for setting up test-specific network mocks + * @param {object} options - Network mock options. + * @param {string} options.chainId - The chain ID used by the default configured network. + * @returns + */ +async function setupMocking(server, testSpecificMock, { chainId }) { await server.forAnyRequest().thenPassThrough({ beforeRequest: (req) => { const { host } = req.headers; @@ -99,7 +108,9 @@ async function setupMocking(server, testSpecificMock) { }); await server - .forGet('https://gas-api.metaswap.codefi.network/networks/1337/gasPrices') + .forGet( + `https://gas-api.metaswap.codefi.network/networks/${chainId}/gasPrices`, + ) .thenCallback(() => { return { statusCode: 200, @@ -130,7 +141,7 @@ async function setupMocking(server, testSpecificMock) { await server .forGet( - 'https://gas-api.metaswap.codefi.network/networks/1337/suggestedGasFees', + `https://gas-api.metaswap.codefi.network/networks/${chainId}/suggestedGasFees`, ) .thenCallback(() => { return { @@ -203,7 +214,7 @@ async function setupMocking(server, testSpecificMock) { }); await server - .forGet('https://token-api.metaswap.codefi.network/tokens/1337') + .forGet(`https://token-api.metaswap.codefi.network/tokens/${chainId}`) .thenCallback(() => { return { statusCode: 200, @@ -343,7 +354,7 @@ async function setupMocking(server, testSpecificMock) { }); await server - .forGet('https://token-api.metaswap.codefi.network/token/1337') + .forGet(`https://token-api.metaswap.codefi.network/token/${chainId}`) .thenCallback(() => { return { statusCode: 200, @@ -352,15 +363,15 @@ async function setupMocking(server, testSpecificMock) { }); // It disables loading of token icons, e.g. this URL: https://static.metafi.codefi.network/api/v1/tokenIcons/1337/0x0000000000000000000000000000000000000000.png - await server - .forGet( - /^https:\/\/static\.metafi\.codefi\.network\/api\/v1\/tokenIcons\/1337\/.*\.png/u, - ) - .thenCallback(() => { - return { - statusCode: 200, - }; - }); + const tokenIconRegex = new RegExp( + `^https:\\/\\/static\\.metafi\\.codefi\\.network\\/api\\/vi\\/tokenIcons\\/${chainId}\\/.*\\.png`, + 'u', + ); + await server.forGet(tokenIconRegex).thenCallback(() => { + return { + statusCode: 200, + }; + }); await server .forGet('https://min-api.cryptocompare.com/data/price') diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js index 92eef2533..1c2c60362 100644 --- a/test/e2e/run-e2e-test.js +++ b/test/e2e/run-e2e-test.js @@ -31,6 +31,11 @@ async function main() { 'Set how many times the test should be retried upon failure.', type: 'number', }) + .option('retry-until-failure', { + default: false, + description: 'Retries until the test fails', + type: 'boolean', + }) .option('leave-running', { default: false, description: @@ -46,7 +51,14 @@ async function main() { .strict() .help('help'); - const { browser, debug, e2eTestPath, retries, leaveRunning } = argv; + const { + browser, + debug, + e2eTestPath, + retries, + retryUntilFailure, + leaveRunning, + } = argv; if (!browser) { exitWithError( @@ -97,7 +109,7 @@ async function main() { const dir = 'test/test-results/e2e'; fs.mkdir(dir, { recursive: true }); - await retry({ retries }, async () => { + await retry({ retries, retryUntilFailure }, async () => { await runInShell( 'yarn', [ diff --git a/test/e2e/tests/contract-interactions.spec.js b/test/e2e/tests/contract-interactions.spec.js index 02e95d7bd..c5c0e6600 100644 --- a/test/e2e/tests/contract-interactions.spec.js +++ b/test/e2e/tests/contract-interactions.spec.js @@ -3,6 +3,9 @@ const { withFixtures, openDapp, locateAccountBalanceDOM, + unlockWallet, + largeDelayMs, + WINDOW_TITLES, } = require('../helpers'); const { SMART_CONTRACTS } = require('../seeder/smart-contracts'); const FixtureBuilder = require('../fixture-builder'); @@ -34,34 +37,32 @@ describe('Deploy contract and call contract methods', function () { smartContract, ); await driver.navigate(); - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); + await unlockWallet(driver); // deploy contract await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent - await driver.findClickableElement('#deployButton'); + await driver.delay(largeDelayMs); await driver.clickElement('#depositButton'); - await driver.waitUntilXWindowHandles(3); - let windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - const dapp = await driver.switchToWindowWithTitle( - 'E2E Test Dapp', - windowHandles, - ); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); + await driver.waitForSelector({ + css: 'span', + text: 'Deposit initiated', + }); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Notification); 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.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement({ text: 'Activity', tag: 'button' }); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', @@ -72,17 +73,16 @@ describe('Deploy contract and call contract methods', function () { }); // calls and confirms a contract method where ETH is received - await driver.switchToWindow(dapp); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#withdrawButton'); await driver.waitUntilXWindowHandles(3); - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Notification); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', ); @@ -92,7 +92,9 @@ describe('Deploy contract and call contract methods', function () { }); // renders the correct ETH balance - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await locateAccountBalanceDOM(driver, ganacheServer); }, ); diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index fe78862d4..57d2a3d6a 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -129,10 +129,6 @@ describe('Create token, approve token and approve token without gas', function ( text: 'Got it', tag: 'button', }); - await driver.clickElement({ - text: 'Use default', - css: '.mm-button-link', - }); await driver.clickElement({ text: 'View details', css: '.token-allowance-container__view-details', @@ -157,17 +153,17 @@ describe('Create token, approve token and approve token without gas', function ( await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.findElement({ - text: 'Review the spending cap for your', - tag: 'div', + text: 'Spending cap request for your ', + css: '.box--flex-direction-row', }); - const defaultSpendingCup = await driver.findElement({ + const defaultSpendingCap = await driver.findElement({ text: '7 TST', css: '.box--flex-direction-row > h6', }); assert.equal( - await defaultSpendingCup.getText(), + await defaultSpendingCap.getText(), '7 TST', 'Default value is not correctly set', ); @@ -253,13 +249,13 @@ describe('Create token, approve token and approve token without gas', function ( tag: 'button', }); - let spendingCup = await driver.findElement({ + let spendingCap = await driver.findElement({ text: '5 TST', css: '.box--flex-direction-row > h6', }); assert.equal( - await spendingCup.getText(), + await spendingCap.getText(), '5 TST', 'Default value is not correctly set', ); @@ -309,12 +305,12 @@ describe('Create token, approve token and approve token without gas', function ( tag: 'button', }); - spendingCup = await driver.findElement({ + spendingCap = await driver.findElement({ text: '9 TST', css: '.box--flex-direction-row > h6', }); assert.equal( - await spendingCup.getText(), + await spendingCap.getText(), '9 TST', 'Default value is not correctly set', ); @@ -429,7 +425,7 @@ describe('Create token, approve token and approve token without gas', function ( ); }); - it('approves token without gas, set default spending cap, submits the transaction and finds the transaction in the transactions list', async function () { + it('approves token without gas, set site suggested spending cap, submits the transaction and finds the transaction in the transactions list', async function () { await withFixtures( { dapp: true, @@ -465,9 +461,16 @@ describe('Create token, approve token and approve token without gas', function ( const pendingTxes = await driver.findElements('.transaction-list-item'); pendingTxes[0].click(); - // set spending cap + + // set custom spending cap + const spendingCap = await driver.findElement( + '[data-testid="custom-spending-cap-input"]', + ); + await spendingCap.fill('5'); + + // set site suggested spending cap await driver.clickElement({ - text: 'Use default', + text: 'Use site suggestion', css: '.mm-button-link', }); await driver.clickElement({ diff --git a/test/e2e/tests/encrypt-decrypt.spec.js b/test/e2e/tests/encrypt-decrypt.spec.js index 59e87648e..bdce53f8f 100644 --- a/test/e2e/tests/encrypt-decrypt.spec.js +++ b/test/e2e/tests/encrypt-decrypt.spec.js @@ -2,7 +2,7 @@ const { strict: assert } = require('assert'); const { convertToHexValue, withFixtures, openDapp } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); -async function getEncryptionKey(driver) { +async function validateEncryptionKey(driver, encryptionKey) { await driver.clickElement('#getEncryptionKeyButton'); await driver.waitUntilXWindowHandles(3); let windowHandles = await driver.getAllWindowHandles(); @@ -15,7 +15,10 @@ async function getEncryptionKey(driver) { await driver.waitUntilXWindowHandles(2); windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - return await driver.findElement('#encryptionKeyDisplay'); + await driver.findElement({ + css: '#encryptionKeyDisplay', + text: encryptionKey, + }); } async function encryptMessage(driver, message) { @@ -51,8 +54,10 @@ async function verifyDecryptedMessageMM(driver, message) { async function verifyDecryptedMessageDapp(driver, message) { const windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - const clearTextLabel = await driver.findElement('#cleartextDisplay'); - assert.equal(await clearTextLabel.getText(), message); + await driver.findElement({ + css: '#cleartextDisplay', + text: message, + }); } describe('Encrypt Decrypt', function () { @@ -67,6 +72,7 @@ describe('Encrypt Decrypt', function () { }; const encryptionKey = 'fxYXfCbun026g5zcCQh7Ia+O0urAEVZWLG8H4Jzu7Xs='; const message = 'Hello, Bob!'; + it('should decrypt an encrypted message', async function () { await withFixtures( { @@ -84,8 +90,7 @@ describe('Encrypt Decrypt', function () { await openDapp(driver); // ------ Get Encryption key ------ - const encryptionKeyLabel = await getEncryptionKey(driver); - assert.equal(await encryptionKeyLabel.getText(), encryptionKey); + await validateEncryptionKey(driver, encryptionKey); // ------ Encrypt ------ await encryptMessage(driver, message); @@ -126,8 +131,7 @@ describe('Encrypt Decrypt', function () { await openDapp(driver); // ------ Get Encryption key ------ - const encryptionKeyLabel = await getEncryptionKey(driver); - assert.equal(await encryptionKeyLabel.getText(), encryptionKey); + await validateEncryptionKey(driver, encryptionKey); // ------ Encrypt Message 1------ await encryptMessage(driver, message); diff --git a/test/e2e/tests/gas-estimates.spec.js b/test/e2e/tests/gas-estimates.spec.js index a7d121f31..f9f27029a 100644 --- a/test/e2e/tests/gas-estimates.spec.js +++ b/test/e2e/tests/gas-estimates.spec.js @@ -4,6 +4,7 @@ const { logInWithBalanceValidation, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); +const { CHAIN_IDS } = require('../../../shared/constants/network'); describe('Gas estimates generated by MetaMask', function () { const baseGanacheOptions = { @@ -139,7 +140,85 @@ describe('Gas estimates generated by MetaMask', function () { }); describe('Send on a network that is not EIP-1559 compatible', function () { - it('show expected gas defaults', async function () { + it('show expected gas defaults on a network supported by legacy gas API', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: { + ...preLondonGanacheOptions, + chainId: parseInt(CHAIN_IDS.BSC, 16), + }, + title: this.test.title, + }, + async ({ driver, ganacheServer }) => { + await driver.navigate(); + await logInWithBalanceValidation(driver, ganacheServer); + + await driver.clickElement('[data-testid="eth-overview-send"]'); + + await driver.fill( + 'input[placeholder="Enter public address (0x) or ENS name"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + + await driver.fill('.unit-input__input', '1'); + + // Check that the gas estimation is what we expect + await driver.findElement({ + cass: '[data-testid="confirm-gas-display"]', + text: '0.000042', + }); + }, + ); + }); + + it('show expected gas defaults on a network supported by legacy gas API when that API is down', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: { + ...preLondonGanacheOptions, + chainId: parseInt(CHAIN_IDS.BSC, 16), + }, + testSpecificMock: (mockServer) => { + mockServer + .forGet( + `https://gas-api.metaswap.codefi.network/networks/${parseInt( + CHAIN_IDS.BSC, + 16, + )}/gasPrices`, + ) + .thenCallback(() => { + return { + statusCode: 422, + }; + }); + }, + title: this.test.title, + }, + async ({ driver, ganacheServer }) => { + await driver.navigate(); + await logInWithBalanceValidation(driver, ganacheServer); + + await driver.clickElement('[data-testid="eth-overview-send"]'); + + await driver.fill( + 'input[placeholder="Enter public address (0x) or ENS name"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + + await driver.fill('.unit-input__input', '1'); + + // Check that the gas estimation is what we expect + await driver.findElement({ + cass: '[data-testid="confirm-gas-display"]', + text: '0.000042', + }); + }, + ); + }); + + it('show expected gas defaults on a network not supported by legacy gas API', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), diff --git a/test/e2e/tests/import-flow.spec.js b/test/e2e/tests/import-flow.spec.js index 19f02219a..b89e2bbf5 100644 --- a/test/e2e/tests/import-flow.spec.js +++ b/test/e2e/tests/import-flow.spec.js @@ -51,14 +51,14 @@ describe('Import flow', function () { await driver.findVisibleElement('.qr-code__wrapper'); // shows a QR code for the account - await driver.findVisibleElement('.popover-container'); + await driver.findVisibleElement('.mm-modal'); // shows the correct account address const address = await driver.findElement( '.multichain-address-copy-button', ); assert.equal(await address.getText(), '0x0Cc...afD3'); - await driver.clickElement('[data-testid="popover-close"]'); + await driver.clickElement('.mm-modal button[aria-label="Close"]'); // logs out of the account await driver.clickElement( diff --git a/test/e2e/tests/incremental-security.spec.js b/test/e2e/tests/incremental-security.spec.js index 29535738c..e96b49c19 100644 --- a/test/e2e/tests/incremental-security.spec.js +++ b/test/e2e/tests/incremental-security.spec.js @@ -78,8 +78,8 @@ describe('Incremental Security', function () { const publicAddress = await address.getText(); // wait for account modal to be visible - const accountModal = await driver.findVisibleElement('.popover-bg'); - await driver.clickElement('[data-testid="popover-close"]'); + const accountModal = await driver.findVisibleElement('.mm-modal'); + await driver.clickElement('.mm-modal button[aria-label="Close"]'); // wait for account modal to be removed from DOM await accountModal.waitForElementState('hidden'); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 2c222ae95..c1284f70c 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -161,16 +161,18 @@ class Driver { // replacement for the implementation below. It takes an option options // bucket that can include the state attribute to wait for elements that // match the selector to be removed from the DOM. - const selector = this.buildLocator(rawLocator); let element; if (!['visible', 'detached'].includes(state)) { throw new Error(`Provided state selector ${state} is not supported`); } if (state === 'visible') { - element = await this.driver.wait(until.elementLocated(selector), timeout); + element = await this.driver.wait( + until.elementLocated(this.buildLocator(rawLocator)), + timeout, + ); } else if (state === 'detached') { element = await this.driver.wait( - until.stalenessOf(await this.findElement(selector)), + until.stalenessOf(await this.findElement(rawLocator)), timeout, ); } @@ -205,15 +207,13 @@ class Driver { } async findVisibleElement(rawLocator) { - const locator = this.buildLocator(rawLocator); - const element = await this.findElement(locator); + const element = await this.findElement(rawLocator); await this.driver.wait(until.elementIsVisible(element), this.timeout); return wrapElementWithAPI(element, this); } async findClickableElement(rawLocator) { - const locator = this.buildLocator(rawLocator); - const element = await this.findElement(locator); + const element = await this.findElement(rawLocator); await Promise.all([ this.driver.wait(until.elementIsVisible(element), this.timeout), this.driver.wait(until.elementIsEnabled(element), this.timeout), @@ -231,8 +231,7 @@ class Driver { } async findClickableElements(rawLocator) { - const locator = this.buildLocator(rawLocator); - const elements = await this.findElements(locator); + const elements = await this.findElements(rawLocator); await Promise.all( elements.reduce((acc, element) => { acc.push( @@ -246,14 +245,12 @@ class Driver { } async clickElement(rawLocator) { - const locator = this.buildLocator(rawLocator); - const element = await this.findClickableElement(locator); + const element = await this.findClickableElement(rawLocator); await element.click(); } async clickPoint(rawLocator, x, y) { - const locator = this.buildLocator(rawLocator); - const element = await this.findElement(locator); + const element = await this.findElement(rawLocator); await this.driver .actions() .move({ origin: element, x, y }) @@ -281,10 +278,9 @@ class Driver { } async assertElementNotPresent(rawLocator) { - const locator = this.buildLocator(rawLocator); let dataTab; try { - dataTab = await this.findElement(locator); + dataTab = await this.findElement(rawLocator); } catch (err) { assert( err instanceof webdriverError.NoSuchElementError || @@ -375,7 +371,7 @@ class Driver { return await this.driver.getAllWindowHandles(); } - async waitUntilXWindowHandles(x, delayStep = 1000, timeout = 5000) { + async waitUntilXWindowHandles(x, delayStep = 1000, timeout = this.timeout) { let timeElapsed = 0; let windowHandles = []; while (timeElapsed <= timeout) { diff --git a/test/env.js b/test/env.js index 4056fc287..dd3c370b8 100644 --- a/test/env.js +++ b/test/env.js @@ -1,4 +1,4 @@ process.env.METAMASK_ENVIRONMENT = 'test'; process.env.SUPPORT_LINK = 'https://support.metamask.io'; process.env.IFRAME_EXECUTION_ENVIRONMENT_URL = - 'https://execution.metamask.io/0.35.2-flask.1/index.html'; + 'https://execution.metamask.io/0.36.1-flask.1/index.html'; diff --git a/test/jest/setup.js b/test/jest/setup.js index a3c5d2568..94feffdff 100644 --- a/test/jest/setup.js +++ b/test/jest/setup.js @@ -127,3 +127,6 @@ expect.extend({ }; }, }); + +// Setup window.prompt +global.prompt = () => undefined; diff --git a/ui/components/app/custom-spending-cap/__snapshots__/custom-spending-cap.test.js.snap b/ui/components/app/custom-spending-cap/__snapshots__/custom-spending-cap.test.js.snap index 46ca44ffb..2b2dff9b4 100644 --- a/ui/components/app/custom-spending-cap/__snapshots__/custom-spending-cap.test.js.snap +++ b/ui/components/app/custom-spending-cap/__snapshots__/custom-spending-cap.test.js.snap @@ -51,15 +51,6 @@ exports[`CustomSpendingCap should match snapshot 1`] = ` -
- -
@@ -85,7 +76,7 @@ exports[`CustomSpendingCap should match snapshot 1`] = ` class="box custom-spending-cap__description box--flex-direction-row" >
@@ -93,7 +84,7 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
- 10 + 7 TST
@@ -101,6 +92,13 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
+ + Learn more + diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.js b/ui/components/app/custom-spending-cap/custom-spending-cap.js index d2da1adfe..da9033c07 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.js @@ -1,5 +1,5 @@ import React, { useState, useContext, useEffect, useRef } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import BigNumber from 'bignumber.js'; @@ -21,7 +21,6 @@ import { BackgroundColor, TextColor, } from '../../../helpers/constants/design-system'; -import { getCustomTokenAmount } from '../../../selectors'; import { setCustomTokenAmount } from '../../../ducks/app/app'; import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; @@ -34,6 +33,7 @@ import { Numeric } from '../../../../shared/modules/Numeric'; import { estimateGas } from '../../../store/actions'; import { getCustomTxParamsData } from '../../../pages/confirm-approve/confirm-approve.util'; import { useGasFeeContext } from '../../../contexts/gasFee'; +import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip'; export default function CustomSpendingCap({ @@ -45,18 +45,17 @@ export default function CustomSpendingCap({ passTheErrorText, decimals, setInputChangeInProgress, + customSpendingCap, + setCustomSpendingCap, }) { const t = useContext(I18nContext); const dispatch = useDispatch(); const { updateTransaction } = useGasFeeContext(); const inputRef = useRef(null); - const value = useSelector(getCustomTokenAmount); - const [error, setError] = useState(''); - const [showUseDefaultButton, setShowUseDefaultButton] = useState( - value !== String(dappProposedValue) && true, - ); + const [showUseSiteSuggestionButton, setShowUseSiteSuggestionButton] = + useState(customSpendingCap !== String(dappProposedValue) && true); const inputLogicEmptyStateText = t('inputLogicEmptyState'); const replaceCommaToDot = (inputValue) => { @@ -102,7 +101,7 @@ export default function CustomSpendingCap({ }; const [customSpendingCapText, setCustomSpendingCapText] = useState( - getInputTextLogic(value).description, + getInputTextLogic(customSpendingCap).description, ); const handleChange = async (valueInput) => { @@ -139,37 +138,42 @@ export default function CustomSpendingCap({ setError(spendingCapError); } } - + setCustomSpendingCap(String(valueInput)); dispatch(setCustomTokenAmount(String(valueInput))); - try { - const newData = getCustomTxParamsData(txParams.data, { - customPermissionAmount: valueInput, - decimals, - }); - const { from, to, value: txValue } = txParams; - const estimatedGasLimit = await estimateGas({ - from, - to, - value: txValue, - data: newData, - }); - if (estimatedGasLimit) { - await updateTransaction({ - gasLimit: hexToDecimal(addHexPrefix(estimatedGasLimit)), + if (String(valueInput) !== '') { + try { + const newData = getCustomTxParamsData(txParams.data, { + customPermissionAmount: valueInput, + decimals, }); + const { from, to, value: txValue } = txParams; + const estimatedGasLimit = await estimateGas({ + from, + to, + value: txValue, + data: newData, + }); + if (estimatedGasLimit) { + await updateTransaction({ + gasLimit: hexToDecimal(addHexPrefix(estimatedGasLimit)), + }); + } + } catch (exp) { + console.error('Error in trying to update gas limit', exp); } - } catch (exp) { - console.error('Error in trying to update gas limit', exp); } + setInputChangeInProgress(false); }; useEffect(() => { - if (value !== String(dappProposedValue)) { - setShowUseDefaultButton(true); + if (customSpendingCap === String(dappProposedValue)) { + setShowUseSiteSuggestionButton(false); + } else { + setShowUseSiteSuggestionButton(true); } - }, [value, dappProposedValue]); + }, [customSpendingCap, dappProposedValue]); useEffect(() => { passTheErrorText(error); @@ -185,7 +189,7 @@ export default function CustomSpendingCap({ }, [inputRef.current]); const chooseTooltipContentText = decConversionGreaterThan( - value, + customSpendingCap, currentTokenBalance, ) ? t('warningTooltipText', [ @@ -221,7 +225,7 @@ export default function CustomSpendingCap({ > @@ -345,4 +361,12 @@ CustomSpendingCap.propTypes = { * Updating input state to changing */ setInputChangeInProgress: PropTypes.func.isRequired, + /** + * Custom token amount or The dapp suggested amount + */ + customSpendingCap: PropTypes.string, + /** + * State method to update the custom token value + */ + setCustomSpendingCap: PropTypes.func.isRequired, }; diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js index 0e3edfa6b..0e492a43b 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js @@ -23,6 +23,9 @@ export default { decimals: { control: 'text', }, + customSpendingCap: { + control: { type: 'text' }, + }, }, args: { tokenName: 'DAI', @@ -30,6 +33,7 @@ export default { dappProposedValue: '7', siteOrigin: 'Uniswap.org', decimals: '4', + customSpendingCap: '7', }, }; @@ -38,3 +42,13 @@ export const DefaultStory = (args) => { }; DefaultStory.storyName = 'Default'; + +export const CustomSpendingCapStory = (args) => { + return ; +}; +CustomSpendingCapStory.storyName = 'CustomSpendingCapStory'; + +CustomSpendingCapStory.args = { + ...DefaultStory.args, + customSpendingCap: '8', +}; diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.test.js b/ui/components/app/custom-spending-cap/custom-spending-cap.test.js index 17f475691..41b338a7c 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.test.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.test.js @@ -26,6 +26,8 @@ const props = { decimals: '4', passTheErrorText: () => undefined, setInputChangeInProgress: () => undefined, + customSpendingCap: '7', + setCustomSpendingCap: () => undefined, }; describe('CustomSpendingCap', () => { diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js index 832e8a770..e48f3462b 100644 --- a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js @@ -2,11 +2,9 @@ import React, { useCallback, useContext, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { I18nContext } from '../../../contexts/i18n'; -import { Button } from '../../component-library'; -import Box from '../../ui/box'; import { AlignItems, - DISPLAY, + Display, JustifyContent, } from '../../../helpers/constants/design-system'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -15,6 +13,7 @@ import { MetaMetricsEventKeyType, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import { Button, Box } from '../../component-library'; const radius = 14; const strokeWidth = 2; @@ -131,7 +130,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) { {renderPreCompleteContent()} diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js index 2fd23e8ef..2658b3cec 100644 --- a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js @@ -86,7 +86,7 @@ describe('HoldToRevealButton', () => { const button = getByText('Hold to reveal SRP'); - fireEvent.mouseDown(button); + fireEvent.pointerDown(button); const circleLocked = getByLabelText('hold to reveal circle locked'); fireEvent.transitionEnd(circleLocked); diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index d76f7298a..f8ae5650e 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -44,7 +44,6 @@ export const safeComponentList = { SnapDelineator, Copyable, Spinner, - hr: 'hr', SnapUIMarkdown, ///: END:ONLY_INCLUDE_IN }; diff --git a/ui/components/app/modals/export-private-key-modal/export-private-key-modal.component.test.js b/ui/components/app/modals/export-private-key-modal/export-private-key-modal.component.test.js index f4a913280..c435d85ce 100644 --- a/ui/components/app/modals/export-private-key-modal/export-private-key-modal.component.test.js +++ b/ui/components/app/modals/export-private-key-modal/export-private-key-modal.component.test.js @@ -114,7 +114,7 @@ describe('Export Private Key Modal', () => { const holdButton = getByText('Hold to reveal Private Key'); expect(holdButton).toBeInTheDocument(); - fireEvent.mouseDown(holdButton); + fireEvent.pointerDown(holdButton); const circle = getByLabelText('hold to reveal circle locked'); fireEvent.transitionEnd(circle); diff --git a/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.test.js b/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.test.js index 52327246e..8a8ecc71a 100644 --- a/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.test.js +++ b/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.test.js @@ -67,7 +67,7 @@ describe('Hold to Reveal Modal', () => { expect(holdButton).toBeDefined(); - fireEvent.mouseUp(holdButton); + fireEvent.pointerUp(holdButton); expect(holdButton).toBeDefined(); }); @@ -84,7 +84,7 @@ describe('Hold to Reveal Modal', () => { const holdButton = getByText('Hold to reveal SRP'); const circleLocked = queryByLabelText('hold to reveal circle locked'); - fireEvent.mouseDown(holdButton); + fireEvent.pointerDown(holdButton); fireEvent.transitionEnd(circleLocked); const circleUnlocked = queryByLabelText('hold to reveal circle unlocked'); @@ -92,7 +92,7 @@ describe('Hold to Reveal Modal', () => { await waitFor(() => { expect(holdButton.firstChild).toHaveClass( - 'box hold-to-reveal-button__icon-container box--flex-direction-row', + 'hold-to-reveal-button__icon-container', ); expect(onLongPressStub).toHaveBeenCalled(); }); @@ -164,7 +164,7 @@ describe('Hold to Reveal Modal', () => { const holdButton = getByText('Hold to reveal SRP'); const circleLocked = queryByLabelText('hold to reveal circle locked'); - fireEvent.mouseDown(holdButton); + fireEvent.pointerDown(holdButton); fireEvent.transitionEnd(circleLocked); const circleUnlocked = queryByLabelText('hold to reveal circle unlocked'); @@ -172,7 +172,7 @@ describe('Hold to Reveal Modal', () => { await waitFor(() => { expect(holdButton.firstChild).toHaveClass( - 'box hold-to-reveal-button__icon-container box--flex-direction-row', + 'hold-to-reveal-button__icon-container', ); expect(onLongPressStub).toHaveBeenCalled(); expect(hideModalStub).not.toHaveBeenCalled(); diff --git a/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap b/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap index 152b24ab7..6be92d2bd 100644 --- a/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap +++ b/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap @@ -26,11 +26,11 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = `
@@ -46,7 +46,7 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = ` class="box nft-details__nft-item box--flex-direction-row" >
} position="bottom"> + + + )}
); }; PermissionCell.propTypes = { + snapId: PropTypes.string, + permissionName: PropTypes.string.isRequired, title: PropTypes.oneOfType([ PropTypes.string.isRequired, PropTypes.object.isRequired, @@ -125,6 +139,7 @@ PermissionCell.propTypes = { avatarIcon: PropTypes.any.isRequired, dateApproved: PropTypes.number, revoked: PropTypes.bool, + showOptions: PropTypes.bool, }; export default PermissionCell; diff --git a/ui/components/app/permission-cell/permission-cell.test.js b/ui/components/app/permission-cell/permission-cell.test.js index f4567827f..1d7dd2ab5 100644 --- a/ui/components/app/permission-cell/permission-cell.test.js +++ b/ui/components/app/permission-cell/permission-cell.test.js @@ -19,6 +19,7 @@ describe('Permission Cell', () => { it('renders approved permission cell', () => { renderWithProvider( { it('renders revoked permission cell', () => { renderWithProvider( { it('renders requested permission cell', () => { renderWithProvider(
@@ -109,12 +128,9 @@ export default class PermissionPageContainerContent extends PureComponent { iconUrl={subjectMetadata.iconUrl} iconName={subjectMetadata.name} headerTitle={title} - headerText={ - subjectMetadata.extensionId - ? t('allowExternalExtensionTo', [subjectMetadata.extensionId]) - : t('allowThisSiteTo') - } + headerText={headerText} siteOrigin={subjectMetadata.origin} + subjectType={subjectMetadata.subjectType} />
{this.renderRequestedPermissions()} diff --git a/ui/components/app/permission-page-container/permission-page-container.component.js b/ui/components/app/permission-page-container/permission-page-container.component.js index a352d5b36..1f817edf8 100644 --- a/ui/components/app/permission-page-container/permission-page-container.component.js +++ b/ui/components/app/permission-page-container/permission-page-container.component.js @@ -7,6 +7,7 @@ import { WALLET_SNAP_PERMISSION_KEY, } from '@metamask/rpc-methods'; ///: END:ONLY_INCLUDE_IN +import { SubjectType } from '@metamask/permission-controller'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { PageContainerFooter } from '../../ui/page-container'; import PermissionsConnectFooter from '../permissions-connect-footer'; @@ -189,7 +190,7 @@ export default class PermissionPageContainer extends Component { ///: END:ONLY_INCLUDE_IN return ( -
+ <> { ///: BEGIN:ONLY_INCLUDE_IN(snaps) <> @@ -210,7 +211,9 @@ export default class PermissionPageContainer extends Component { allIdentitiesSelected={allIdentitiesSelected} />
- + {targetSubjectMetadata?.subjectType !== SubjectType.Snap && ( + + )} this.onCancel()} @@ -220,7 +223,7 @@ export default class PermissionPageContainer extends Component { buttonSizeLarge={false} />
-
+ ); } } diff --git a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js index 5f3fc5366..725db0fc4 100644 --- a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js +++ b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js @@ -1,6 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classnames from 'classnames'; +///: BEGIN:ONLY_INCLUDE_IN(snaps) +import { SubjectType } from '@metamask/subject-metadata-controller'; +///: END:ONLY_INCLUDE_IN import SiteOrigin from '../../ui/site-origin'; import Box from '../../ui/box'; import { @@ -19,6 +22,7 @@ export default class PermissionsConnectHeader extends Component { headerText: PropTypes.string, leftIcon: PropTypes.node, rightIcon: PropTypes.node, + subjectType: PropTypes.string, }; static defaultProps = { @@ -29,7 +33,23 @@ export default class PermissionsConnectHeader extends Component { }; renderHeaderIcon() { - const { iconUrl, iconName, siteOrigin, leftIcon, rightIcon } = this.props; + const { + iconUrl, + iconName, + siteOrigin, + leftIcon, + rightIcon, + ///: BEGIN:ONLY_INCLUDE_IN(snaps) + subjectType, + ///: END:ONLY_INCLUDE_IN + } = this.props; + + ///: BEGIN:ONLY_INCLUDE_IN(snaps) + + if (subjectType === SubjectType.Snap) { + return null; + } + ///: END:ONLY_INCLUDE_IN return (
diff --git a/ui/components/app/signature-request-original/signature-request-original.component.js b/ui/components/app/signature-request-original/signature-request-original.component.js index 2fea925af..6dc18e4a4 100644 --- a/ui/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/components/app/signature-request-original/signature-request-original.component.js @@ -64,7 +64,10 @@ export default class SignatureRequestOriginal extends Component { resolvePendingApproval: PropTypes.func.isRequired, completedTx: PropTypes.func.isRequired, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + // Used to show a warning if the signing account is not the selected account + // Largely relevant for contract wallet custodians selectedAccount: PropTypes.object, + mmiOnSignCallback: PropTypes.func, ///: END:ONLY_INCLUDE_IN }; @@ -262,7 +265,7 @@ export default class SignatureRequestOriginal extends Component { clearConfirmTransaction, history, mostRecentOverviewPage, - txData: { type, id }, + txData, hardwareWalletRequiresConnection, rejectPendingApproval, resolvePendingApproval, @@ -275,22 +278,34 @@ export default class SignatureRequestOriginal extends Component { submitText={t('sign')} onCancel={async () => { await rejectPendingApproval( - id, + txData.id, serializeError(ethErrors.provider.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); }} onSubmit={async () => { - if (type === MESSAGE_TYPE.ETH_SIGN) { + if (txData.type === MESSAGE_TYPE.ETH_SIGN) { this.setState({ showSignatureRequestWarning: true }); } else { - await resolvePendingApproval(id); + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + if (this.props.mmiOnSignCallback) { + await this.props.mmiOnSignCallback(txData); + return; + } + ///: END:ONLY_INCLUDE_IN + + await resolvePendingApproval(txData.id); clearConfirmTransaction(); history.push(mostRecentOverviewPage); } }} - disabled={hardwareWalletRequiresConnection} + disabled={ + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + Boolean(txData?.custodyId) || + ///: END:ONLY_INCLUDE_IN + hardwareWalletRequiresConnection + } /> ); }; diff --git a/ui/components/app/signature-request-original/signature-request-original.container.js b/ui/components/app/signature-request-original/signature-request-original.container.js index 47bc1ea13..242ace8f8 100644 --- a/ui/components/app/signature-request-original/signature-request-original.container.js +++ b/ui/components/app/signature-request-original/signature-request-original.container.js @@ -9,6 +9,17 @@ import { rejectAllMessages, completedTx, } from '../../../store/actions'; +///: BEGIN:ONLY_INCLUDE_IN(build-mmi) +// eslint-disable-next-line import/order +import { showCustodianDeepLink } from '@metamask-institutional/extension'; +import { + mmiActionsFactory, + setPersonalMessageInProgress, +} from '../../../store/institutional/institution-background'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { checkForUnapprovedMessages } from '../../../store/institutional/institution-actions'; +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app'; +///: END:ONLY_INCLUDE_IN import { accountsWithSendEtherInfoSelector, getSubjectMetadata, @@ -16,6 +27,8 @@ import { unconfirmedMessagesHashSelector, getTotalUnapprovedMessagesCount, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + unapprovedPersonalMsgsSelector, + getAccountType, getSelectedAccount, ///: END:ONLY_INCLUDE_IN } from '../../../selectors'; @@ -30,6 +43,10 @@ function mapStateToProps(state, ownProps) { msgParams: { from }, } = ownProps.txData; + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const envType = getEnvironmentType(); + ///: END:ONLY_INCLUDE_IN + const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(state, from); const isLedgerWallet = isAddressLedger(state, from); @@ -48,12 +65,63 @@ function mapStateToProps(state, ownProps) { messagesList, messagesCount, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + accountType: getAccountType(state), + isNotification: envType === ENVIRONMENT_TYPE_NOTIFICATION, selectedAccount: getSelectedAccount(state), + unapprovedPersonalMessages: unapprovedPersonalMsgsSelector(state), ///: END:ONLY_INCLUDE_IN }; } -function mapDispatchToProps(dispatch) { +let mapDispatchToProps = null; + +///: BEGIN:ONLY_INCLUDE_IN(build-mmi) +function mmiMapDispatchToProps(dispatch) { + const mmiActions = mmiActionsFactory(); + return { + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + setMsgInProgress: (msgId) => dispatch(setPersonalMessageInProgress(msgId)), + showCustodianDeepLink: ({ + custodyId, + fromAddress, + closeNotification, + onDeepLinkFetched, + onDeepLinkShown, + }) => + showCustodianDeepLink({ + dispatch, + mmiActions, + txId: undefined, + fromAddress, + custodyId, + isSignature: true, + closeNotification, + onDeepLinkFetched, + onDeepLinkShown, + }), + showTransactionsFailedModal: ({ + errorMessage, + closeNotification, + operationFailed, + }) => + dispatch( + showModal({ + name: 'TRANSACTION_FAILED', + errorMessage, + closeNotification, + operationFailed, + }), + ), + setWaitForConfirmDeepLinkDialog: (wait) => + dispatch(mmiActions.setWaitForConfirmDeepLinkDialog(wait)), + goHome: () => dispatch(goHome()), + }; +} + +mapDispatchToProps = mmiMapDispatchToProps; +///: END:ONLY_INCLUDE_IN + +mapDispatchToProps = function (dispatch) { return { goHome: () => dispatch(goHome()), clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), @@ -80,12 +148,21 @@ function mapDispatchToProps(dispatch) { dispatch(rejectAllMessages(messagesList)); }, }; -} +}; function mergeProps(stateProps, dispatchProps, ownProps) { const { txData } = ownProps; - const { allAccounts, messagesList, ...otherStateProps } = stateProps; + const { + allAccounts, + messagesList, + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + accountType, + isNotification, + unapprovedPersonalMessages, + ///: END:ONLY_INCLUDE_IN + ...otherStateProps + } = stateProps; const { msgParams: { from }, @@ -95,6 +172,41 @@ function mergeProps(stateProps, dispatchProps, ownProps) { const { cancelAllApprovals: dispatchCancelAllApprovals } = dispatchProps; + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const mmiOnSignCallback = async (_msgData) => { + if (accountType === 'custody') { + try { + let msgData = _msgData; + let id = _msgData.custodyId; + if (!_msgData.custodyId) { + msgData = checkForUnapprovedMessages( + _msgData, + unapprovedPersonalMessages, + ); + id = msgData.custodyId; + } + dispatchProps.showCustodianDeepLink({ + custodyId: id, + fromAddress: fromAccount.address, + closeNotification: isNotification, + onDeepLinkFetched: () => undefined, + onDeepLinkShown: () => undefined, + }); + await dispatchProps.setMsgInProgress(msgData.metamaskId); + await dispatchProps.setWaitForConfirmDeepLinkDialog(true); + await goHome(); + } catch (err) { + await dispatchProps.setWaitForConfirmDeepLinkDialog(true); + await dispatchProps.showTransactionsFailedModal({ + errorMessage: err.message, + closeNotification: true, + operationFailed: true, + }); + } + } + }; + ///: END:ONLY_INCLUDE_IN + return { ...ownProps, ...otherStateProps, @@ -103,6 +215,9 @@ function mergeProps(stateProps, dispatchProps, ownProps) { txData, cancelAllApprovals: () => dispatchCancelAllApprovals(valuesFor(messagesList)), + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + mmiOnSignCallback, + ///: END:ONLY_INCLUDE_IN }; } diff --git a/ui/components/app/signature-request/signature-request.component.js b/ui/components/app/signature-request/signature-request.component.js index 0e4d2e4eb..97062ed60 100644 --- a/ui/components/app/signature-request/signature-request.component.js +++ b/ui/components/app/signature-request/signature-request.component.js @@ -223,14 +223,16 @@ export default class SignatureRequest extends PureComponent { .toString(); const onSign = async () => { - await resolvePendingApproval(id); - completedTx(id); ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) if (this.props.mmiOnSignCallback) { await this.props.mmiOnSignCallback(txData); + return; } ///: END:ONLY_INCLUDE_IN + await resolvePendingApproval(id); + completedTx(id); + trackEvent({ category: MetaMetricsEventCategory.Transactions, event: 'Confirm', diff --git a/ui/components/app/signature-request/signature-request.container.js b/ui/components/app/signature-request/signature-request.container.js index 4ac75ca65..f6f3e8fea 100644 --- a/ui/components/app/signature-request/signature-request.container.js +++ b/ui/components/app/signature-request/signature-request.container.js @@ -30,7 +30,7 @@ import { setTypedMessageInProgress, } from '../../../store/institutional/institution-background'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; -import { checkForUnapprovedTypedMessages } from '../../../store/institutional/institution-actions'; +import { checkForUnapprovedMessages } from '../../../store/institutional/institution-actions'; ///: END:ONLY_INCLUDE_IN import { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) @@ -214,7 +214,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { let msgData = _msgData; let id = _msgData.custodyId; if (!_msgData.custodyId) { - msgData = checkForUnapprovedTypedMessages( + msgData = checkForUnapprovedMessages( _msgData, unapprovedTypedMessages, ); diff --git a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js index 74bf2adab..4f99c2162 100644 --- a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js +++ b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js @@ -24,7 +24,11 @@ import { getTargetSubjectMetadata } from '../../../../selectors'; import SnapAvatar from '../snap-avatar'; import SnapVersion from '../snap-version/snap-version'; -const SnapAuthorshipHeader = ({ snapId, className }) => { +const SnapAuthorshipHeader = ({ + snapId, + className, + boxShadow = 'var(--shadow-size-lg) var(--color-shadow-default)', +}) => { // We're using optional chaining with snapId, because with the current implementation // of snap update in the snap controller, we do not have reference to snapId when an // update request is rejected because the reference comes from the request itself and not subject metadata @@ -51,7 +55,7 @@ const SnapAuthorshipHeader = ({ snapId, className }) => { display={DISPLAY.FLEX} padding={4} style={{ - boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)', + boxShadow, }} > @@ -91,6 +95,7 @@ SnapAuthorshipHeader.propTypes = { * The className of the SnapAuthorship */ className: PropTypes.string, + boxShadow: PropTypes.string, }; export default SnapAuthorshipHeader; diff --git a/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js b/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js index 6dda11d04..27ab6e3d2 100644 --- a/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js +++ b/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js @@ -6,8 +6,10 @@ import PermissionCell from '../../permission-cell'; import Box from '../../../ui/box'; export default function SnapPermissionsList({ + snapId, permissions, targetSubjectMetadata, + showOptions, }) { const t = useI18nContext(); @@ -17,12 +19,15 @@ export default function SnapPermissionsList({ (permission, index) => { return ( ); }, @@ -32,6 +37,8 @@ export default function SnapPermissionsList({ } SnapPermissionsList.propTypes = { + snapId: PropTypes.string.isRequired, permissions: PropTypes.object.isRequired, targetSubjectMetadata: PropTypes.object.isRequired, + showOptions: PropTypes.bool, }; diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss index 31c5110d8..5f7f97902 100644 --- a/ui/components/app/snaps/snap-ui-renderer/index.scss +++ b/ui/components/app/snaps/snap-ui-renderer/index.scss @@ -9,6 +9,7 @@ &__divider { width: 100%; + height: 1px; } &__panel { diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js index d5103e58e..37d83c2c9 100644 --- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js +++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js @@ -10,6 +10,7 @@ import { OverflowWrap, FontWeight, TextVariant, + BorderColor, } from '../../../../helpers/constants/design-system'; import { SnapDelineator } from '../snap-delineator'; import { useI18nContext } from '../../../../hooks/useI18nContext'; @@ -53,9 +54,12 @@ export const UI_MAPPING = { }, }), divider: () => ({ - element: 'hr', + element: 'Box', props: { className: 'snap-ui-renderer__divider', + backgroundColor: BorderColor.borderDefault, + marginTop: 2, + marginBottom: 2, }, }), copyable: (props) => ({ diff --git a/ui/components/app/snaps/snap-version/snap-version.js b/ui/components/app/snaps/snap-version/snap-version.js index a04e9449f..f70653540 100644 --- a/ui/components/app/snaps/snap-version/snap-version.js +++ b/ui/components/app/snaps/snap-version/snap-version.js @@ -18,10 +18,8 @@ import { Text, } from '../../../component-library'; import Preloader from '../../../ui/icon/preloader/preloader-icon.component'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; const SnapVersion = ({ version, url }) => { - const t = useI18nContext(); return (