From 0064020643705a881ddb4904ff2503956e8d9302 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?=
Date: Thu, 7 Nov 2024 10:23:46 +0000
Subject: [PATCH 01/18] chore: adds Solana snap to preinstall list (#28141)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Adds the Solana snap to the preinstall list.
## **Related issues**
Fixes:
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/scripts/snaps/preinstalled-snaps.ts | 6 ++++
package.json | 1 +
shared/lib/accounts/solana-wallet-snap.ts | 9 ++++++
yarn.lock | 38 ++++++++++++++++++++++-
4 files changed, 53 insertions(+), 1 deletion(-)
create mode 100644 shared/lib/accounts/solana-wallet-snap.ts
diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts
index c725a2cbd837..b596468853b2 100644
--- a/app/scripts/snaps/preinstalled-snaps.ts
+++ b/app/scripts/snaps/preinstalled-snaps.ts
@@ -6,6 +6,9 @@ import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap
import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json';
import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/preinstalled-snap.json';
///: END:ONLY_INCLUDE_IF
+///: BEGIN:ONLY_INCLUDE_IF(solana)
+import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json';
+///: END:ONLY_INCLUDE_IF
// The casts here are less than ideal but we expect the SnapController to validate the inputs.
const PREINSTALLED_SNAPS = Object.freeze([
@@ -16,6 +19,9 @@ const PREINSTALLED_SNAPS = Object.freeze([
BitcoinWalletSnap as unknown as PreinstalledSnap,
PreinstalledExampleSnap as unknown as PreinstalledSnap,
///: END:ONLY_INCLUDE_IF
+ ///: BEGIN:ONLY_INCLUDE_IF(solana)
+ SolanaWalletSnap as unknown as PreinstalledSnap,
+ ///: END:ONLY_INCLUDE_IF
]);
export default PREINSTALLED_SNAPS;
diff --git a/package.json b/package.json
index 14b38d504c87..b570cc6f9245 100644
--- a/package.json
+++ b/package.json
@@ -346,6 +346,7 @@
"@metamask/snaps-rpc-methods": "^11.5.1",
"@metamask/snaps-sdk": "^6.10.0",
"@metamask/snaps-utils": "^8.5.1",
+ "@metamask/solana-wallet-snap": "^0.1.9",
"@metamask/transaction-controller": "^38.1.0",
"@metamask/user-operation-controller": "^13.0.0",
"@metamask/utils": "^10.0.1",
diff --git a/shared/lib/accounts/solana-wallet-snap.ts b/shared/lib/accounts/solana-wallet-snap.ts
new file mode 100644
index 000000000000..1f6622c8bbdc
--- /dev/null
+++ b/shared/lib/accounts/solana-wallet-snap.ts
@@ -0,0 +1,9 @@
+import { SnapId } from '@metamask/snaps-sdk';
+// This dependency is still installed as part of the `package.json`, however
+// the Snap is being pre-installed only for Flask build (for the moment).
+import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json';
+
+export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId;
+
+export const SOLANA_WALLET_NAME: string =
+ SolanaWalletSnap.manifest.proposedName;
diff --git a/yarn.lock b/yarn.lock
index 9a54de8897d6..aa975513b90a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5575,6 +5575,23 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/keyring-api@npm:^9.0.0":
+ version: 9.0.0
+ resolution: "@metamask/keyring-api@npm:9.0.0"
+ dependencies:
+ "@metamask/snaps-sdk": "npm:^6.7.0"
+ "@metamask/superstruct": "npm:^3.1.0"
+ "@metamask/utils": "npm:^9.2.1"
+ "@types/uuid": "npm:^9.0.8"
+ bech32: "npm:^2.0.0"
+ uuid: "npm:^9.0.1"
+ webextension-polyfill: "npm:^0.12.0"
+ peerDependencies:
+ "@metamask/providers": ^17.2.0
+ checksum: 10/ff552c04a4d06c7b1a43d52809a9c141d38772586388f0ab96123bce445f148aa7f7e8165d03fa92ac391351de252c4b299fc2c16e690193f669b5329941fe75
+ languageName: node
+ linkType: hard
+
"@metamask/keyring-controller@npm:^17.1.0, @metamask/keyring-controller@npm:^17.2.2":
version: 17.2.2
resolution: "@metamask/keyring-controller@npm:17.2.2"
@@ -6349,6 +6366,17 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/solana-wallet-snap@npm:^0.1.9":
+ version: 0.1.9
+ resolution: "@metamask/solana-wallet-snap@npm:0.1.9"
+ dependencies:
+ "@metamask/keyring-api": "npm:^9.0.0"
+ "@metamask/snaps-sdk": "npm:^6.9.0"
+ buffer: "npm:^6.0.3"
+ checksum: 10/ec540948e1b5c693b0a31a32521d84c5d3796a5d62d1dfa0986cae47483040a0381c30419af4a86b2402efa5e95283b45a5d17bb705c11a181d0c6fa70b5be60
+ languageName: node
+ linkType: hard
+
"@metamask/superstruct@npm:^3.0.0, @metamask/superstruct@npm:^3.1.0":
version: 3.1.0
resolution: "@metamask/superstruct@npm:3.1.0"
@@ -26550,6 +26578,7 @@ __metadata:
"@metamask/snaps-rpc-methods": "npm:^11.5.1"
"@metamask/snaps-sdk": "npm:^6.10.0"
"@metamask/snaps-utils": "npm:^8.5.1"
+ "@metamask/solana-wallet-snap": "npm:^0.1.9"
"@metamask/test-bundler": "npm:^1.0.0"
"@metamask/test-dapp": "npm:8.13.0"
"@metamask/transaction-controller": "npm:^38.1.0"
@@ -37259,7 +37288,14 @@ __metadata:
languageName: node
linkType: hard
-"webextension-polyfill@npm:>=0.10.0 <1.0, webextension-polyfill@npm:^0.10.0":
+"webextension-polyfill@npm:>=0.10.0 <1.0, webextension-polyfill@npm:^0.12.0":
+ version: 0.12.0
+ resolution: "webextension-polyfill@npm:0.12.0"
+ checksum: 10/77e648b958b573ef075e75a0c180e2bbd74dee17b3145e86d21fcbb168c4999e4a311654fe634b8178997bee9b35ea5808d8d3d3e5ff2ad138f197f4f0ea75d9
+ languageName: node
+ linkType: hard
+
+"webextension-polyfill@npm:^0.10.0":
version: 0.10.0
resolution: "webextension-polyfill@npm:0.10.0"
checksum: 10/51ff30ebed4b1aa802b7f0347f05021b2fe492078bb1a597223d43995fcee96e2da8f914a2f6e36f988c1877ed5ab36ca7077f2f3ab828955151a59e4c01bf7e
From bde47a637ecd3172bb9ac6877c815cdc97014596 Mon Sep 17 00:00:00 2001
From: digiwand <20778143+digiwand@users.noreply.github.com>
Date: Thu, 7 Nov 2024 18:01:00 +0700
Subject: [PATCH 02/18] refactor: rename
SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST (#28337)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Rename `SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS` →
`SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST` to be more
explicit now that we are fetching the chain ids list from the Security
Provider API introduced in
https://github.com/MetaMask/metamask-extension/pull/25716
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28337?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2846
Related: https://github.com/MetaMask/metamask-extension/pull/25716
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/scripts/lib/ppom/ppom-util.ts | 4 ++--
shared/constants/security-provider.ts | 2 +-
test/e2e/mock-e2e.js | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts
index 7662c364b651..e8c54ee5eb53 100644
--- a/app/scripts/lib/ppom/ppom-util.ts
+++ b/app/scripts/lib/ppom/ppom-util.ts
@@ -11,7 +11,7 @@ import { SignatureController } from '@metamask/signature-controller';
import {
BlockaidReason,
BlockaidResultType,
- SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS,
+ SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST,
SecurityAlertSource,
} from '../../../../shared/constants/security-provider';
import { SIGNING_METHODS } from '../../../../shared/constants/transaction';
@@ -123,7 +123,7 @@ export function handlePPOMError(
}
export async function isChainSupported(chainId: Hex): Promise {
- let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS;
+ let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST;
try {
if (isSecurityAlertsAPIEnabled()) {
diff --git a/shared/constants/security-provider.ts b/shared/constants/security-provider.ts
index e6fff53ee28a..082f68aa88de 100644
--- a/shared/constants/security-provider.ts
+++ b/shared/constants/security-provider.ts
@@ -89,7 +89,7 @@ export const FALSE_POSITIVE_REPORT_BASE_URL =
export const SECURITY_PROVIDER_UTM_SOURCE = 'metamask-ppom';
-export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS: Hex[] = [
+export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST: Hex[] = [
CHAIN_IDS.ARBITRUM,
CHAIN_IDS.AVALANCHE,
CHAIN_IDS.BASE,
diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js
index 85636fcb9089..5d372f3389a3 100644
--- a/test/e2e/mock-e2e.js
+++ b/test/e2e/mock-e2e.js
@@ -1,7 +1,7 @@
const fs = require('fs');
const {
- SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS,
+ SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST,
} = require('../../shared/constants/security-provider');
const {
BRIDGE_DEV_API_BASE_URL,
@@ -160,7 +160,7 @@ async function setupMocking(
.thenCallback(() => {
return {
statusCode: 200,
- json: SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS,
+ json: SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST,
};
});
From 3401ef01c48343f9d2c6e6a16c36757f20f5e577 Mon Sep 17 00:00:00 2001
From: Pedro Figueiredo
Date: Thu, 7 Nov 2024 12:21:19 +0000
Subject: [PATCH 03/18] fix: Updates to the simulations component (#28107)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Updates the copy, layout and text color for 3 scenarios: No changes,
Generic Error and Fiat value not available as per design review.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28107?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27072
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/_locales/de/messages.json | 9 --
app/_locales/el/messages.json | 9 --
app/_locales/en/messages.json | 13 +--
app/_locales/en_GB/messages.json | 9 --
app/_locales/es/messages.json | 9 --
app/_locales/fr/messages.json | 9 --
app/_locales/hi/messages.json | 9 --
app/_locales/id/messages.json | 9 --
app/_locales/ja/messages.json | 9 --
app/_locales/ko/messages.json | 9 --
app/_locales/pt/messages.json | 9 --
app/_locales/ru/messages.json | 9 --
app/_locales/tl/messages.json | 9 --
app/_locales/tr/messages.json | 9 --
app/_locales/vi/messages.json | 9 --
app/_locales/zh_CN/messages.json | 9 --
.../simulation-details.spec.ts | 4 +-
.../info/personal-sign/personal-sign.tsx | 92 ++++++++++++++++---
.../simulation-details/fiat-display.test.tsx | 3 -
.../simulation-details/fiat-display.tsx | 2 +-
.../simulation-details.test.tsx | 8 +-
.../simulation-details/simulation-details.tsx | 30 ++++--
.../confirm-transaction-base.test.js.snap | 10 +-
23 files changed, 116 insertions(+), 181 deletions(-)
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index 771deef4c28c..19c7731c778f 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Anmelden mit"
},
- "simulationDetailsFailed": {
- "message": "Es ist ein Fehler beim Laden Ihrer Schätzung aufgetreten."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Nicht verfügbar"
},
"simulationDetailsIncomingHeading": {
"message": "Sie erhalten"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Keine Änderungen für Ihre Wallet prognostiziert."
- },
"simulationDetailsOutgoingHeading": {
"message": "Sie senden"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Ressourcen"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Sie melden sich bei einer Website an und es sind keine Änderungen an Ihrem Konto vorgesehen."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json
index 7ae594c8b9b8..ac4542893959 100644
--- a/app/_locales/el/messages.json
+++ b/app/_locales/el/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Σύνδεση με"
},
- "simulationDetailsFailed": {
- "message": "Υπήρξε σφάλμα στη φόρτωση της εκτίμησής σας."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Μη διαθέσιμο"
},
"simulationDetailsIncomingHeading": {
"message": "Λαμβάνετε"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Δεν προβλέπονται αλλαγές για το πορτοφόλι σας"
- },
"simulationDetailsOutgoingHeading": {
"message": "Στέλνετε"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Πόροι"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Συνδέεστε σε έναν ιστότοπο και δεν προβλέπονται αλλαγές στον λογαριασμό σας."
- },
"siweURI": {
"message": "Διεύθυνση URL"
},
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index ab6b06411731..62dda6c29ef0 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -4961,17 +4961,14 @@
"simulationDetailsERC20ApproveDesc": {
"message": "You're giving someone else permission to spend this amount from your account."
},
- "simulationDetailsFailed": {
- "message": "There was an error loading your estimation."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Not Available"
},
"simulationDetailsIncomingHeading": {
"message": "You receive"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "No changes predicted for your wallet"
+ "simulationDetailsNoChanges": {
+ "message": "No changes"
},
"simulationDetailsOutgoingHeading": {
"message": "You send"
@@ -4995,6 +4992,9 @@
"simulationDetailsTransactionReverted": {
"message": "This transaction is likely to fail"
},
+ "simulationDetailsUnavailable": {
+ "message": "Unavailable"
+ },
"simulationErrorMessageV2": {
"message": "We were not able to estimate gas. There might be an error in the contract and this transaction may fail."
},
@@ -5016,9 +5016,6 @@
"siweResources": {
"message": "Resources"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "You’re signing into a site and there are no predicted changes to your account."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json
index e8eb1e58ea71..8dd2e32dac39 100644
--- a/app/_locales/en_GB/messages.json
+++ b/app/_locales/en_GB/messages.json
@@ -4711,18 +4711,12 @@
"signingInWith": {
"message": "Signing in with"
},
- "simulationDetailsFailed": {
- "message": "There was an error loading your estimation."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Not Available"
},
"simulationDetailsIncomingHeading": {
"message": "You receive"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "No changes predicted for your wallet"
- },
"simulationDetailsOutgoingHeading": {
"message": "You send"
},
@@ -4760,9 +4754,6 @@
"siweResources": {
"message": "Resources"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "You’re signing into a site and there are no predicted changes to your account."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index 0f774d8f6ba2..1d23b736fbfd 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -4537,18 +4537,12 @@
"signingInWith": {
"message": "Iniciar sesión con"
},
- "simulationDetailsFailed": {
- "message": "Se produjo un error al cargar su estimación."
- },
"simulationDetailsFiatNotAvailable": {
"message": "No disponible"
},
"simulationDetailsIncomingHeading": {
"message": "Usted recibe"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "No se prevén cambios para su monedero"
- },
"simulationDetailsOutgoingHeading": {
"message": "Envía"
},
@@ -4586,9 +4580,6 @@
"siweResources": {
"message": "Recursos"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Está iniciando sesión en un sitio y no se prevén cambios en su cuenta."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index 985dfd44c9dd..e9b8fd588b89 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Se connecter avec"
},
- "simulationDetailsFailed": {
- "message": "Une erreur s’est produite lors du chargement de l’estimation."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Non disponible"
},
"simulationDetailsIncomingHeading": {
"message": "Vous recevez"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Aucun changement prévu pour votre portefeuille"
- },
"simulationDetailsOutgoingHeading": {
"message": "Vous envoyez"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Ressources"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Vous êtes en train de vous connecter à un site, aucun changement ne devrait être apporté à votre compte."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json
index 540023a75fac..9b2c06f96c11 100644
--- a/app/_locales/hi/messages.json
+++ b/app/_locales/hi/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "के साथ साइन इन करना"
},
- "simulationDetailsFailed": {
- "message": "आपका एस्टीमेशन लोड करने में गड़बड़ी हुई।"
- },
"simulationDetailsFiatNotAvailable": {
"message": "उपलब्ध नहीं है"
},
"simulationDetailsIncomingHeading": {
"message": "आप पाते हैं"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "आपके वॉलेट के लिए किसी बदलाव का प्रेडिक्शन नहीं किया गया है"
- },
"simulationDetailsOutgoingHeading": {
"message": "आप भेजते हैं"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "संसाधन"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "आप किसी साइट पर साइन इन कर रहे हैं और आपके अकाउंट में कोई अनुमानित परिवर्तन नहीं हैं।"
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json
index 81f7d2a9c633..1331d7b9c482 100644
--- a/app/_locales/id/messages.json
+++ b/app/_locales/id/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Masuk dengan"
},
- "simulationDetailsFailed": {
- "message": "Terjadi kesalahan saat memuat estimasi Anda."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Tidak Tersedia"
},
"simulationDetailsIncomingHeading": {
"message": "Anda menerima"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Tidak ada perubahan yang terprediksi untuk dompet Anda"
- },
"simulationDetailsOutgoingHeading": {
"message": "Anda mengirim"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Sumber daya"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Anda masuk ke sebuah situs dan tidak ada perkiraan perubahan pada akun Anda."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 5787bb88c397..3107eb3cb152 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "サインイン方法:"
},
- "simulationDetailsFailed": {
- "message": "予測結果の読み込み中にエラーが発生しました。"
- },
"simulationDetailsFiatNotAvailable": {
"message": "利用できません"
},
"simulationDetailsIncomingHeading": {
"message": "受取額"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "ウォレット残高の増減は予測されていません"
- },
"simulationDetailsOutgoingHeading": {
"message": "送金額"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "リソース"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "サイトにサインインしようとしていて、予想されるアカウントの変更はありません。"
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index 32d7bd4399b5..6dc468cdd5ef 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "다음으로 로그인:"
},
- "simulationDetailsFailed": {
- "message": "추정치를 불러오는 동안 오류가 발생했습니다."
- },
"simulationDetailsFiatNotAvailable": {
"message": "이용할 수 없음"
},
"simulationDetailsIncomingHeading": {
"message": "받음:"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "지갑에 예상 변동 사항 없음"
- },
"simulationDetailsOutgoingHeading": {
"message": "보냄:"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "리소스"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "사이트에 로그인 중이며 계정에 예상되는 변경 사항이 없습니다."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json
index 4eecb941d36f..03f41c1c62b8 100644
--- a/app/_locales/pt/messages.json
+++ b/app/_locales/pt/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Assinando com"
},
- "simulationDetailsFailed": {
- "message": "Houve um erro ao carregar sua estimativa."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Não disponível"
},
"simulationDetailsIncomingHeading": {
"message": "Você recebe"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Nenhuma alteração prevista para sua carteira"
- },
"simulationDetailsOutgoingHeading": {
"message": "Você envia"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Recursos"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Você está fazendo login em um site e não há alterações previstas em sua conta."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index ce53cc239de5..e56afc418785 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Вход с помощью"
},
- "simulationDetailsFailed": {
- "message": "Не удалось загрузить прогноз."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Недоступно"
},
"simulationDetailsIncomingHeading": {
"message": "Вы получаете"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Никаких изменений в вашем кошельке не прогнозируется"
- },
"simulationDetailsOutgoingHeading": {
"message": "Вы отправляете"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Ресурсы"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Вы входите на сайт, и в вашем счете не происходит никаких прогнозируемых изменений."
- },
"siweURI": {
"message": "URL-адрес"
},
diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json
index 8909ac662e34..af56d958c313 100644
--- a/app/_locales/tl/messages.json
+++ b/app/_locales/tl/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Nagsa-sign in gamit ang"
},
- "simulationDetailsFailed": {
- "message": "Mayroong error sa pag-load ng iyong pagtataya."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Hindi Available"
},
"simulationDetailsIncomingHeading": {
"message": "Natanggap mo"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Walang pagbabagong nahulaan para sa iyong wallet"
- },
"simulationDetailsOutgoingHeading": {
"message": "Nagpadala ka"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Mga Mapagkukunan"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Ikaw ay nagsa-sign in sa isang site at walang mga inaasahang pagbabago sa iyong account."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index 8c4e44d3192e..6732513395d8 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Şununla giriş yap:"
},
- "simulationDetailsFailed": {
- "message": "Tahmininiz yüklenirken bir hata oldu."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Mevcut Değil"
},
"simulationDetailsIncomingHeading": {
"message": "Aldığınız"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Cüzdanınız için değişiklik öngörülmüyor"
- },
"simulationDetailsOutgoingHeading": {
"message": "Gönderdiğiniz"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Kaynaklar"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Bir siteye giriş yapıyorsunuz ve hesabınızda öngörülen herhangi bir değişiklik yok."
- },
"siweURI": {
"message": "URL adresi"
},
diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json
index b741fe6eb536..9f6ff5119366 100644
--- a/app/_locales/vi/messages.json
+++ b/app/_locales/vi/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "Đăng nhập bằng"
},
- "simulationDetailsFailed": {
- "message": "Đã xảy ra lỗi khi tải kết quả ước tính."
- },
"simulationDetailsFiatNotAvailable": {
"message": "Không có sẵn"
},
"simulationDetailsIncomingHeading": {
"message": "Bạn nhận được"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "Ví của bạn dự kiến không có thay đổi nào"
- },
"simulationDetailsOutgoingHeading": {
"message": "Bạn gửi"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "Tài nguyên"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "Bạn đang đăng nhập vào một trang web và dự kiến không có thay đổi nào đối với tài khoản của bạn."
- },
"siweURI": {
"message": "URL"
},
diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json
index e5695cdfaecf..eef758dacdad 100644
--- a/app/_locales/zh_CN/messages.json
+++ b/app/_locales/zh_CN/messages.json
@@ -4540,18 +4540,12 @@
"signingInWith": {
"message": "使用以下登录方式"
},
- "simulationDetailsFailed": {
- "message": "加载估算时出错。"
- },
"simulationDetailsFiatNotAvailable": {
"message": "不可用"
},
"simulationDetailsIncomingHeading": {
"message": "您收到"
},
- "simulationDetailsNoBalanceChanges": {
- "message": "预计您的钱包不会发生变化"
- },
"simulationDetailsOutgoingHeading": {
"message": "您发送"
},
@@ -4589,9 +4583,6 @@
"siweResources": {
"message": "资源"
},
- "siweSignatureSimulationDetailInfo": {
- "message": "您正在登录某个网站,并且您的账户没有预期变化。"
- },
"siweURI": {
"message": "URL"
},
diff --git a/test/e2e/tests/simulation-details/simulation-details.spec.ts b/test/e2e/tests/simulation-details/simulation-details.spec.ts
index 46f4cdb5860a..bbac41b5a1a1 100644
--- a/test/e2e/tests/simulation-details/simulation-details.spec.ts
+++ b/test/e2e/tests/simulation-details/simulation-details.spec.ts
@@ -215,7 +215,7 @@ describe('Simulation Details', () => {
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
await driver.findElement({
css: '[data-testid="simulation-details-layout"]',
- text: 'No changes predicted for your wallet',
+ text: 'No changes',
});
},
);
@@ -276,7 +276,7 @@ describe('Simulation Details', () => {
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
await driver.findElement({
css: '[data-testid="simulation-details-layout"]',
- text: 'There was an error loading your estimation',
+ text: 'Unavailable',
});
},
);
diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx
index 5eb798439ca8..5c0af68742c5 100644
--- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx
+++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx
@@ -2,24 +2,42 @@ import React from 'react';
import { useSelector } from 'react-redux';
import {
- ConfirmInfoRow,
ConfirmInfoRowAddress,
ConfirmInfoRowText,
ConfirmInfoRowUrl,
} from '../../../../../../components/app/confirm/info/row';
+import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row';
import { RowAlertKey } from '../../../../../../components/app/confirm/info/row/constants';
-import { useI18nContext } from '../../../../../../hooks/useI18nContext';
-import { useConfirmContext } from '../../../../context/confirm';
+import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section';
+import {
+ Box,
+ Icon,
+ IconName,
+ IconSize,
+ Text,
+} from '../../../../../../components/component-library';
+import Tooltip from '../../../../../../components/ui/tooltip';
+import {
+ AlignItems,
+ BorderColor,
+ BorderRadius,
+ Display,
+ FlexDirection,
+ IconColor,
+ JustifyContent,
+ TextColor,
+ TextVariant,
+} from '../../../../../../helpers/constants/design-system';
+import { isSnapId } from '../../../../../../helpers/utils/snaps';
import {
hexToText,
sanitizeString,
} from '../../../../../../helpers/utils/util';
-import { SignatureRequestType } from '../../../../types/confirm';
+import { useI18nContext } from '../../../../../../hooks/useI18nContext';
+import { useConfirmContext } from '../../../../context/confirm';
import { selectUseTransactionSimulations } from '../../../../selectors/preferences';
+import { SignatureRequestType } from '../../../../types/confirm';
import { isSIWESignatureRequest } from '../../../../utils';
-import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row';
-import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section';
-import { isSnapId } from '../../../../../../helpers/utils/snaps';
import { SIWESignInfo } from './siwe-sign';
const PersonalSignInfo: React.FC = () => {
@@ -49,16 +67,66 @@ const PersonalSignInfo: React.FC = () => {
}
}
+ const SimulationDetailsKey = (
+
+
+ {t('simulationDetailsTitle')}
+
+
+
+
+
+ );
+
+ const SimulationDetailsValue = (
+
+ {t('simulationDetailsNoChanges')}
+
+ );
+
return (
<>
{isSIWE && useTransactionSimulations && (
-
-
-
+
+ {SimulationDetailsKey}
+ {SimulationDetailsValue}
+
+
)}
diff --git a/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx b/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx
index 922a2e414916..7a91ba129d6b 100644
--- a/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx
+++ b/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx
@@ -58,7 +58,6 @@ describe('FiatDisplay', () => {
describe('IndividualFiatDisplay', () => {
// @ts-expect-error This is missing from the Mocha type definitions
it.each([
- [FIAT_UNAVAILABLE, 'Not Available'],
[100, '$100'],
[-100, '$100'],
])(
@@ -84,8 +83,6 @@ describe('FiatDisplay', () => {
describe('TotalFiatDisplay', () => {
// @ts-expect-error This is missing from the Mocha type definitions
it.each([
- [[FIAT_UNAVAILABLE, FIAT_UNAVAILABLE], 'Not Available'],
- [[], 'Not Available'],
[[100, 200, FIAT_UNAVAILABLE, 300], 'Total = $600'],
[[-100, -200, FIAT_UNAVAILABLE, -300], 'Total = $600'],
])(
diff --git a/ui/pages/confirmations/components/simulation-details/fiat-display.tsx b/ui/pages/confirmations/components/simulation-details/fiat-display.tsx
index ac3fcebcc159..d7ec958f2a5b 100644
--- a/ui/pages/confirmations/components/simulation-details/fiat-display.tsx
+++ b/ui/pages/confirmations/components/simulation-details/fiat-display.tsx
@@ -50,7 +50,7 @@ export const IndividualFiatDisplay: React.FC<{
}
if (fiatAmount === FIAT_UNAVAILABLE) {
- return ;
+ return null;
}
const absFiat = Math.abs(fiatAmount);
const fiatDisplayValue = fiatFormatter(absFiat, { shorten });
diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx
index 339ffbdd6a1a..9294f7d1b5b5 100644
--- a/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx
+++ b/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx
@@ -101,17 +101,13 @@ describe('SimulationDetails', () => {
renderSimulationDetails({
error: { message: 'Unknown error' },
});
- expect(
- screen.getByText(/error loading your estimation/u),
- ).toBeInTheDocument();
+ expect(screen.getByText(/Unavailable/u)).toBeInTheDocument();
});
it('renders empty content when there are no balance changes', () => {
renderSimulationDetails({});
- expect(
- screen.getByText(/No changes predicted for your wallet/u),
- ).toBeInTheDocument();
+ expect(screen.getByText(/No changes/u)).toBeInTheDocument();
});
it('passes the correct properties to BalanceChangeList components', () => {
diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.tsx
index 382fcb180f82..57ee1f2ff3fb 100644
--- a/ui/pages/confirmations/components/simulation-details/simulation-details.tsx
+++ b/ui/pages/confirmations/components/simulation-details/simulation-details.tsx
@@ -62,17 +62,23 @@ const ErrorContent: React.FC<{ error: SimulationError }> = ({ error }) => {
function getMessage() {
return error.code === SimulationErrorCode.Reverted
? t('simulationDetailsTransactionReverted')
- : t('simulationDetailsFailed');
+ : t('simulationDetailsUnavailable');
}
return (
-
+ {error.code === SimulationErrorCode.Reverted && (
+
+ )}
{getMessage()}
);
@@ -84,8 +90,8 @@ const ErrorContent: React.FC<{ error: SimulationError }> = ({ error }) => {
const EmptyContent: React.FC = () => {
const t = useI18nContext();
return (
-
- {t('simulationDetailsNoBalanceChanges')}
+
+ {t('simulationDetailsNoChanges')}
);
};
@@ -260,12 +266,19 @@ export const SimulationDetails: React.FC = ({
}
if (error) {
+ const inHeaderProp = error.code !== SimulationErrorCode.Reverted && {
+ inHeader: ,
+ };
+
return (
-
+ {error.code === SimulationErrorCode.Reverted && (
+
+ )}
);
}
@@ -277,9 +290,8 @@ export const SimulationDetails: React.FC = ({
-
-
+ inHeader={ }
+ />
);
}
diff --git a/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap b/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap
index 3bd2313850a4..5cdbdd5de6a5 100644
--- a/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap
+++ b/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap
@@ -337,12 +337,12 @@ exports[`Confirm Transaction Base should match snapshot 1`] = `
+
+ No changes
+
-
- No changes predicted for your wallet
-
Date: Thu, 7 Nov 2024 14:40:43 +0000
Subject: [PATCH 04/18] feat: Convert mmi controller to a non-controller
(#27983)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
We want to bring MMIController up to date with our latest controller
patterns.
After review, it turns out that MMIController does not have any state
and therefore should not inherit from BaseController (or anything, for
that matter).
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27983?quickstart=1)
## **Related issues**
Fixes: #25926
## **Manual testing steps**
N/A
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../controllers/mmi-controller.test.ts | 91 ++++++++++------
app/scripts/controllers/mmi-controller.ts | 100 ++++++++++--------
app/scripts/metamask-controller.js | 6 +-
shared/constants/mmi-controller.ts | 56 ++++++++--
4 files changed, 165 insertions(+), 88 deletions(-)
diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts
index 64bc46132724..479c4cb0f14d 100644
--- a/app/scripts/controllers/mmi-controller.test.ts
+++ b/app/scripts/controllers/mmi-controller.test.ts
@@ -17,13 +17,14 @@ import {
NETWORK_TYPES,
TEST_NETWORK_TICKER_MAP,
} from '../../../shared/constants/network';
-import MMIController from './mmi-controller';
+import { MMIController, AllowedActions } from './mmi-controller';
import { AppStateController } from './app-state-controller';
import { ControllerMessenger } from '@metamask/base-controller';
import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory';
import MetaMetricsController from './metametrics';
import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods';
import { mockNetworkState } from '../../../test/stub/networks';
+import { InfuraNetworkType } from '@metamask/controller-utils';
import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk';
jest.mock('@metamask-institutional/portfolio-dashboard', () => ({
@@ -39,6 +40,21 @@ jest.mock('./permissions', () => ({
}),
}));
+export const createMockNetworkConfiguration = (
+ override?: Partial
,
+): NetworkConfiguration => {
+ return {
+ chainId: CHAIN_IDS.SEPOLIA,
+ blockExplorerUrls: [],
+ defaultRpcEndpointIndex: 0,
+ name: 'Mock Network',
+ nativeCurrency: 'MOCK TOKEN',
+ rpcEndpoints: [],
+ defaultBlockExplorerUrlIndex: 0,
+ ...override,
+ };
+};
+
const mockAccount = {
address: '0x758b8178a9A4B7206d1f648c4a77C515Cbac7001',
id: 'mock-id',
@@ -73,10 +89,10 @@ describe('MMIController', function () {
mmiConfigurationController,
controllerMessenger,
accountsController,
- networkController,
keyringController,
metaMetricsController,
- custodyController;
+ custodyController,
+ mmiControllerMessenger;
beforeEach(async function () {
const mockMessenger = {
@@ -89,22 +105,10 @@ describe('MMIController', function () {
subscribe: jest.fn(),
};
- networkController = new NetworkController({
- messenger: new ControllerMessenger().getRestricted({
- name: 'NetworkController',
- allowedEvents: [
- 'NetworkController:stateChange',
- 'NetworkController:networkWillChange',
- 'NetworkController:networkDidChange',
- 'NetworkController:infuraIsBlocked',
- 'NetworkController:infuraIsUnblocked',
- ],
- }),
- state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }),
- infuraProjectId: 'mock-infura-project-id',
- });
-
- controllerMessenger = new ControllerMessenger();
+ const controllerMessenger = new ControllerMessenger<
+ AllowedActions,
+ never
+ >();
accountsController = new AccountsController({
messenger: controllerMessenger.getRestricted({
@@ -212,7 +216,31 @@ describe('MMIController', function () {
onNetworkDidChange: jest.fn(),
});
- const mmiControllerMessenger = controllerMessenger.getRestricted({
+ controllerMessenger.registerActionHandler(
+ 'NetworkController:getState',
+ jest.fn().mockReturnValue(mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA })),
+ );
+
+ controllerMessenger.registerActionHandler(
+ 'NetworkController:setActiveNetwork',
+ InfuraNetworkType['sepolia'],
+ );
+
+ controllerMessenger.registerActionHandler(
+ 'NetworkController:getNetworkClientById',
+ jest.fn().mockReturnValue({
+ configuration: {
+ chainId: CHAIN_IDS.SEPOLIA,
+ }
+ }),
+ );
+
+ controllerMessenger.registerActionHandler(
+ 'NetworkController:getNetworkConfigurationByChainId',
+ jest.fn().mockReturnValue(createMockNetworkConfiguration()),
+ );
+
+ mmiControllerMessenger = controllerMessenger.getRestricted({
name: 'MMIController',
allowedActions: [
'AccountsController:getAccountByAddress',
@@ -220,6 +248,10 @@ describe('MMIController', function () {
'AccountsController:listAccounts',
'AccountsController:getSelectedAccount',
'AccountsController:setSelectedAccount',
+ 'NetworkController:getState',
+ 'NetworkController:setActiveNetwork',
+ 'NetworkController:getNetworkClientById',
+ 'NetworkController:getNetworkConfigurationByChainId'
],
});
@@ -253,7 +285,6 @@ describe('MMIController', function () {
})
}
}),
- networkController,
permissionController,
custodyController,
metaMetricsController,
@@ -507,9 +538,7 @@ describe('MMIController', function () {
CUSTODIAN_TYPES['CUSTODIAN-TYPE'] = {
keyringClass: { type: 'mock-keyring-class' },
};
- mmiController.messenger.call = jest
- .fn()
- .mockReturnValue({ address: '0x1' });
+ jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue({ address: '0x1' });
mmiController.custodyController.getCustodyTypeByAddress = jest
.fn()
.mockReturnValue('custodian-type');
@@ -628,9 +657,7 @@ describe('MMIController', function () {
mmiController.custodyController.getAccountDetails = jest
.fn()
.mockReturnValue({});
- mmiController.messenger.call = jest
- .fn()
- .mockReturnValue([mockAccount, mockAccount2]);
+ jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue([mockAccount, mockAccount2]);
mmiController.mmiConfigurationController.store.getState = jest
.fn()
.mockReturnValue({
@@ -701,7 +728,7 @@ describe('MMIController', function () {
describe('handleMmiCheckIfTokenIsPresent', () => {
it('should check if a token is present', async () => {
- mmiController.messenger.call = jest
+ mmiController.messagingSystem.call = jest
.fn()
.mockReturnValue({ address: '0x1' });
mmiController.custodyController.getCustodyTypeByAddress = jest
@@ -733,7 +760,7 @@ describe('MMIController', function () {
describe('handleMmiDashboardData', () => {
it('should return internalAccounts as identities', async () => {
- const controllerMessengerSpy = jest.spyOn(controllerMessenger, 'call');
+ const controllerMessengerSpy = jest.spyOn(mmiControllerMessenger, 'call');
await mmiController.handleMmiDashboardData();
expect(controllerMessengerSpy).toHaveBeenCalledWith(
@@ -811,7 +838,7 @@ describe('MMIController', function () {
describe('setAccountAndNetwork', () => {
it('should set a new selected account if the selectedAddress and the address from the arguments is different', async () => {
- const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call');
+ const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call');
await mmiController.setAccountAndNetwork(
'mock-origin',
mockAccount2.address,
@@ -829,14 +856,14 @@ describe('MMIController', function () {
});
it('should not set a new selected account the accounts are the same', async () => {
- const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call');
+ const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call');
await mmiController.setAccountAndNetwork(
'mock-origin',
mockAccount.address,
'0x1',
);
- expect(selectedAccountSpy).toHaveBeenCalledTimes(1);
+ expect(selectedAccountSpy).toHaveBeenCalledTimes(4);
const selectedAccount = accountsController.getSelectedAccount();
expect(selectedAccount.id).toBe(mockAccount.id);
});
diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts
index 65cdac69ba0b..ac8de8b76656 100644
--- a/app/scripts/controllers/mmi-controller.ts
+++ b/app/scripts/controllers/mmi-controller.ts
@@ -1,4 +1,3 @@
-import EventEmitter from 'events';
import log from 'loglevel';
import { captureException } from '@sentry/browser';
import {
@@ -21,13 +20,13 @@ import { IApiCallLogEntry } from '@metamask-institutional/types';
import { TransactionUpdateController } from '@metamask-institutional/transaction-update';
import { TransactionMeta } from '@metamask/transaction-controller';
import { KeyringTypes } from '@metamask/keyring-controller';
+import { NetworkState } from '@metamask/network-controller';
import {
MessageParamsPersonal,
MessageParamsTyped,
SignatureController,
} from '@metamask/signature-controller';
import { OriginalRequest } from '@metamask/message-manager';
-import { NetworkController } from '@metamask/network-controller';
import { InternalAccount } from '@metamask/keyring-api';
import { toHex } from '@metamask/controller-utils';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
@@ -41,15 +40,12 @@ import {
Label,
Signature,
ConnectionRequest,
+ MMIControllerMessenger,
} from '../../../shared/constants/mmi-controller';
-// TODO: Remove restricted import
-// eslint-disable-next-line import/no-restricted-paths
-import { getCurrentChainId } from '../../../ui/selectors';
import MetaMetricsController from './metametrics';
import { getPermissionBackgroundApiMethods } from './permissions';
import AccountTrackerController from './account-tracker-controller';
import { AppStateController } from './app-state-controller';
-import { PreferencesController } from './preferences-controller';
type UpdateCustodianTransactionsParameters = {
keyring: CustodyKeyring;
@@ -64,7 +60,7 @@ type UpdateCustodianTransactionsParameters = {
setTxHash: (txId: string, txHash: string) => void;
};
-export default class MMIController extends EventEmitter {
+export class MMIController {
public opts: MMIControllerOptions;
public mmiConfigurationController: MmiConfigurationController;
@@ -73,8 +69,6 @@ export default class MMIController extends EventEmitter {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public keyringController: any;
- public preferencesController: PreferencesController;
-
public appStateController: AppStateController;
public transactionUpdateController: TransactionUpdateController;
@@ -93,7 +87,7 @@ export default class MMIController extends EventEmitter {
private metaMetricsController: MetaMetricsController;
- private networkController: NetworkController;
+ #networkControllerState: NetworkState;
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -101,9 +95,7 @@ export default class MMIController extends EventEmitter {
private signatureController: SignatureController;
- // TODO: Replace `any` with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private messenger: any;
+ private messagingSystem: MMIControllerMessenger;
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -139,13 +131,10 @@ export default class MMIController extends EventEmitter {
};
constructor(opts: MMIControllerOptions) {
- super();
-
this.opts = opts;
- this.messenger = opts.messenger;
+ this.messagingSystem = opts.messenger;
this.mmiConfigurationController = opts.mmiConfigurationController;
this.keyringController = opts.keyringController;
- this.preferencesController = opts.preferencesController;
this.appStateController = opts.appStateController;
this.transactionUpdateController = opts.transactionUpdateController;
this.custodyController = opts.custodyController;
@@ -153,7 +142,6 @@ export default class MMIController extends EventEmitter {
this.getPendingNonce = opts.getPendingNonce;
this.accountTrackerController = opts.accountTrackerController;
this.metaMetricsController = opts.metaMetricsController;
- this.networkController = opts.networkController;
this.permissionController = opts.permissionController;
this.signatureController = opts.signatureController;
this.platform = opts.platform;
@@ -214,6 +202,10 @@ export default class MMIController extends EventEmitter {
this.setConnectionRequest(payload);
},
);
+
+ this.#networkControllerState = this.messagingSystem.call(
+ 'NetworkController:getState',
+ );
} // End of constructor
async persistKeyringsAfterRefreshTokenChange() {
@@ -402,7 +394,10 @@ export default class MMIController extends EventEmitter {
// Check if any address is already added
if (
newAccounts.some((address) =>
- this.messenger.call('AccountsController:getAccountByAddress', address),
+ this.messagingSystem.call(
+ 'AccountsController:getAccountByAddress',
+ address,
+ ),
)
) {
throw new Error('Cannot import duplicate accounts');
@@ -502,15 +497,17 @@ export default class MMIController extends EventEmitter {
// If the label is defined
if (label) {
// Set the label for the address
- const account = this.messenger.call(
+ const account = this.messagingSystem.call(
'AccountsController:getAccountByAddress',
address,
);
- this.messenger.call(
- 'AccountsController:setAccountName',
- account.id,
- label,
- );
+ if (account) {
+ this.messagingSystem.call(
+ 'AccountsController:setAccountName',
+ account.id,
+ label,
+ );
+ }
}
}
});
@@ -552,7 +549,7 @@ export default class MMIController extends EventEmitter {
) {
let currentCustodyType: string = '';
if (!custodianType) {
- const { address } = this.messenger.call(
+ const { address } = this.messagingSystem.call(
'AccountsController:getSelectedAccount',
);
currentCustodyType = this.custodyController.getCustodyTypeByAddress(
@@ -637,7 +634,7 @@ export default class MMIController extends EventEmitter {
// Based on a custodian name, get all the tokens associated with that custodian
async getCustodianJWTList(custodianEnvName: string) {
- const internalAccounts = this.messenger.call(
+ const internalAccounts = this.messagingSystem.call(
'AccountsController:listAccounts',
);
@@ -736,7 +733,8 @@ export default class MMIController extends EventEmitter {
const currentAddress =
address ||
- this.messenger.call('AccountsController:getSelectedAccount').address;
+ this.messagingSystem.call('AccountsController:getSelectedAccount')
+ .address;
const currentCustodyType = this.custodyController.getCustodyTypeByAddress(
toChecksumHexAddress(currentAddress),
);
@@ -761,7 +759,7 @@ export default class MMIController extends EventEmitter {
// TEMP: Convert internal accounts to match identities format
// TODO: Convert handleMmiPortfolio to use internal accounts
- const internalAccounts = this.messenger
+ const internalAccounts = this.messagingSystem
.call('AccountsController:listAccounts')
.map((internalAccount: InternalAccount) => {
return {
@@ -774,9 +772,10 @@ export default class MMIController extends EventEmitter {
this.custodyController.getAccountDetails(address);
const extensionId = this.extension.runtime.id;
- const networks = Object.values(
- this.networkController.state.networkConfigurationsByChainId,
+ const { networkConfigurationsByChainId } = this.messagingSystem.call(
+ 'NetworkController:getState',
);
+ const networks = Object.values(networkConfigurationsByChainId);
return handleMmiPortfolio({
keyringAccounts,
@@ -848,30 +847,38 @@ export default class MMIController extends EventEmitter {
async setAccountAndNetwork(origin: string, address: string, chainId: number) {
await this.appStateController.getUnlockPromise(true);
const addressToLowerCase = address.toLowerCase();
- const { address: selectedAddress } = this.messenger.call(
+ const { address: selectedAddress } = this.messagingSystem.call(
'AccountsController:getSelectedAccount',
);
if (selectedAddress.toLowerCase() !== addressToLowerCase) {
- const internalAccount = this.messenger.call(
+ const internalAccount = this.messagingSystem.call(
'AccountsController:getAccountByAddress',
addressToLowerCase,
);
- this.messenger.call(
- 'AccountsController:setSelectedAccount',
- internalAccount.id,
- );
+ if (internalAccount) {
+ this.messagingSystem.call(
+ 'AccountsController:setSelectedAccount',
+ internalAccount.id,
+ );
+ }
}
- const selectedChainId = getCurrentChainId({
- metamask: this.networkController.state,
- });
+ const { selectedNetworkClientId } = this.messagingSystem.call(
+ 'NetworkController:getState',
+ );
+ const {
+ configuration: { chainId: selectedChainId },
+ } = this.messagingSystem.call(
+ 'NetworkController:getNetworkClientById',
+ selectedNetworkClientId,
+ );
if (selectedChainId !== toHex(chainId)) {
- const networkConfiguration =
- this.networkController.state.networkConfigurationsByChainId[
- toHex(chainId)
- ];
+ const networkConfiguration = this.messagingSystem.call(
+ 'NetworkController:getNetworkConfigurationByChainId',
+ toHex(chainId),
+ );
const { networkClientId } =
networkConfiguration?.rpcEndpoints?.[
@@ -879,7 +886,10 @@ export default class MMIController extends EventEmitter {
] ?? {};
if (networkClientId) {
- await this.networkController.setActiveNetwork(networkClientId);
+ await this.messagingSystem.call(
+ 'NetworkController:setActiveNetwork',
+ networkClientId,
+ );
}
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 89d91f134ae2..b11ffac6d2a8 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -282,7 +282,7 @@ import {
checkForMultipleVersionsRunning,
} from './detect-multiple-instances';
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
-import MMIController from './controllers/mmi-controller';
+import { MMIController } from './controllers/mmi-controller';
import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory';
///: END:ONLY_INCLUDE_IF
import ComposableObservableStore from './lib/ComposableObservableStore';
@@ -2026,6 +2026,8 @@ export default class MetamaskController extends EventEmitter {
'AccountsController:listAccounts',
'AccountsController:getSelectedAccount',
'AccountsController:setSelectedAccount',
+ 'NetworkController:getState',
+ 'NetworkController:setActiveNetwork',
],
});
@@ -2033,7 +2035,6 @@ export default class MetamaskController extends EventEmitter {
messenger: mmiControllerMessenger,
mmiConfigurationController: this.mmiConfigurationController,
keyringController: this.keyringController,
- preferencesController: this.preferencesController,
appStateController: this.appStateController,
transactionUpdateController: this.transactionUpdateController,
custodyController: this.custodyController,
@@ -2041,7 +2042,6 @@ export default class MetamaskController extends EventEmitter {
getPendingNonce: this.getPendingNonce.bind(this),
accountTrackerController: this.accountTrackerController,
metaMetricsController: this.metaMetricsController,
- networkController: this.networkController,
permissionController: this.permissionController,
signatureController: this.signatureController,
platform: this.platform,
diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts
index 67be9f72cee6..83998fe6e7b9 100644
--- a/shared/constants/mmi-controller.ts
+++ b/shared/constants/mmi-controller.ts
@@ -3,10 +3,20 @@ import { TransactionMeta } from '@metamask/transaction-controller';
import { TransactionUpdateController } from '@metamask-institutional/transaction-update';
import { CustodyController } from '@metamask-institutional/custody-controller';
import { SignatureController } from '@metamask/signature-controller';
-import { NetworkController } from '@metamask/network-controller';
-// TODO: Remove restricted import
-// eslint-disable-next-line import/no-restricted-paths
-import { PreferencesController } from '../../app/scripts/controllers/preferences-controller';
+import {
+ NetworkController,
+ NetworkControllerGetNetworkClientByIdAction,
+ NetworkControllerGetStateAction,
+ NetworkControllerSetActiveNetworkAction,
+} from '@metamask/network-controller';
+import {
+ AccountsControllerGetAccountByAddressAction,
+ AccountsControllerSetAccountNameAction,
+ AccountsControllerListAccountsAction,
+ AccountsControllerGetSelectedAccountAction,
+ AccountsControllerSetSelectedAccountAction,
+} from '@metamask/accounts-controller';
+import { RestrictedControllerMessenger } from '@metamask/base-controller';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { AppStateController } from '../../app/scripts/controllers/app-state-controller';
@@ -17,18 +27,48 @@ import AccountTrackerController from '../../app/scripts/controllers/account-trac
// eslint-disable-next-line import/no-restricted-paths
import MetaMetricsController from '../../app/scripts/controllers/metametrics';
+// Unique name for the controller
+const controllerName = 'MMIController';
+
+type NetworkControllerGetNetworkConfigurationByChainId = {
+ type: `NetworkController:getNetworkConfigurationByChainId`;
+ handler: NetworkController['getNetworkConfigurationByChainId'];
+};
+
+/**
+ * Actions that this controller is allowed to call.
+ */
+export type AllowedActions =
+ | AccountsControllerGetAccountByAddressAction
+ | AccountsControllerSetAccountNameAction
+ | AccountsControllerListAccountsAction
+ | AccountsControllerGetSelectedAccountAction
+ | AccountsControllerSetSelectedAccountAction
+ | NetworkControllerGetStateAction
+ | NetworkControllerSetActiveNetworkAction
+ | NetworkControllerGetNetworkClientByIdAction
+ | NetworkControllerGetNetworkConfigurationByChainId;
+
+/**
+ * Messenger type for the {@link MMIController}.
+ */
+export type MMIControllerMessenger = RestrictedControllerMessenger<
+ typeof controllerName,
+ AllowedActions,
+ never,
+ AllowedActions['type'],
+ never
+>;
+
export type MMIControllerOptions = {
mmiConfigurationController: MmiConfigurationController;
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
keyringController: any;
- preferencesController: PreferencesController;
appStateController: AppStateController;
transactionUpdateController: TransactionUpdateController;
custodyController: CustodyController;
- // TODO: Replace `any` with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- messenger: any;
+ messenger: MMIControllerMessenger;
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getState: () => any;
From afa736569ce1bfbd5292dc28b9694ae13c7d8941 Mon Sep 17 00:00:00 2001
From: Mathieu Artu
Date: Thu, 7 Nov 2024 17:06:24 +0100
Subject: [PATCH 05/18] fix: disable account syncing (#28359)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR disables account syncing.
The QA team found a critical bug that prevents us from being confident
enough to release this feature now. We'll continue investigating and
we'll re-enable when we're 100% confident about this.
## **Description**
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28359?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
1. No testing steps. Account syncing is disabled
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/scripts/metamask-controller.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index b11ffac6d2a8..caac30573406 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -156,6 +156,7 @@ import {
NotificationServicesPushController,
NotificationServicesController,
} from '@metamask/notification-services-controller';
+import { isProduction } from '../../shared/modules/environment';
import {
methodsRequiringNetworkSwitch,
methodsThatCanSwitchNetworkWithoutApproval,
@@ -1572,7 +1573,7 @@ export default class MetamaskController extends EventEmitter {
},
},
env: {
- isAccountSyncingEnabled: isManifestV3,
+ isAccountSyncingEnabled: !isProduction() && isManifestV3,
},
messenger: this.controllerMessenger.getRestricted({
name: 'UserStorageController',
From 1614632ab7bd8a73ed7e6e9c1e44ef26e00583b1 Mon Sep 17 00:00:00 2001
From: David Walsh
Date: Thu, 7 Nov 2024 11:01:21 -0600
Subject: [PATCH 06/18] fix: Revert "fix: Negate privacy mode in Send screen"
(#28360)
Reverts MetaMask/metamask-extension#28248
Need to revert this before
https://github.com/MetaMask/metamask-extension/pull/28021
---
ui/components/app/currency-input/currency-input.js | 1 -
.../user-preferenced-currency-display.component.d.ts | 1 -
.../user-preferenced-currency-display.component.js | 3 ---
.../asset-picker-amount/asset-balance/asset-balance-text.tsx | 3 ---
.../ui/currency-display/currency-display.component.d.ts | 1 -
.../ui/currency-display/currency-display.component.js | 4 +---
ui/pages/asset/components/asset-page.tsx | 3 ---
7 files changed, 1 insertion(+), 15 deletions(-)
diff --git a/ui/components/app/currency-input/currency-input.js b/ui/components/app/currency-input/currency-input.js
index da69f3cbbe70..43da00ad3ab0 100644
--- a/ui/components/app/currency-input/currency-input.js
+++ b/ui/components/app/currency-input/currency-input.js
@@ -222,7 +222,6 @@ export default function CurrencyInput({
suffix={suffix}
className="currency-input__conversion-component"
displayValue={displayValue}
- privacyModeExempt
/>
);
};
diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
index ba465f1c5f08..4db61d568f4a 100644
--- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
+++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
@@ -16,7 +16,6 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion<
showCurrencySuffix?: boolean;
shouldCheckShowNativeToken?: boolean;
isAggregatedFiatOverviewBalance?: boolean;
- privacyModeExempt?: boolean;
}
>;
diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
index 77157e4b5c95..613b731d0a16 100644
--- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
+++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
@@ -28,7 +28,6 @@ export default function UserPreferencedCurrencyDisplay({
showNative,
showCurrencySuffix,
shouldCheckShowNativeToken,
- privacyModeExempt,
...restProps
}) {
// NOTE: When displaying currencies, we need the actual account to detect whether we're in a
@@ -84,7 +83,6 @@ export default function UserPreferencedCurrencyDisplay({
numberOfDecimals={numberOfDecimals}
prefixComponent={prefixComponent}
suffix={showCurrencySuffix && !showEthLogo && currency}
- privacyModeExempt={privacyModeExempt}
/>
);
}
@@ -128,7 +126,6 @@ const UserPreferencedCurrencyDisplayPropTypes = {
textProps: PropTypes.object,
suffixProps: PropTypes.object,
shouldCheckShowNativeToken: PropTypes.bool,
- privacyModeExempt: PropTypes.bool,
};
UserPreferencedCurrencyDisplay.propTypes =
diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx
index fbf608deedd8..67def7ee82b1 100644
--- a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx
+++ b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx
@@ -105,7 +105,6 @@ export function AssetBalanceText({
currency={secondaryCurrency}
numberOfDecimals={2}
displayValue={`${formattedFiat}${errorText}`}
- privacyModeExempt
/>
);
}
@@ -117,7 +116,6 @@ export function AssetBalanceText({
{...commonProps}
value={asset.balance}
type={PRIMARY}
- privacyModeExempt
/>
{errorText ? (
);
}
diff --git a/ui/components/ui/currency-display/currency-display.component.d.ts b/ui/components/ui/currency-display/currency-display.component.d.ts
index fd642cd4e6cd..d8e1d6b60e1e 100644
--- a/ui/components/ui/currency-display/currency-display.component.d.ts
+++ b/ui/components/ui/currency-display/currency-display.component.d.ts
@@ -25,7 +25,6 @@ export type CurrencyDisplayProps = OverridingUnion<
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
suffixProps?: Record;
- privacyModeExempt?: boolean;
}
>;
diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js
index a1b413dd6a7f..a0bb114409f6 100644
--- a/ui/components/ui/currency-display/currency-display.component.js
+++ b/ui/components/ui/currency-display/currency-display.component.js
@@ -35,7 +35,6 @@ export default function CurrencyDisplay({
textProps = {},
suffixProps = {},
isAggregatedFiatOverviewBalance = false,
- privacyModeExempt,
...props
}) {
const { privacyMode } = useSelector(getPreferences);
@@ -77,7 +76,7 @@ export default function CurrencyDisplay({
className="currency-display-component__text"
ellipsis
variant={TextVariant.inherit}
- isHidden={!privacyModeExempt && privacyMode}
+ isHidden={privacyMode}
data-testid="account-value-and-suffix"
{...textProps}
>
@@ -126,7 +125,6 @@ const CurrencyDisplayPropTypes = {
textProps: PropTypes.object,
suffixProps: PropTypes.object,
isAggregatedFiatOverviewBalance: PropTypes.bool,
- privacyModeExempt: PropTypes.bool,
};
CurrencyDisplay.propTypes = CurrencyDisplayPropTypes;
diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx
index 4fb294b17294..c70b60169edb 100644
--- a/ui/pages/asset/components/asset-page.tsx
+++ b/ui/pages/asset/components/asset-page.tsx
@@ -8,7 +8,6 @@ import {
getCurrentCurrency,
getIsBridgeChain,
getIsSwapsChain,
- getPreferences,
getSelectedInternalAccount,
getSwapsDefaultToken,
getTokensMarketData,
@@ -103,7 +102,6 @@ const AssetPage = ({
const conversionRate = useSelector(getConversionRate);
const allMarketData = useSelector(getTokensMarketData);
const isBridgeChain = useSelector(getIsBridgeChain);
- const { privacyMode } = useSelector(getPreferences);
const isBuyableChain = useSelector(getIsNativeTokenBuyable);
const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual);
const account = useSelector(getSelectedInternalAccount, isEqual);
@@ -198,7 +196,6 @@ const AssetPage = ({
tokenImage={image}
isOriginalTokenSymbol={asset.isOriginalNativeSymbol}
isNativeCurrency={true}
- privacyMode={privacyMode}
/>
) : (
Date: Thu, 7 Nov 2024 11:29:03 -0600
Subject: [PATCH 07/18] fix: bump `@metamask/queued-request-controller` with
patch fix (#28355)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Bumps version of QueuedRequestController, with a patch that fixes an
issue where `QueuedRequestController.state.queuedRequestCount` is not
updated after flushing requests for an origin
## References
- https://github.com/MetaMask/core/pull/4899
- https://github.com/MetaMask/core/pull/4846
- https://github.com/MetaMask/metamask-extension/pull/28090
## Fixes
Fixes #28358
[Slack discussion in v12.7.0 RC
Thread](https://consensys.slack.com/archives/C029JG63136/p1730918073046389?thread_ts=1729246801.516029&cid=C029JG63136)
## Before
https://drive.google.com/file/d/1ujdQgVLlT8KlwRwO-Cc3XvRHPrkpxIg_/view?usp=drive_link
## After
https://github.com/user-attachments/assets/e77928e5-165b-441a-b4da-0e10471c0529
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28355?quickstart=1)
## **Manual testing steps**
On a dapp permissioned for chain A and B, on chain A, queue up one send
transaction, then use wallet_switchEthereumChain to switch to chain B,
then queue up several more send transactions. Reject/approve the first
transaction. Afterwards, you should see chain B as the active chain for
the dapp, and all subsequent approvals cleared/rejected automatically.
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
package.json | 2 +-
yarn.lock | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/package.json b/package.json
index b570cc6f9245..2fd832a3ce71 100644
--- a/package.json
+++ b/package.json
@@ -333,7 +333,7 @@
"@metamask/preinstalled-example-snap": "^0.2.0",
"@metamask/profile-sync-controller": "^0.9.7",
"@metamask/providers": "^14.0.2",
- "@metamask/queued-request-controller": "^7.0.0",
+ "@metamask/queued-request-controller": "^7.0.1",
"@metamask/rate-limit-controller": "^6.0.0",
"@metamask/rpc-errors": "^7.0.0",
"@metamask/safe-event-emitter": "^3.1.1",
diff --git a/yarn.lock b/yarn.lock
index aa975513b90a..f22f36057099 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6069,12 +6069,12 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/queued-request-controller@npm:^7.0.0":
- version: 7.0.0
- resolution: "@metamask/queued-request-controller@npm:7.0.0"
+"@metamask/queued-request-controller@npm:^7.0.1":
+ version: 7.0.1
+ resolution: "@metamask/queued-request-controller@npm:7.0.1"
dependencies:
"@metamask/base-controller": "npm:^7.0.2"
- "@metamask/controller-utils": "npm:^11.4.1"
+ "@metamask/controller-utils": "npm:^11.4.2"
"@metamask/json-rpc-engine": "npm:^10.0.1"
"@metamask/rpc-errors": "npm:^7.0.1"
"@metamask/swappable-obj-proxy": "npm:^2.2.0"
@@ -6082,7 +6082,7 @@ __metadata:
peerDependencies:
"@metamask/network-controller": ^22.0.0
"@metamask/selected-network-controller": ^19.0.0
- checksum: 10/69118c11e3faecdbec7c9f02f4ecec4734ce0950115bfac0cdd4338309898690ae3187bcef1cc4f75f54c5c02eff07d80286d3ef29088a665039c13cb50bef88
+ checksum: 10/e5b16b3dc2fa0dcf74a81b5046abb65bc05da3802ee891b5a59a80b980301c790cf949d72adba00ead6f5b3d2eaac40694308297f7dc08eb5e5f05b5a68bbf57
languageName: node
linkType: hard
@@ -26565,7 +26565,7 @@ __metadata:
"@metamask/preinstalled-example-snap": "npm:^0.2.0"
"@metamask/profile-sync-controller": "npm:^0.9.7"
"@metamask/providers": "npm:^14.0.2"
- "@metamask/queued-request-controller": "npm:^7.0.0"
+ "@metamask/queued-request-controller": "npm:^7.0.1"
"@metamask/rate-limit-controller": "npm:^6.0.0"
"@metamask/rpc-errors": "npm:^7.0.0"
"@metamask/safe-event-emitter": "npm:^3.1.1"
From 2484e864219220a5088e831b41c0144c8a717f10 Mon Sep 17 00:00:00 2001
From: David Murdoch <187813+davidmurdoch@users.noreply.github.com>
Date: Thu, 7 Nov 2024 12:57:17 -0500
Subject: [PATCH 08/18] perf: ensure `setupLocale` doesn't fetch
`_locales/en/messages.json` twice (#26553)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
We always load the english version of `messages.json`, but we also
always load the user's locale's `messages.json`. These can be the same
thing but our locale loader didn't take that into consideration. This PR
updates the function to only load the user's local if it differs from
our default locale, otherwise it just uses the same `messages.json`
between the two.
Our locale function has a side effect: it loads locale-related
Internationalization features as well. So this PR also updates those
side-effects in a similar manner to avoid doing the work twice.
---
shared/lib/error-utils.js | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js
index 89caef28026d..66efd6fb61fb 100644
--- a/shared/lib/error-utils.js
+++ b/shared/lib/error-utils.js
@@ -5,17 +5,27 @@ import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred
import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n';
import switchDirection from './switch-direction';
+const defaultLocale = 'en';
const _setupLocale = async (currentLocale) => {
- const currentLocaleMessages = currentLocale
- ? await fetchLocale(currentLocale)
- : {};
- const enLocaleMessages = await fetchLocale('en');
+ const enRelativeTime = loadRelativeTimeFormatLocaleData(defaultLocale);
+ const enLocale = fetchLocale(defaultLocale);
- await loadRelativeTimeFormatLocaleData('en');
- if (currentLocale) {
- await loadRelativeTimeFormatLocaleData(currentLocale);
+ const promises = [enRelativeTime, enLocale];
+ if (currentLocale === defaultLocale) {
+ // enLocaleMessages and currentLocaleMessages are the same; reuse enLocale
+ promises.push(enLocale); // currentLocaleMessages
+ } else if (currentLocale) {
+ // currentLocale does not match enLocaleMessages
+ promises.push(fetchLocale(currentLocale)); // currentLocaleMessages
+ promises.push(loadRelativeTimeFormatLocaleData(currentLocale));
+ } else {
+ // currentLocale is not set
+ promises.push(Promise.resolve({})); // currentLocaleMessages
}
+ const [, enLocaleMessages, currentLocaleMessages] = await Promise.all(
+ promises,
+ );
return { currentLocaleMessages, enLocaleMessages };
};
From 54c563ec980699cb28af5df143de6fa4cacd2b7e Mon Sep 17 00:00:00 2001
From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:19:14 +0400
Subject: [PATCH 09/18] fix: mv2 firefox csp header (#27770)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27770?quickstart=1)
This PR implements a workaround for a long-standing Firefox MV2 bug
where the content-security-policy header is not bypassed, triggering an
error.
The solution is simple: we check if the extension is MV2 running in
Firefox. If yes, we override the header to prevent the error from
raising.
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/3133,
https://github.com/MetaMask/MetaMask-planning/issues/3342
## **Manual testing steps**
1. Opening github.com should not trigger the CSP error
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---------
Co-authored-by: David Murdoch <187813+davidmurdoch@users.noreply.github.com>
---
app/_locales/en/messages.json | 6 ++
app/scripts/background.js | 41 ++++++++
app/scripts/constants/sentry-state.ts | 1 +
.../preferences-controller.test.ts | 17 ++++
.../controllers/preferences-controller.ts | 20 ++++
app/scripts/lib/backup.test.js | 1 +
app/scripts/metamask-controller.js | 4 +
development/build/utils.js | 2 +-
development/create-static-server.js | 13 ++-
development/static-server.js | 2 +-
.../test/plugins.SelfInjectPlugin.test.ts | 4 +-
.../utils/plugins/SelfInjectPlugin/index.ts | 21 +++-
.../utils/plugins/SelfInjectPlugin/types.ts | 15 ++-
package.json | 1 +
shared/modules/add-nonce-to-csp.test.ts | 98 +++++++++++++++++++
shared/modules/add-nonce-to-csp.ts | 38 +++++++
shared/modules/provider-injection.js | 94 +++++++++++-------
shared/modules/provider-injection.test.ts | 6 +-
test/e2e/default-fixture.js | 1 +
test/e2e/fixture-builder.js | 1 +
test/e2e/helpers.js | 5 +-
test/e2e/phishing-warning-page-server.js | 2 +-
.../index.html | 9 ++
.../content-security-policy.spec.ts | 46 +++++++++
...rs-after-init-opt-in-background-state.json | 1 +
.../errors-after-init-opt-in-ui-state.json | 1 +
...s-before-init-opt-in-background-state.json | 1 +
.../errors-before-init-opt-in-ui-state.json | 1 +
.../synchronous-injection.spec.js | 2 +-
ui/helpers/constants/settings.js | 15 +++
ui/helpers/utils/settings-search.js | 6 +-
ui/helpers/utils/settings-search.test.js | 9 +-
.../advanced-tab/advanced-tab.component.js | 45 +++++++++
.../advanced-tab/advanced-tab.container.js | 6 ++
.../advanced-tab/advanced-tab.stories.js | 18 ++++
ui/store/actions.ts | 12 +++
yarn.lock | 10 ++
37 files changed, 515 insertions(+), 60 deletions(-)
create mode 100644 shared/modules/add-nonce-to-csp.test.ts
create mode 100644 shared/modules/add-nonce-to-csp.ts
create mode 100644 test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html
create mode 100644 test/e2e/tests/content-security-policy/content-security-policy.spec.ts
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 62dda6c29ef0..202dc04c2fa8 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -3789,6 +3789,12 @@
"outdatedBrowserNotification": {
"message": "Your browser is out of date. If you don't update your browser, you won't be able to get security patches and new features from MetaMask."
},
+ "overrideContentSecurityPolicyHeader": {
+ "message": "Override Content-Security-Policy header"
+ },
+ "overrideContentSecurityPolicyHeaderDescription": {
+ "message": "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility."
+ },
"padlock": {
"message": "Padlock"
},
diff --git a/app/scripts/background.js b/app/scripts/background.js
index bacb6adddf9f..90a52b6c0d19 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -56,6 +56,8 @@ import {
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { getCurrentChainId } from '../../ui/selectors';
+import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp';
+import { checkURLForProviderInjection } from '../../shared/modules/provider-injection';
import migrations from './migrations';
import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension';
@@ -333,6 +335,40 @@ function maybeDetectPhishing(theController) {
);
}
+/**
+ * Overrides the Content-Security-Policy (CSP) header by adding a nonce to the `script-src` directive.
+ * This is a workaround for [Bug #1446231](https://bugzilla.mozilla.org/show_bug.cgi?id=1446231),
+ * which involves overriding the page CSP for inline script nodes injected by extension content scripts.
+ */
+function overrideContentSecurityPolicyHeader() {
+ // The extension url is unique per install on Firefox, so we can safely add it as a nonce to the CSP header
+ const nonce = btoa(browser.runtime.getURL('/'));
+ browser.webRequest.onHeadersReceived.addListener(
+ ({ responseHeaders, url }) => {
+ // Check whether inpage.js is going to be injected into the page or not.
+ // There is no reason to modify the headers if we are not injecting inpage.js.
+ const isInjected = checkURLForProviderInjection(new URL(url));
+
+ // Check if the user has enabled the overrideContentSecurityPolicyHeader preference
+ const isEnabled =
+ controller.preferencesController.state
+ .overrideContentSecurityPolicyHeader;
+
+ if (isInjected && isEnabled) {
+ for (const header of responseHeaders) {
+ if (header.name.toLowerCase() === 'content-security-policy') {
+ header.value = addNonceToCsp(header.value, nonce);
+ }
+ }
+ }
+
+ return { responseHeaders };
+ },
+ { types: ['main_frame', 'sub_frame'], urls: ['http://*/*', 'https://*/*'] },
+ ['blocking', 'responseHeaders'],
+ );
+}
+
// These are set after initialization
let connectRemote;
let connectExternalExtension;
@@ -479,6 +515,11 @@ async function initialize() {
if (!isManifestV3) {
await loadPhishingWarningPage();
+ // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231
+ if (getPlatform() === PLATFORM_FIREFOX) {
+ overrideContentSecurityPolicyHeader();
+ }
}
await sendReadyMessageToTabs();
log.info('MetaMask initialization complete.');
diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts
index 3125016ea0b5..655851590441 100644
--- a/app/scripts/constants/sentry-state.ts
+++ b/app/scripts/constants/sentry-state.ts
@@ -223,6 +223,7 @@ export const SENTRY_BACKGROUND_STATE = {
advancedGasFee: true,
currentLocale: true,
dismissSeedBackUpReminder: true,
+ overrideContentSecurityPolicyHeader: true,
featureFlags: true,
forgottenPassword: true,
identities: false,
diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts
index 25010cdd3a0f..39a2d49648b2 100644
--- a/app/scripts/controllers/preferences-controller.test.ts
+++ b/app/scripts/controllers/preferences-controller.test.ts
@@ -837,6 +837,23 @@ describe('preferences controller', () => {
});
});
+ describe('overrideContentSecurityPolicyHeader', () => {
+ it('defaults overrideContentSecurityPolicyHeader to true', () => {
+ const { controller } = setupController({});
+ expect(
+ controller.state.overrideContentSecurityPolicyHeader,
+ ).toStrictEqual(true);
+ });
+
+ it('set overrideContentSecurityPolicyHeader to false', () => {
+ const { controller } = setupController({});
+ controller.setOverrideContentSecurityPolicyHeader(false);
+ expect(
+ controller.state.overrideContentSecurityPolicyHeader,
+ ).toStrictEqual(false);
+ });
+ });
+
describe('snapsAddSnapAccountModalDismissed', () => {
it('defaults snapsAddSnapAccountModalDismissed to false', () => {
const { controller } = setupController({});
diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts
index b4ce3ca71e64..dce2ef3d0512 100644
--- a/app/scripts/controllers/preferences-controller.ts
+++ b/app/scripts/controllers/preferences-controller.ts
@@ -133,6 +133,7 @@ export type PreferencesControllerState = Omit<
useNonceField: boolean;
usePhishDetect: boolean;
dismissSeedBackUpReminder: boolean;
+ overrideContentSecurityPolicyHeader: boolean;
useMultiAccountBalanceChecker: boolean;
useSafeChainsListValidation: boolean;
use4ByteResolution: boolean;
@@ -175,6 +176,7 @@ export const getDefaultPreferencesControllerState =
useNonceField: false,
usePhishDetect: true,
dismissSeedBackUpReminder: false,
+ overrideContentSecurityPolicyHeader: true,
useMultiAccountBalanceChecker: true,
useSafeChainsListValidation: true,
// set to true means the dynamic list from the API is being used
@@ -306,6 +308,10 @@ const controllerMetadata = {
persist: true,
anonymous: true,
},
+ overrideContentSecurityPolicyHeader: {
+ persist: true,
+ anonymous: true,
+ },
useMultiAccountBalanceChecker: {
persist: true,
anonymous: true,
@@ -1009,6 +1015,20 @@ export class PreferencesController extends BaseController<
});
}
+ /**
+ * A setter for the user preference to override the Content-Security-Policy header
+ *
+ * @param overrideContentSecurityPolicyHeader - User preference for overriding the Content-Security-Policy header.
+ */
+ setOverrideContentSecurityPolicyHeader(
+ overrideContentSecurityPolicyHeader: boolean,
+ ): void {
+ this.update((state) => {
+ state.overrideContentSecurityPolicyHeader =
+ overrideContentSecurityPolicyHeader;
+ });
+ }
+
/**
* A setter for the incomingTransactions in preference to be updated
*
diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js
index b3a7f176c2e6..826aa04018d9 100644
--- a/app/scripts/lib/backup.test.js
+++ b/app/scripts/lib/backup.test.js
@@ -150,6 +150,7 @@ const jsonData = JSON.stringify({
useNonceField: false,
usePhishDetect: true,
dismissSeedBackUpReminder: false,
+ overrideContentSecurityPolicyHeader: true,
useTokenDetection: false,
useCollectibleDetection: false,
openSeaEnabled: false,
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index caac30573406..d43c12ff24a1 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -3487,6 +3487,10 @@ export default class MetamaskController extends EventEmitter {
preferencesController.setDismissSeedBackUpReminder.bind(
preferencesController,
),
+ setOverrideContentSecurityPolicyHeader:
+ preferencesController.setOverrideContentSecurityPolicyHeader.bind(
+ preferencesController,
+ ),
setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind(
preferencesController,
),
diff --git a/development/build/utils.js b/development/build/utils.js
index 525815d2520a..626aacd588c7 100644
--- a/development/build/utils.js
+++ b/development/build/utils.js
@@ -293,7 +293,7 @@ function getBuildName({
function makeSelfInjecting(filePath) {
const fileContents = readFileSync(filePath, 'utf8');
const textContent = JSON.stringify(fileContents);
- const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};d.documentElement.appendChild(s).remove();}`;
+ const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa((globalThis.browser||chrome).runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`;
writeFileSync(filePath, js, 'utf8');
}
diff --git a/development/create-static-server.js b/development/create-static-server.js
index a8d5e28b0088..8e55fa54ca13 100755
--- a/development/create-static-server.js
+++ b/development/create-static-server.js
@@ -4,10 +4,17 @@ const path = require('path');
const serveHandler = require('serve-handler');
-const createStaticServer = (rootDirectory) => {
+/**
+ * Creates an HTTP server that serves static files from a directory using serve-handler.
+ * If a request URL starts with `/node_modules/`, it rewrites the URL and serves files from the `node_modules` directory.
+ *
+ * @param { NonNullable[2]> } options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler
+ * @returns {http.Server} An instance of an HTTP server configured with the specified options.
+ */
+const createStaticServer = (options) => {
return http.createServer((request, response) => {
if (request.url.startsWith('/node_modules/')) {
- request.url = request.url.substr(14);
+ request.url = request.url.slice(14);
return serveHandler(request, response, {
directoryListing: false,
public: path.resolve('./node_modules'),
@@ -15,7 +22,7 @@ const createStaticServer = (rootDirectory) => {
}
return serveHandler(request, response, {
directoryListing: false,
- public: rootDirectory,
+ ...options,
});
});
};
diff --git a/development/static-server.js b/development/static-server.js
index bb15133d6fdd..ec3a51a512f0 100755
--- a/development/static-server.js
+++ b/development/static-server.js
@@ -31,7 +31,7 @@ const onRequest = (request, response) => {
};
const startServer = ({ port, rootDirectory }) => {
- const server = createStaticServer(rootDirectory);
+ const server = createStaticServer({ public: rootDirectory });
server.on('request', onRequest);
diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts
index 3a3ef729eacf..b6390654a4bb 100644
--- a/development/webpack/test/plugins.SelfInjectPlugin.test.ts
+++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts
@@ -55,7 +55,7 @@ describe('SelfInjectPlugin', () => {
// reference the `sourceMappingURL`
assert.strictEqual(
newSource,
- `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`,
+ `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`,
);
} else {
// the new source should NOT reference the new sourcemap, since it's
@@ -66,7 +66,7 @@ describe('SelfInjectPlugin', () => {
// console.
assert.strictEqual(
newSource,
- `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`,
+ `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`,
);
}
diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts
index b80f6102ab75..18d3624310ae 100644
--- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts
+++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts
@@ -6,6 +6,19 @@ import type { SelfInjectPluginOptions, Source, Compiler } from './types';
export { type SelfInjectPluginOptions } from './types';
+/**
+ * Generates a runtime URL expression for a given path.
+ *
+ * This function constructs a URL string using the `runtime.getURL` method
+ * from either the `globalThis.browser` or `chrome` object, depending on
+ * which one is available in the global scope.
+ *
+ * @param path - The path of the runtime URL.
+ * @returns The constructed runtime URL string.
+ */
+const getRuntimeURLExpression = (path: string) =>
+ `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`;
+
/**
* Default options for the SelfInjectPlugin.
*/
@@ -13,8 +26,11 @@ const defaultOptions = {
// The default `sourceUrlExpression` is configured for browser extensions.
// It generates the absolute url of the given file as an extension url.
// e.g., `chrome-extension:///scripts/inpage.js`
- sourceUrlExpression: (filename: string) =>
- `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(filename)})`,
+ sourceUrlExpression: getRuntimeURLExpression,
+ // The default `nonceExpression` is configured for browser extensions.
+ // It generates the absolute url of a path as an extension url in base64.
+ // e.g., `Y2hyb21lLWV4dGVuc2lvbjovLzxleHRlbnNpb24taWQ+Lw==`
+ nonceExpression: (path: string) => `btoa(${getRuntimeURLExpression(path)})`,
} satisfies SelfInjectPluginOptions;
/**
@@ -142,6 +158,7 @@ export class SelfInjectPlugin {
`\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``,
);
newSource.add(`;`);
+ newSource.add(`s.nonce=${this.options.nonceExpression('/')};`);
// add and immediately remove the script to avoid modifying the DOM.
newSource.add(`d.documentElement.appendChild(s).remove()`);
newSource.add(`}`);
diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts
index 2240227bd76a..e70467cd270b 100644
--- a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts
+++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts
@@ -23,7 +23,7 @@ export type SelfInjectPluginOptions = {
* will be injected into matched file to provide a sourceURL for the self
* injected script.
*
- * Defaults to `(filename: string) => (globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")`
+ * Defaults to `(filename: string) => (globalThis.browser||chrome).runtime.getURL("${filename}")`
*
* @example Custom
* ```js
@@ -39,11 +39,22 @@ export type SelfInjectPluginOptions = {
*
* ```js
* {
- * sourceUrlExpression: (filename) => `(globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")`
+ * sourceUrlExpression: (filename) => `(globalThis.browser||chrome).runtime.getURL("${filename}")`
* }
* ```
* @param filename - the chunk's relative filename as it will exist in the output directory
* @returns
*/
sourceUrlExpression?: (filename: string) => string;
+ /**
+ * A function that returns a JavaScript expression escaped as a string which
+ * will be injected into matched file to set a nonce for the self
+ * injected script.
+ *
+ * Defaults to `(path: string) => btoa((globalThis.browser||chrome).runtime.getURL("${path}"))`
+ *
+ * @param path - the path to be encoded as a nonce
+ * @returns
+ */
+ nonceExpression?: (path: string) => string;
};
diff --git a/package.json b/package.json
index 2fd832a3ce71..c612dd76fb20 100644
--- a/package.json
+++ b/package.json
@@ -528,6 +528,7 @@
"@types/redux-mock-store": "1.0.6",
"@types/remote-redux-devtools": "^0.5.5",
"@types/selenium-webdriver": "^4.1.19",
+ "@types/serve-handler": "^6.1.4",
"@types/sinon": "^10.0.13",
"@types/sprintf-js": "^1",
"@types/w3c-web-hid": "^1.0.3",
diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts
new file mode 100644
index 000000000000..dc80bfd9a92a
--- /dev/null
+++ b/shared/modules/add-nonce-to-csp.test.ts
@@ -0,0 +1,98 @@
+import { addNonceToCsp } from './add-nonce-to-csp';
+
+describe('addNonceToCsp', () => {
+ it('empty string', () => {
+ const input = '';
+ const expected = '';
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('one empty directive', () => {
+ const input = 'script-src';
+ const expected = `script-src 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('one directive, one value', () => {
+ const input = 'script-src default.example';
+ const expected = `script-src default.example 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('one directive, two values', () => {
+ const input = "script-src 'self' default.example";
+ const expected = `script-src 'self' default.example 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('multiple directives', () => {
+ const input =
+ "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example";
+ const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('no applicable directive', () => {
+ const input = 'img-src https://example.com';
+ const expected = `img-src https://example.com`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('non-ASCII directives', () => {
+ const input = 'script-src default.example;\u0080;style-src style.example';
+ const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('uppercase directive names', () => {
+ const input = 'SCRIPT-SRC DEFAULT.EXAMPLE';
+ const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('duplicate directive names', () => {
+ const input =
+ 'default-src default.example; script-src script.example; script-src script.example';
+ const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('nonce value contains script-src', () => {
+ const input =
+ "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com";
+ const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('url value contains script-src', () => {
+ const input =
+ "default-src 'self' https://script-src.com; script-src 'self' https://example.com";
+ const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('fallback to default-src', () => {
+ const input = `default-src 'none'`;
+ const expected = `default-src 'none' 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+
+ it('keep ascii whitespace characters', () => {
+ const input = ' script-src default.example ';
+ const expected = ` script-src default.example 'nonce-test'`;
+ const output = addNonceToCsp(input, 'test');
+ expect(output).toBe(expected);
+ });
+});
diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts
new file mode 100644
index 000000000000..a8b7fe333089
--- /dev/null
+++ b/shared/modules/add-nonce-to-csp.ts
@@ -0,0 +1,38 @@
+// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE.
+// See .
+const ASCII_WHITESPACE_CHARS = ['\t', '\n', '\f', '\r', ' '].join('');
+
+const matchDirective = (directive: string) =>
+ /* eslint-disable require-unicode-regexp */
+ new RegExp(
+ `^([${ASCII_WHITESPACE_CHARS}]*${directive}[${ASCII_WHITESPACE_CHARS}]*)`, // Match the directive and surrounding ASCII whitespace
+ 'is', // Case-insensitive, including newlines
+ );
+const matchScript = matchDirective('script-src');
+const matchDefault = matchDirective('default-src');
+
+/**
+ * Adds a nonce to a Content Security Policy (CSP) string.
+ *
+ * @param text - The Content Security Policy (CSP) string to add the nonce to.
+ * @param nonce - The nonce to add to the Content Security Policy (CSP) string.
+ * @returns The updated Content Security Policy (CSP) string.
+ */
+export const addNonceToCsp = (text: string, nonce: string) => {
+ const formattedNonce = ` 'nonce-${nonce}'`;
+ const directives = text.split(';');
+ const scriptIndex = directives.findIndex((directive) =>
+ matchScript.test(directive),
+ );
+ if (scriptIndex >= 0) {
+ directives[scriptIndex] += formattedNonce;
+ } else {
+ const defaultIndex = directives.findIndex((directive) =>
+ matchDefault.test(directive),
+ );
+ if (defaultIndex >= 0) {
+ directives[defaultIndex] += formattedNonce;
+ }
+ }
+ return directives.join(';');
+};
diff --git a/shared/modules/provider-injection.js b/shared/modules/provider-injection.js
index 25a316e93440..b96df88c29e2 100644
--- a/shared/modules/provider-injection.js
+++ b/shared/modules/provider-injection.js
@@ -5,40 +5,36 @@
*/
export default function shouldInjectProvider() {
return (
- doctypeCheck() &&
- suffixCheck() &&
- documentElementCheck() &&
- !blockedDomainCheck()
+ checkURLForProviderInjection(new URL(window.location)) &&
+ checkDocumentForProviderInjection()
);
}
/**
- * Checks the doctype of the current document if it exists
+ * Checks if a given URL is eligible for provider injection.
*
- * @returns {boolean} {@code true} if the doctype is html or if none exists
+ * This function determines if a URL passes the suffix check and is not part of the blocked domains.
+ *
+ * @param {URL} url - The URL to be checked for injection.
+ * @returns {boolean} Returns `true` if the URL passes the suffix check and is not blocked, otherwise `false`.
*/
-function doctypeCheck() {
- const { doctype } = window.document;
- if (doctype) {
- return doctype.name === 'html';
- }
- return true;
+export function checkURLForProviderInjection(url) {
+ return suffixCheck(url) && !blockedDomainCheck(url);
}
/**
- * Returns whether or not the extension (suffix) of the current document is prohibited
+ * Returns whether or not the extension (suffix) of the given URL's pathname is prohibited
*
- * This checks {@code window.location.pathname} against a set of file extensions
- * that we should not inject the provider into. This check is indifferent of
- * query parameters in the location.
+ * This checks the provided URL's pathname against a set of file extensions
+ * that we should not inject the provider into.
*
- * @returns {boolean} whether or not the extension of the current document is prohibited
+ * @param {URL} url - The URL to check
+ * @returns {boolean} whether or not the extension of the given URL's pathname is prohibited
*/
-function suffixCheck() {
+function suffixCheck({ pathname }) {
const prohibitedTypes = [/\.xml$/u, /\.pdf$/u];
- const currentUrl = window.location.pathname;
for (let i = 0; i < prohibitedTypes.length; i++) {
- if (prohibitedTypes[i].test(currentUrl)) {
+ if (prohibitedTypes[i].test(pathname)) {
return false;
}
}
@@ -46,24 +42,12 @@ function suffixCheck() {
}
/**
- * Checks the documentElement of the current document
+ * Checks if the given domain is blocked
*
- * @returns {boolean} {@code true} if the documentElement is an html node or if none exists
+ * @param {URL} url - The URL to check
+ * @returns {boolean} {@code true} if the given domain is blocked
*/
-function documentElementCheck() {
- const documentElement = document.documentElement.nodeName;
- if (documentElement) {
- return documentElement.toLowerCase() === 'html';
- }
- return true;
-}
-
-/**
- * Checks if the current domain is blocked
- *
- * @returns {boolean} {@code true} if the current domain is blocked
- */
-function blockedDomainCheck() {
+function blockedDomainCheck(url) {
// If making any changes, please also update the same list found in the MetaMask-Mobile & SDK repositories
const blockedDomains = [
'execution.consensys.io',
@@ -85,8 +69,7 @@ function blockedDomainCheck() {
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
];
- const { hostname: currentHostname, pathname: currentPathname } =
- window.location;
+ const { hostname: currentHostname, pathname: currentPathname } = url;
const trimTrailingSlash = (str) =>
str.endsWith('/') ? str.slice(0, -1) : str;
@@ -104,3 +87,38 @@ function blockedDomainCheck() {
)
);
}
+
+/**
+ * Checks if the document is suitable for provider injection by verifying the doctype and document element.
+ *
+ * @returns {boolean} `true` if the document passes both the doctype and document element checks, otherwise `false`.
+ */
+export function checkDocumentForProviderInjection() {
+ return doctypeCheck() && documentElementCheck();
+}
+
+/**
+ * Checks the doctype of the current document if it exists
+ *
+ * @returns {boolean} {@code true} if the doctype is html or if none exists
+ */
+function doctypeCheck() {
+ const { doctype } = window.document;
+ if (doctype) {
+ return doctype.name === 'html';
+ }
+ return true;
+}
+
+/**
+ * Checks the documentElement of the current document
+ *
+ * @returns {boolean} {@code true} if the documentElement is an html node or if none exists
+ */
+function documentElementCheck() {
+ const documentElement = document.documentElement.nodeName;
+ if (documentElement) {
+ return documentElement.toLowerCase() === 'html';
+ }
+ return true;
+}
diff --git a/shared/modules/provider-injection.test.ts b/shared/modules/provider-injection.test.ts
index 1742e31b4db6..c7da24b46642 100644
--- a/shared/modules/provider-injection.test.ts
+++ b/shared/modules/provider-injection.test.ts
@@ -8,11 +8,7 @@ describe('shouldInjectProvider', () => {
const urlObj = new URL(urlString);
mockedWindow.mockImplementation(() => ({
- location: {
- hostname: urlObj.hostname,
- origin: urlObj.origin,
- pathname: urlObj.pathname,
- },
+ location: urlObj,
document: {
doctype: {
name: 'html',
diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js
index 2d3d2999ed43..fd2d5be42891 100644
--- a/test/e2e/default-fixture.js
+++ b/test/e2e/default-fixture.js
@@ -193,6 +193,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) {
currentLocale: 'en',
useExternalServices: true,
dismissSeedBackUpReminder: true,
+ overrideContentSecurityPolicyHeader: true,
featureFlags: {},
forgottenPassword: false,
identities: {
diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js
index 334e2f74ceca..bea9e9bad77f 100644
--- a/test/e2e/fixture-builder.js
+++ b/test/e2e/fixture-builder.js
@@ -63,6 +63,7 @@ function onboardingFixture() {
advancedGasFee: {},
currentLocale: 'en',
dismissSeedBackUpReminder: false,
+ overrideContentSecurityPolicyHeader: true,
featureFlags: {},
forgottenPassword: false,
identities: {},
diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js
index 1cb9e47f1f80..c3705c1ebf6c 100644
--- a/test/e2e/helpers.js
+++ b/test/e2e/helpers.js
@@ -65,6 +65,7 @@ async function withFixtures(options, testSuite) {
smartContract,
driverOptions,
dappOptions,
+ staticServerOptions,
title,
ignoredConsoleErrors = [],
dappPath = undefined,
@@ -159,7 +160,9 @@ async function withFixtures(options, testSuite) {
'dist',
);
}
- dappServer.push(createStaticServer(dappDirectory));
+ dappServer.push(
+ createStaticServer({ public: dappDirectory, ...staticServerOptions }),
+ );
dappServer[i].listen(`${dappBasePort + i}`);
await new Promise((resolve, reject) => {
dappServer[i].on('listening', resolve);
diff --git a/test/e2e/phishing-warning-page-server.js b/test/e2e/phishing-warning-page-server.js
index 2f6099e1a3d0..a66c9211ed83 100644
--- a/test/e2e/phishing-warning-page-server.js
+++ b/test/e2e/phishing-warning-page-server.js
@@ -13,7 +13,7 @@ const phishingWarningDirectory = path.resolve(
class PhishingWarningPageServer {
constructor() {
- this._server = createStaticServer(phishingWarningDirectory);
+ this._server = createStaticServer({ public: phishingWarningDirectory });
}
async start({ port = 9999 } = {}) {
diff --git a/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html
new file mode 100644
index 000000000000..dae42d094890
--- /dev/null
+++ b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Mock CSP header testing
+
+
+ Mock Page for Content-Security-Policy header testing
+
+
diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts
new file mode 100644
index 000000000000..996c64343c5b
--- /dev/null
+++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts
@@ -0,0 +1,46 @@
+import { strict as assert } from 'assert';
+import { Suite } from 'mocha';
+import {
+ defaultGanacheOptions,
+ openDapp,
+ unlockWallet,
+ withFixtures,
+} from '../../helpers';
+import FixtureBuilder from '../../fixture-builder';
+
+describe('Content-Security-Policy', function (this: Suite) {
+ it('opening a restricted website should still load the extension', async function () {
+ await withFixtures(
+ {
+ dapp: true,
+ dappPaths: [
+ './tests/content-security-policy/content-security-policy-mock-page',
+ ],
+ staticServerOptions: {
+ headers: [
+ {
+ source: 'index.html',
+ headers: [
+ {
+ key: 'Content-Security-Policy',
+ value: `default-src 'none'`,
+ },
+ ],
+ },
+ ],
+ },
+ fixtures: new FixtureBuilder().build(),
+ ganacheOptions: defaultGanacheOptions,
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }) => {
+ await unlockWallet(driver);
+ await openDapp(driver);
+ const isExtensionLoaded: boolean = await driver.executeScript(
+ 'return typeof window.ethereum !== "undefined"',
+ );
+ assert.equal(isExtensionLoaded, true);
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json
index 1a871780591f..cebacab2b1d9 100644
--- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json
+++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json
@@ -198,6 +198,7 @@
"useNonceField": false,
"usePhishDetect": true,
"dismissSeedBackUpReminder": true,
+ "overrideContentSecurityPolicyHeader": true,
"useMultiAccountBalanceChecker": true,
"useSafeChainsListValidation": "boolean",
"useTokenDetection": true,
diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json
index e8f8f81a6293..97399d34c508 100644
--- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json
+++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json
@@ -115,6 +115,7 @@
"useNonceField": false,
"usePhishDetect": true,
"dismissSeedBackUpReminder": true,
+ "overrideContentSecurityPolicyHeader": true,
"useMultiAccountBalanceChecker": true,
"useSafeChainsListValidation": true,
"useTokenDetection": true,
diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json
index 1a51023a2ca1..7622ad15937c 100644
--- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json
+++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json
@@ -115,6 +115,7 @@
"currentLocale": "en",
"useExternalServices": "boolean",
"dismissSeedBackUpReminder": true,
+ "overrideContentSecurityPolicyHeader": true,
"featureFlags": {},
"forgottenPassword": false,
"identities": "object",
diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json
index bf7f87a16134..b3fa8d117beb 100644
--- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json
+++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json
@@ -115,6 +115,7 @@
"currentLocale": "en",
"useExternalServices": "boolean",
"dismissSeedBackUpReminder": true,
+ "overrideContentSecurityPolicyHeader": true,
"featureFlags": {},
"forgottenPassword": false,
"identities": "object",
diff --git a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js
index 917e289f320f..8e4254fb8b34 100644
--- a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js
+++ b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js
@@ -7,7 +7,7 @@ const dappPort = 8080;
describe('The provider', function () {
it('can be injected synchronously and successfully used by a dapp', async function () {
- const dappServer = createStaticServer(__dirname);
+ const dappServer = createStaticServer({ public: __dirname });
dappServer.listen(dappPort);
await new Promise((resolve, reject) => {
dappServer.on('listening', resolve);
diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js
index c22b0cbcf183..232bcdae5aff 100644
--- a/ui/helpers/constants/settings.js
+++ b/ui/helpers/constants/settings.js
@@ -1,4 +1,8 @@
/* eslint-disable @metamask/design-tokens/color-no-hex*/
+// TODO: Remove restricted import
+// eslint-disable-next-line import/no-restricted-paths
+import { getPlatform } from '../../../app/scripts/lib/util';
+import { PLATFORM_FIREFOX } from '../../../shared/constants/app';
import { IconName } from '../../components/component-library';
import {
ADVANCED_ROUTE,
@@ -19,6 +23,7 @@ import {
* # @param {string} route tab route with appended arbitrary, unique anchor tag / hash route
* # @param {string} iconName
* # @param {string} featureFlag ENV variable name. If the ENV value exists, the route will be searchable; else, route will not be searchable.
+ * # @param {boolean} hidden If true, the route will not be searchable.
*/
/** @type {SettingRouteConfig[]} */
@@ -154,6 +159,16 @@ const SETTINGS_CONSTANTS = [
route: `${ADVANCED_ROUTE}#export-data`,
icon: 'fas fa-download',
},
+ // advanced settingsRefs[11]
+ {
+ tabMessage: (t) => t('advanced'),
+ sectionMessage: (t) => t('overrideContentSecurityPolicyHeader'),
+ descriptionMessage: (t) =>
+ t('overrideContentSecurityPolicyHeaderDescription'),
+ route: `${ADVANCED_ROUTE}#override-content-security-policy-header`,
+ icon: 'fas fa-sliders-h',
+ hidden: getPlatform() !== PLATFORM_FIREFOX,
+ },
{
tabMessage: (t) => t('contacts'),
sectionMessage: (t) => t('contacts'),
diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js
index 07b4501c0208..8c11ad8fad52 100644
--- a/ui/helpers/utils/settings-search.js
+++ b/ui/helpers/utils/settings-search.js
@@ -8,8 +8,10 @@ export function getSettingsRoutes() {
if (settingsRoutes) {
return settingsRoutes;
}
- settingsRoutes = SETTINGS_CONSTANTS.filter((routeObject) =>
- routeObject.featureFlag ? process.env[routeObject.featureFlag] : true,
+ settingsRoutes = SETTINGS_CONSTANTS.filter(
+ (routeObject) =>
+ (routeObject.featureFlag ? process.env[routeObject.featureFlag] : true) &&
+ !routeObject.hidden,
);
return settingsRoutes;
}
diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js
index cc7b875d8c5e..30af3ee6b9da 100644
--- a/ui/helpers/utils/settings-search.test.js
+++ b/ui/helpers/utils/settings-search.test.js
@@ -68,6 +68,10 @@ const t = (key) => {
return 'Dismiss Secret Recovery Phrase backup reminder';
case 'dismissReminderDescriptionField':
return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds';
+ case 'overrideContentSecurityPolicyHeader':
+ return 'Override Content-Security-Policy header';
+ case 'overrideContentSecurityPolicyHeaderDescription':
+ return "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility.";
case 'Contacts':
return 'Contacts';
case 'securityAndPrivacy':
@@ -147,9 +151,12 @@ describe('Settings Search Utils', () => {
describe('getSettingsRoutes', () => {
it('should be an array of settings routes objects', () => {
const NUM_OF_ENV_FEATURE_FLAG_SETTINGS = 4;
+ const NUM_OF_HIDDEN_SETTINGS = 1;
expect(getSettingsRoutes()).toHaveLength(
- SETTINGS_CONSTANTS.length - NUM_OF_ENV_FEATURE_FLAG_SETTINGS,
+ SETTINGS_CONSTANTS.length -
+ NUM_OF_ENV_FEATURE_FLAG_SETTINGS -
+ NUM_OF_HIDDEN_SETTINGS,
);
});
});
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js
index 132b97f7caa9..6be7bcd52004 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.component.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js
@@ -29,6 +29,10 @@ import {
getNumberOfSettingRoutesInTab,
handleSettingsRefs,
} from '../../../helpers/utils/settings-search';
+// TODO: Remove restricted import
+// eslint-disable-next-line import/no-restricted-paths
+import { getPlatform } from '../../../../app/scripts/lib/util';
+import { PLATFORM_FIREFOX } from '../../../../shared/constants/app';
export default class AdvancedTab extends PureComponent {
static contextTypes = {
@@ -58,6 +62,8 @@ export default class AdvancedTab extends PureComponent {
backupUserData: PropTypes.func.isRequired,
showExtensionInFullSizeView: PropTypes.bool,
setShowExtensionInFullSizeView: PropTypes.func.isRequired,
+ overrideContentSecurityPolicyHeader: PropTypes.bool,
+ setOverrideContentSecurityPolicyHeader: PropTypes.func.isRequired,
};
state = {
@@ -583,6 +589,42 @@ export default class AdvancedTab extends PureComponent {
);
}
+ renderOverrideContentSecurityPolicyHeader() {
+ const { t } = this.context;
+ const {
+ overrideContentSecurityPolicyHeader,
+ setOverrideContentSecurityPolicyHeader,
+ } = this.props;
+
+ return (
+
+
+
{t('overrideContentSecurityPolicyHeader')}
+
+ {t('overrideContentSecurityPolicyHeaderDescription')}
+
+
+
+
+ setOverrideContentSecurityPolicyHeader(!value)}
+ offLabel={t('off')}
+ onLabel={t('on')}
+ />
+
+
+ );
+ }
+
render() {
const { errorInSettings } = this.props;
// When adding/removing/editing the order of renders, double-check the order of the settingsRefs. This affects settings-search.js
@@ -602,6 +644,9 @@ export default class AdvancedTab extends PureComponent {
{this.renderAutoLockTimeLimit()}
{this.renderUserDataBackup()}
{this.renderDismissSeedBackupReminderControl()}
+ {getPlatform() === PLATFORM_FIREFOX
+ ? this.renderOverrideContentSecurityPolicyHeader()
+ : null}
);
}
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js
index aaa094e0655c..0be61499c1af 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.container.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js
@@ -7,6 +7,7 @@ import {
backupUserData,
setAutoLockTimeLimit,
setDismissSeedBackUpReminder,
+ setOverrideContentSecurityPolicyHeader,
setFeatureFlag,
setShowExtensionInFullSizeView,
setShowFiatConversionOnTestnetsPreference,
@@ -31,6 +32,7 @@ export const mapStateToProps = (state) => {
featureFlags: { sendHexData } = {},
useNonceField,
dismissSeedBackUpReminder,
+ overrideContentSecurityPolicyHeader,
} = metamask;
const {
showFiatInTestnets,
@@ -49,6 +51,7 @@ export const mapStateToProps = (state) => {
autoLockTimeLimit,
useNonceField,
dismissSeedBackUpReminder,
+ overrideContentSecurityPolicyHeader,
};
};
@@ -81,6 +84,9 @@ export const mapDispatchToProps = (dispatch) => {
setDismissSeedBackUpReminder: (value) => {
return dispatch(setDismissSeedBackUpReminder(value));
},
+ setOverrideContentSecurityPolicyHeader: (value) => {
+ return dispatch(setOverrideContentSecurityPolicyHeader(value));
+ },
};
};
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js
index 36c84cbba8c0..855bdd8aa86a 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.stories.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js
@@ -12,6 +12,7 @@ export default {
showFiatInTestnets: { control: 'boolean' },
useLedgerLive: { control: 'boolean' },
dismissSeedBackUpReminder: { control: 'boolean' },
+ overrideContentSecurityPolicyHeader: { control: 'boolean' },
setAutoLockTimeLimit: { action: 'setAutoLockTimeLimit' },
setShowFiatConversionOnTestnetsPreference: {
action: 'setShowFiatConversionOnTestnetsPreference',
@@ -20,6 +21,9 @@ export default {
setIpfsGateway: { action: 'setIpfsGateway' },
setIsIpfsGatewayEnabled: { action: 'setIsIpfsGatewayEnabled' },
setDismissSeedBackUpReminder: { action: 'setDismissSeedBackUpReminder' },
+ setOverrideContentSecurityPolicyHeader: {
+ action: 'setOverrideContentSecurityPolicyHeader',
+ },
setUseNonceField: { action: 'setUseNonceField' },
setHexDataFeatureFlag: { action: 'setHexDataFeatureFlag' },
displayErrorInSettings: { action: 'displayErrorInSettings' },
@@ -38,6 +42,7 @@ export const DefaultStory = (args) => {
sendHexData,
showFiatInTestnets,
dismissSeedBackUpReminder,
+ overrideContentSecurityPolicyHeader,
},
updateArgs,
] = useArgs();
@@ -65,6 +70,12 @@ export const DefaultStory = (args) => {
dismissSeedBackUpReminder: !dismissSeedBackUpReminder,
});
};
+
+ const handleOverrideContentSecurityPolicyHeader = () => {
+ updateArgs({
+ overrideContentSecurityPolicyHeader: !overrideContentSecurityPolicyHeader,
+ });
+ };
return (
{
setShowFiatConversionOnTestnetsPreference={handleShowFiatInTestnets}
dismissSeedBackUpReminder={dismissSeedBackUpReminder}
setDismissSeedBackUpReminder={handleDismissSeedBackUpReminder}
+ overrideContentSecurityPolicyHeader={
+ overrideContentSecurityPolicyHeader
+ }
+ setOverrideContentSecurityPolicyHeader={
+ handleOverrideContentSecurityPolicyHeader
+ }
ipfsGateway="ipfs-gateway"
/>
@@ -91,4 +108,5 @@ DefaultStory.args = {
showFiatInTestnets: false,
useLedgerLive: false,
dismissSeedBackUpReminder: false,
+ overrideContentSecurityPolicyHeader: true,
};
diff --git a/ui/store/actions.ts b/ui/store/actions.ts
index 4f66067a5759..67de497817c8 100644
--- a/ui/store/actions.ts
+++ b/ui/store/actions.ts
@@ -4218,6 +4218,18 @@ export function setDismissSeedBackUpReminder(
};
}
+export function setOverrideContentSecurityPolicyHeader(
+ value: boolean,
+): ThunkAction {
+ return async (dispatch: MetaMaskReduxDispatch) => {
+ dispatch(showLoadingIndication());
+ await submitRequestToBackground('setOverrideContentSecurityPolicyHeader', [
+ value,
+ ]);
+ dispatch(hideLoadingIndication());
+ };
+}
+
export function getRpcMethodPreferences(): ThunkAction<
void,
MetaMaskReduxState,
diff --git a/yarn.lock b/yarn.lock
index f22f36057099..4352ef21767a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11134,6 +11134,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/serve-handler@npm:^6.1.4":
+ version: 6.1.4
+ resolution: "@types/serve-handler@npm:6.1.4"
+ dependencies:
+ "@types/node": "npm:*"
+ checksum: 10/c92ae204605659b37202af97cfcc7690be43b9290692c1d6c3c93805b399044fd67573af4eb2e7b1fd975451db6d0d5c6cd2f09b20997209fa3341f345f661e4
+ languageName: node
+ linkType: hard
+
"@types/serve-index@npm:^1.9.4":
version: 1.9.4
resolution: "@types/serve-index@npm:1.9.4"
@@ -26654,6 +26663,7 @@ __metadata:
"@types/redux-mock-store": "npm:1.0.6"
"@types/remote-redux-devtools": "npm:^0.5.5"
"@types/selenium-webdriver": "npm:^4.1.19"
+ "@types/serve-handler": "npm:^6.1.4"
"@types/sinon": "npm:^10.0.13"
"@types/sprintf-js": "npm:^1"
"@types/w3c-web-hid": "npm:^1.0.3"
From 82fdd64f5787ac4da20d2da6ddcd55787d136163 Mon Sep 17 00:00:00 2001
From: Nick Gambino <35090461+gambinish@users.noreply.github.com>
Date: Thu, 7 Nov 2024 15:16:31 -0800
Subject: [PATCH 10/18] fix: Bug 28347 - Privacy mode tweaks (#28367)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Privacy Mode should only effect PortfolioView and main account picker
popover. It should not impact other areas of the App like Send/Swap/Gas
because the toggle only exists on PortfolioView.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28367?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/28347
## **Manual testing steps**
You can toggle privacyMode with eyeball on main PortfolioView
Should respect privacyMode:
1. Go to PortfolioView, toggling eyeball should show/hide balances for
tokens as well as main balance
2. Go to AccountPicker from main Portfolio View, balances should
hide/show
3. Go to AccountPicker from asset detail view, balances should hide/show
Should _not_ respect privacyMode:
1. Go to AssetDetails, token balance should show on main page
2. Should not be respected on send/swap/gas screens
3. Balances should not be impacted elsewhere in the app. Please try to
verify this while reviewing.
## **Screenshots/Recordings**
https://github.com/user-attachments/assets/695fa68a-c9bb-4871-b03c-8c41c88b1344
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
ui/components/app/assets/token-cell/token-cell.tsx | 5 +++--
ui/components/app/assets/token-list/token-list.tsx | 4 +++-
.../user-preferenced-currency-display.component.d.ts | 1 +
.../user-preferenced-currency-display.component.js | 3 +++
ui/components/app/wallet-overview/coin-overview.tsx | 6 +++---
.../multichain/account-list-item/account-list-item.js | 7 +++++++
.../multichain/account-list-menu/account-list-menu.tsx | 3 +++
.../ui/currency-display/currency-display.component.js | 5 ++---
ui/pages/routes/routes.component.js | 7 ++++++-
ui/pages/routes/routes.container.js | 3 ++-
10 files changed, 33 insertions(+), 11 deletions(-)
diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx
index 3a042de1ebb8..31bb388aa65b 100644
--- a/ui/components/app/assets/token-cell/token-cell.tsx
+++ b/ui/components/app/assets/token-cell/token-cell.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
-import { getTokenList, getPreferences } from '../../../../selectors';
+import { getTokenList } from '../../../../selectors';
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
import { TokenListItem } from '../../../multichain';
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
@@ -12,6 +12,7 @@ type TokenCellProps = {
symbol: string;
string?: string;
image: string;
+ privacyMode?: boolean;
onClick?: (arg: string) => void;
};
@@ -20,10 +21,10 @@ export default function TokenCell({
image,
symbol,
string,
+ privacyMode = false,
onClick,
}: TokenCellProps) {
const tokenList = useSelector(getTokenList);
- const { privacyMode } = useSelector(getPreferences);
const tokenData = Object.values(tokenList).find(
(token) =>
isEqualCaseInsensitive(token.symbol, symbol) &&
diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx
index 11190c68f267..f0b17d686026 100644
--- a/ui/components/app/assets/token-list/token-list.tsx
+++ b/ui/components/app/assets/token-list/token-list.tsx
@@ -30,7 +30,8 @@ export default function TokenList({
nativeToken,
}: TokenListProps) {
const t = useI18nContext();
- const { tokenSortConfig, tokenNetworkFilter } = useSelector(getPreferences);
+ const { tokenSortConfig, tokenNetworkFilter, privacyMode } =
+ useSelector(getPreferences);
const selectedAccount = useSelector(getSelectedAccount);
const conversionRate = useSelector(getConversionRate);
const nativeTokenWithBalance = useNativeTokenBalance();
@@ -88,6 +89,7 @@ export default function TokenList({
);
diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
index 4db61d568f4a..779309858a18 100644
--- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
+++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
@@ -16,6 +16,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion<
showCurrencySuffix?: boolean;
shouldCheckShowNativeToken?: boolean;
isAggregatedFiatOverviewBalance?: boolean;
+ privacyMode?: boolean;
}
>;
diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
index 613b731d0a16..a466f7813672 100644
--- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
+++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
@@ -28,6 +28,7 @@ export default function UserPreferencedCurrencyDisplay({
showNative,
showCurrencySuffix,
shouldCheckShowNativeToken,
+ privacyMode = false,
...restProps
}) {
// NOTE: When displaying currencies, we need the actual account to detect whether we're in a
@@ -83,6 +84,7 @@ export default function UserPreferencedCurrencyDisplay({
numberOfDecimals={numberOfDecimals}
prefixComponent={prefixComponent}
suffix={showCurrencySuffix && !showEthLogo && currency}
+ privacyMode={privacyMode}
/>
);
}
@@ -126,6 +128,7 @@ const UserPreferencedCurrencyDisplayPropTypes = {
textProps: PropTypes.object,
suffixProps: PropTypes.object,
shouldCheckShowNativeToken: PropTypes.bool,
+ privacyMode: PropTypes.bool,
};
UserPreferencedCurrencyDisplay.propTypes =
diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx
index 9f267c96a53d..93d9e1061428 100644
--- a/ui/components/app/wallet-overview/coin-overview.tsx
+++ b/ui/components/app/wallet-overview/coin-overview.tsx
@@ -132,7 +132,8 @@ export const CoinOverview = ({
const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover);
const isTestnet = useSelector(getIsTestnet);
- const { showFiatInTestnets, privacyMode } = useSelector(getPreferences);
+ const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } =
+ useSelector(getPreferences);
const selectedAccount = useSelector(getSelectedAccount);
const shouldHideZeroBalanceTokens = useSelector(
@@ -143,8 +144,6 @@ export const CoinOverview = ({
shouldHideZeroBalanceTokens,
);
- const { showNativeTokenAsMainBalance } = useSelector(getPreferences);
-
const isEvm = useSelector(getMultichainIsEvm);
const isNotAggregatedFiatBalance =
showNativeTokenAsMainBalance || isTestnet || !isEvm;
@@ -281,6 +280,7 @@ export const CoinOverview = ({
isAggregatedFiatOverviewBalance={
!showNativeTokenAsMainBalance && !isTestnet
}
+ privacyMode={privacyMode}
/>
{
const t = useI18nContext();
const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false);
@@ -313,6 +314,7 @@ const AccountListItem = ({
type={PRIMARY}
showFiat={showFiat}
data-testid="first-currency-display"
+ privacyMode={privacyMode}
/>
@@ -360,6 +362,7 @@ const AccountListItem = ({
type={SECONDARY}
showNative
data-testid="second-currency-display"
+ privacyMode={privacyMode}
/>
@@ -507,6 +510,10 @@ AccountListItem.propTypes = {
* Determines if list item should be scrolled to when selected
*/
shouldScrollToWhenSelected: PropTypes.bool,
+ /**
+ * Determines if list balance should be obfuscated
+ */
+ privacyMode: PropTypes.bool,
};
AccountListItem.displayName = 'AccountListItem';
diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx
index eff0d3cb8868..cfb49d246ca6 100644
--- a/ui/components/multichain/account-list-menu/account-list-menu.tsx
+++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx
@@ -188,6 +188,7 @@ export const mergeAccounts = (
type AccountListMenuProps = {
onClose: () => void;
+ privacyMode?: boolean;
showAccountCreation?: boolean;
accountListItemProps?: object;
allowedAccountTypes?: KeyringAccountType[];
@@ -195,6 +196,7 @@ type AccountListMenuProps = {
export const AccountListMenu = ({
onClose,
+ privacyMode = false,
showAccountCreation = true,
accountListItemProps,
allowedAccountTypes = [
@@ -644,6 +646,7 @@ export const AccountListMenu = ({
isHidden={Boolean(account.hidden)}
currentTabOrigin={currentTabOrigin}
isActive={Boolean(account.active)}
+ privacyMode={privacyMode}
{...accountListItemProps}
/>
diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js
index a0bb114409f6..7e2569ffaee3 100644
--- a/ui/components/ui/currency-display/currency-display.component.js
+++ b/ui/components/ui/currency-display/currency-display.component.js
@@ -1,10 +1,8 @@
import React from 'react';
-import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay';
import { EtherDenomination } from '../../../../shared/constants/common';
-import { getPreferences } from '../../../selectors';
import { SensitiveText, Box } from '../../component-library';
import {
AlignItems,
@@ -35,9 +33,9 @@ export default function CurrencyDisplay({
textProps = {},
suffixProps = {},
isAggregatedFiatOverviewBalance = false,
+ privacyMode = false,
...props
}) {
- const { privacyMode } = useSelector(getPreferences);
const [title, parts] = useCurrencyDisplay(value, {
account,
displayValue,
@@ -125,6 +123,7 @@ const CurrencyDisplayPropTypes = {
textProps: PropTypes.object,
suffixProps: PropTypes.object,
isAggregatedFiatOverviewBalance: PropTypes.bool,
+ privacyMode: PropTypes.bool,
};
CurrencyDisplay.propTypes = CurrencyDisplayPropTypes;
diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js
index e26e17be9e23..83e707c30f85 100644
--- a/ui/pages/routes/routes.component.js
+++ b/ui/pages/routes/routes.component.js
@@ -138,6 +138,7 @@ export default class Routes extends Component {
history: PropTypes.object,
location: PropTypes.object,
autoLockTimeLimit: PropTypes.number,
+ privacyMode: PropTypes.bool,
pageChanged: PropTypes.func.isRequired,
browserEnvironmentOs: PropTypes.string,
browserEnvironmentBrowser: PropTypes.string,
@@ -417,6 +418,7 @@ export default class Routes extends Component {
switchedNetworkDetails,
clearSwitchedNetworkDetails,
clearEditedNetwork,
+ privacyMode,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
isShowKeyringSnapRemovalResultModal,
hideShowKeyringSnapRemovalResultModal,
@@ -494,7 +496,10 @@ export default class Routes extends Component {
///: END:ONLY_INCLUDE_IF
}
{isAccountMenuOpen ? (
- toggleAccountMenu()} />
+ toggleAccountMenu()}
+ privacyMode={privacyMode}
+ />
) : null}
{isNetworkMenuOpen ? (
Date: Thu, 7 Nov 2024 17:34:47 -0800
Subject: [PATCH 11/18] build: update yarn to v4.5.1 (#28365)
## **Description**
Updates yarn to the latest v4.5.1, and adjusts LavaMoat policies to
accommodate.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28365?quickstart=1)
---
.../generate-attributions/package.json | 2 +-
lavamoat/build-system/policy.json | 20 ++-----------------
package.json | 2 +-
3 files changed, 4 insertions(+), 20 deletions(-)
diff --git a/development/generate-attributions/package.json b/development/generate-attributions/package.json
index 92bf1a5153c2..5778cbd0c634 100644
--- a/development/generate-attributions/package.json
+++ b/development/generate-attributions/package.json
@@ -9,7 +9,7 @@
},
"engines": {
"node": ">= 20",
- "yarn": "^4.4.1"
+ "yarn": "^4.5.1"
},
"lavamoat": {
"allowScripts": {
diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json
index 5a607452526c..2d6b7ce3ad14 100644
--- a/lavamoat/build-system/policy.json
+++ b/lavamoat/build-system/policy.json
@@ -3004,7 +3004,7 @@
"eslint-plugin-prettier": true,
"eslint-plugin-react": true,
"eslint-plugin-react-hooks": true,
- "eslint>@eslint/eslintrc>ajv": true,
+ "eslint>ajv": true,
"eslint>globals": true,
"eslint>ignore": true,
"eslint>minimatch": true,
@@ -3012,17 +3012,6 @@
"nock>debug": true
}
},
- "eslint>@eslint/eslintrc>ajv": {
- "globals": {
- "console": true
- },
- "packages": {
- "@metamask/snaps-utils>fast-json-stable-stringify": true,
- "eslint>@eslint/eslintrc>ajv>json-schema-traverse": true,
- "eslint>fast-deep-equal": true,
- "uri-js": true
- }
- },
"eslint>@eslint/eslintrc>import-fresh": {
"builtin": {
"path.dirname": true
@@ -8491,17 +8480,12 @@
"process.stdout.write": true
},
"packages": {
+ "eslint>ajv": true,
"lodash": true,
- "stylelint>table>ajv": true,
"stylelint>table>slice-ansi": true,
"stylelint>table>string-width": true
}
},
- "stylelint>table>ajv": {
- "packages": {
- "eslint>fast-deep-equal": true
- }
- },
"stylelint>table>slice-ansi": {
"packages": {
"stylelint>table>slice-ansi>ansi-styles": true,
diff --git a/package.json b/package.json
index c612dd76fb20..338e3eaa2068 100644
--- a/package.json
+++ b/package.json
@@ -751,5 +751,5 @@
"jest-preview": false
}
},
- "packageManager": "yarn@4.4.1"
+ "packageManager": "yarn@4.5.1"
}
From 04dd7d318ec039640960f1fcd4ba26acedb7940e Mon Sep 17 00:00:00 2001
From: Matthew Walsh
Date: Fri, 8 Nov 2024 10:17:15 +0000
Subject: [PATCH 12/18] fix: gas limit estimation (#28327)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Upgrade transaction controller to fix gas limit estimation on specific
networks.
## **Related issues**
Fixes: #28307 #28175
## **Manual testing steps**
See issue.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
package.json | 2 +-
test/e2e/flask/user-operations.spec.ts | 6 +++--
.../tests/transaction/edit-gas-fee.spec.js | 16 +++++------
.../transaction/multiple-transactions.spec.js | 27 ++++++++-----------
yarn.lock | 12 ++++-----
5 files changed, 29 insertions(+), 34 deletions(-)
diff --git a/package.json b/package.json
index 338e3eaa2068..13bddaa69287 100644
--- a/package.json
+++ b/package.json
@@ -347,7 +347,7 @@
"@metamask/snaps-sdk": "^6.10.0",
"@metamask/snaps-utils": "^8.5.1",
"@metamask/solana-wallet-snap": "^0.1.9",
- "@metamask/transaction-controller": "^38.1.0",
+ "@metamask/transaction-controller": "^38.3.0",
"@metamask/user-operation-controller": "^13.0.0",
"@metamask/utils": "^10.0.1",
"@ngraveio/bc-ur": "^1.1.12",
diff --git a/test/e2e/flask/user-operations.spec.ts b/test/e2e/flask/user-operations.spec.ts
index be7141444c97..7512e0b563c9 100644
--- a/test/e2e/flask/user-operations.spec.ts
+++ b/test/e2e/flask/user-operations.spec.ts
@@ -256,7 +256,8 @@ describe('User Operations', function () {
from: ERC_4337_ACCOUNT,
to: GANACHE_ACCOUNT,
value: convertETHToHexGwei(1),
- data: '0x',
+ maxFeePerGas: '0x0',
+ maxPriorityFeePerGas: '0x0',
});
await confirmTransaction(driver);
@@ -294,7 +295,8 @@ describe('User Operations', function () {
from: ERC_4337_ACCOUNT,
to: GANACHE_ACCOUNT,
value: convertETHToHexGwei(1),
- data: '0x',
+ maxFeePerGas: '0x0',
+ maxPriorityFeePerGas: '0x0',
});
await confirmTransaction(driver);
diff --git a/test/e2e/tests/transaction/edit-gas-fee.spec.js b/test/e2e/tests/transaction/edit-gas-fee.spec.js
index 85ae4da3a31f..918831f8f3ad 100644
--- a/test/e2e/tests/transaction/edit-gas-fee.spec.js
+++ b/test/e2e/tests/transaction/edit-gas-fee.spec.js
@@ -1,11 +1,11 @@
const { strict: assert } = require('assert');
const {
createInternalTransaction,
+ createDappTransaction,
} = require('../../page-objects/flows/transaction');
const {
withFixtures,
- openDapp,
unlockWallet,
generateGanacheOptions,
WINDOW_TITLES,
@@ -172,11 +172,9 @@ describe('Editing Confirm Transaction', function () {
// login to extension
await unlockWallet(driver);
- // open dapp and connect
- await openDapp(driver);
- await driver.clickElement({
- text: 'Send EIP 1559 Transaction',
- tag: 'button',
+ await createDappTransaction(driver, {
+ maxFeePerGas: '0x2000000000',
+ maxPriorityFeePerGas: '0x1000000000',
});
// check transaction in extension popup
@@ -198,12 +196,12 @@ describe('Editing Confirm Transaction', function () {
'.currency-display-component__text',
);
const transactionAmount = transactionAmounts[0];
- assert.equal(await transactionAmount.getText(), '0');
+ assert.equal(await transactionAmount.getText(), '0.001');
// has correct updated value on the confirm screen the transaction
await driver.waitForSelector({
css: '.currency-display-component__text',
- text: '0.00021',
+ text: '0.00185144',
});
// confirms the transaction
@@ -227,7 +225,7 @@ describe('Editing Confirm Transaction', function () {
'[data-testid="transaction-list-item-primary-currency"]',
);
assert.equal(txValues.length, 1);
- assert.ok(/-0\s*ETH/u.test(await txValues[0].getText()));
+ assert.ok(/-0.001\s*ETH/u.test(await txValues[0].getText()));
},
);
});
diff --git a/test/e2e/tests/transaction/multiple-transactions.spec.js b/test/e2e/tests/transaction/multiple-transactions.spec.js
index 8f1318c31e3b..4d913cb07edb 100644
--- a/test/e2e/tests/transaction/multiple-transactions.spec.js
+++ b/test/e2e/tests/transaction/multiple-transactions.spec.js
@@ -26,19 +26,13 @@ describe('Multiple transactions', function () {
// initiates a transaction from the dapp
await openDapp(driver);
// creates first transaction
- await driver.clickElement({
- text: 'Send EIP 1559 Transaction',
- tag: 'button',
- });
+ await createDappTransaction(driver);
await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
// creates second transaction
- await driver.clickElement({
- text: 'Send EIP 1559 Transaction',
- tag: 'button',
- });
+ await createDappTransaction(driver);
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
// confirms second transaction
@@ -94,19 +88,13 @@ describe('Multiple transactions', function () {
// initiates a transaction from the dapp
await openDapp(driver);
// creates first transaction
- await driver.clickElement({
- text: 'Send EIP 1559 Transaction',
- tag: 'button',
- });
+ await createDappTransaction(driver);
await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
// creates second transaction
- await driver.clickElement({
- text: 'Send EIP 1559 Transaction',
- tag: 'button',
- });
+ await createDappTransaction(driver);
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
// rejects second transaction
@@ -141,3 +129,10 @@ describe('Multiple transactions', function () {
);
});
});
+
+async function createDappTransaction(driver) {
+ await driver.clickElement({
+ text: 'Send EIP 1559 Without Gas',
+ tag: 'button',
+ });
+}
diff --git a/yarn.lock b/yarn.lock
index 4352ef21767a..bb91a8129ac2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6458,9 +6458,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/transaction-controller@npm:^38.1.0":
- version: 38.1.0
- resolution: "@metamask/transaction-controller@npm:38.1.0"
+"@metamask/transaction-controller@npm:^38.3.0":
+ version: 38.3.0
+ resolution: "@metamask/transaction-controller@npm:38.3.0"
dependencies:
"@ethereumjs/common": "npm:^3.2.0"
"@ethereumjs/tx": "npm:^4.2.0"
@@ -6469,7 +6469,7 @@ __metadata:
"@ethersproject/contracts": "npm:^5.7.0"
"@ethersproject/providers": "npm:^5.7.0"
"@metamask/base-controller": "npm:^7.0.2"
- "@metamask/controller-utils": "npm:^11.4.1"
+ "@metamask/controller-utils": "npm:^11.4.2"
"@metamask/eth-query": "npm:^4.0.0"
"@metamask/metamask-eth-abis": "npm:^3.1.1"
"@metamask/nonce-tracker": "npm:^6.0.0"
@@ -6487,7 +6487,7 @@ __metadata:
"@metamask/approval-controller": ^7.0.0
"@metamask/gas-fee-controller": ^22.0.0
"@metamask/network-controller": ^22.0.0
- checksum: 10/c1bdca52bbbce42a76ec9c640197534ec6c223b0f5d5815acfa53490dc1175850ea9aeeb6ae3c5ec34218f0bdbbbeb3e8731e2552aa9411e3ed7798a5dea8ab5
+ checksum: 10/f4e8e3a1a31e3e62b0d1a59bbe15ebfa4dc3e4cf077fb95c1815c00661c60ef4676046c49f57eab9749cd31d3e55ac3fed7bc247e3f5a3d459f2dcb03998633d
languageName: node
linkType: hard
@@ -26590,7 +26590,7 @@ __metadata:
"@metamask/solana-wallet-snap": "npm:^0.1.9"
"@metamask/test-bundler": "npm:^1.0.0"
"@metamask/test-dapp": "npm:8.13.0"
- "@metamask/transaction-controller": "npm:^38.1.0"
+ "@metamask/transaction-controller": "npm:^38.3.0"
"@metamask/user-operation-controller": "npm:^13.0.0"
"@metamask/utils": "npm:^10.0.1"
"@ngraveio/bc-ur": "npm:^1.1.12"
From c5643837734ce1cbdd0b2e55b5c35aa5accef816 Mon Sep 17 00:00:00 2001
From: Matthew Walsh
Date: Fri, 8 Nov 2024 10:25:30 +0000
Subject: [PATCH 13/18] refactor: remove global network usage from transaction
confirmations (#28236)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Remove usages of global network selectors from transaction confirmation
React components and hooks.
Specifically:
- Remove usages of the following selectors:
- `getConversionRate`
- `getCurrentChainId`
- `getNativeCurrency`
- `getNetworkIdentifier`
- `getNftContracts`
- `getNfts`
- `getProviderConfig`
- `getRpcPrefsForCurrentProvider`
- Add new selectors:
- `selectConversionRateByChainId`
- `selectNftsByChainId`
- `selectNftContractsByChainId`
- `selectNetworkIdentifierByChainId`
- Add ESLint rule to prevent further usage of global network selectors
in confirmations directory.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28236?quickstart=1)
## **Related issues**
Fixes:
[#3469](https://github.com/MetaMask/MetaMask-planning/issues/3469)
[#3373](https://github.com/MetaMask/MetaMask-planning/issues/3373)
[#3486](https://github.com/MetaMask/MetaMask-planning/issues/3486)
[#3487](https://github.com/MetaMask/MetaMask-planning/issues/3487)
## **Manual testing steps**
Full regression of all transaction confirmations and related
functionality.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---------
Co-authored-by: Jyoti Puri
---
.eslintrc.js | 25 +++++++++++
.storybook/preview.js | 5 ++-
.../confirmations/contract-interaction.ts | 3 +-
.../advanced-gas-fee-defaults.js | 14 +++---
.../advanced-gas-fee-defaults.test.js | 4 +-
.../confirm-hexdata/confirm-hexdata.test.js | 18 +++++++-
.../confirm-page-container.component.js | 12 +++--
.../confirm-page-container.container.js | 3 --
.../confirm-subtitle/confirm-subtitle.test.js | 5 ++-
.../approve-static-simulation.tsx | 1 +
.../confirm/info/approve/approve.tsx | 1 +
.../edit-spending-cap-modal.tsx | 1 +
.../approve/spending-cap/spending-cap.tsx | 1 +
.../confirm/info/hooks/use-token-values.ts | 1 +
.../confirm/info/hooks/useFeeCalculations.ts | 12 +++--
.../advanced-details.stories.tsx | 2 +-
.../native-send-heading.tsx | 13 ++++--
.../nft-send-heading/nft-send-heading.tsx | 2 +
.../transaction-data.stories.tsx | 1 -
.../transaction-details.stories.tsx | 3 --
.../title/hooks/useCurrentSpendingCap.ts | 8 +++-
.../contract-details-modal.js | 2 +-
.../contract-token-values.js | 8 ++--
.../useBalanceChanges.test.ts | 17 ++++---
.../simulation-details/useBalanceChanges.ts | 11 +++--
.../transaction-alerts/transaction-alerts.js | 11 ++++-
.../transaction-alerts.stories.js | 42 ++++++-----------
.../transaction-alerts.test.js | 40 ++++++++++++++++-
.../confirm-approve-content.component.js | 12 ++---
.../confirm-approve/confirm-approve.js | 26 ++++++-----
.../confirm-send-token/confirm-send-token.js | 20 ++++++---
.../confirm-token-transaction-base.js | 37 +++++++++------
.../confirm-transaction-base.component.js | 11 +----
.../confirm-transaction-base.container.js | 31 +++++++++----
.../confirm-transaction-base.test.js | 41 -----------------
.../confirm-token-transaction-switch.js | 4 +-
.../contract-interaction.stories.tsx | 19 ++++----
.../confirmations/confirm/stories/utils.tsx | 21 +--------
ui/pages/confirmations/hooks/test-utils.js | 12 ++---
.../confirmations/hooks/useAssetDetails.js | 13 ++++--
.../hooks/useTransactionFunctionType.js | 16 +++++--
.../hooks/useTransactionFunctionType.test.js | 27 ++++++++---
.../confirmations/hooks/useTransactionInfo.js | 3 +-
.../hooks/useTransactionInfo.test.js | 7 ++-
.../send/gas-display/gas-display.js | 18 ++++----
.../token-allowance.test.js.snap | 14 +++---
.../token-allowance/token-allowance.js | 28 +++++++++---
.../token-allowance/token-allowance.test.js | 2 +-
ui/selectors/selectors.js | 45 +++++++++++++++++++
49 files changed, 416 insertions(+), 257 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 846158a741ef..4aa9ef688592 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -472,5 +472,30 @@ module.exports = {
'@metamask/design-tokens/color-no-hex': 'off',
},
},
+ {
+ files: ['ui/pages/confirmations/**/*.{js,ts,tsx}'],
+ rules: {
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector: `ImportSpecifier[imported.name=/${[
+ 'getConversionRate',
+ 'getCurrentChainId',
+ 'getNativeCurrency',
+ 'getNetworkIdentifier',
+ 'getNftContracts',
+ 'getNfts',
+ 'getProviderConfig',
+ 'getRpcPrefsForCurrentProvider',
+ 'getUSDConversionRate',
+ 'isCurrentProviderCustom',
+ ]
+ .map((method) => `(${method})`)
+ .join('|')}/]`,
+ message: 'Avoid using global network selectors in confirmations',
+ },
+ ],
+ },
+ },
],
};
diff --git a/.storybook/preview.js b/.storybook/preview.js
index b46c91339273..525c364f2072 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -15,6 +15,7 @@ import MetaMetricsProviderStorybook from './metametrics';
import testData from './test-data.js';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
+import { MemoryRouter } from 'react-router-dom';
import { setBackgroundConnection } from '../ui/store/background-connection';
import { metamaskStorybookTheme } from './metamask-storybook-theme';
import { DocsContainer } from '@storybook/addon-docs';
@@ -147,7 +148,7 @@ const metamaskDecorator = (story, context) => {
return (
-
+
{
-
+
);
};
diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts
index 49a6e1aad1ab..cbe5dd2bd2ba 100644
--- a/test/data/confirmations/contract-interaction.ts
+++ b/test/data/confirmations/contract-interaction.ts
@@ -1,4 +1,5 @@
import {
+ CHAIN_IDS,
SimulationData,
TransactionMeta,
TransactionStatus,
@@ -18,7 +19,7 @@ export const CONTRACT_INTERACTION_SENDER_ADDRESS =
export const DEPOSIT_METHOD_DATA = '0xd0e30db0';
-export const CHAIN_ID = '0xaa36a7';
+export const CHAIN_ID = CHAIN_IDS.GOERLI;
export const genUnapprovedContractInteractionConfirmation = ({
address = CONTRACT_INTERACTION_SENDER_ADDRESS,
diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js
index a52b4c16f893..106984363699 100644
--- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js
+++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js
@@ -11,8 +11,7 @@ import {
} from '../../../../../helpers/constants/design-system';
import {
getAdvancedGasFeeValues,
- getCurrentChainId,
- getNetworkIdentifier,
+ selectNetworkIdentifierByChainId,
} from '../../../../../selectors';
import { setAdvancedGasFee } from '../../../../../store/actions';
import { useGasFeeContext } from '../../../../../contexts/gasFee';
@@ -35,11 +34,14 @@ const AdvancedGasFeeDefaults = () => {
10,
).toString();
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
- // This will need to use a different chainId in multinetwork
- const chainId = useSelector(getCurrentChainId);
- const networkIdentifier = useSelector(getNetworkIdentifier);
const { updateTransactionEventFragment } = useTransactionEventFragment();
- const { editGasMode } = useGasFeeContext();
+ const { editGasMode, transaction } = useGasFeeContext();
+ const { chainId } = transaction;
+
+ const networkIdentifier = useSelector((state) =>
+ selectNetworkIdentifierByChainId(state, chainId),
+ );
+
const [isDefaultSettingsSelected, setDefaultSettingsSelected] = useState(
Boolean(advancedGasFeeValues) &&
advancedGasFeeValues.maxBaseFee === maxBaseFee &&
diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js
index 5e330a8b90c7..07c7fc8faaab 100644
--- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js
+++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js
@@ -21,6 +21,7 @@ import { mockNetworkState } from '../../../../../../test/stub/networks';
import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults';
const TEXT_SELECTOR = 'Save these values as my default for the Goerli network.';
+const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI;
jest.mock('../../../../../store/actions', () => ({
gasFeeStartPollingByNetworkClientId: jest
@@ -43,7 +44,7 @@ const render = async (defaultGasParams, contextParams) => {
metamask: {
...mockState.metamask,
...defaultGasParams,
- ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }),
+ ...mockNetworkState({ chainId: CHAIN_ID_MOCK }),
accounts: {
[mockSelectedInternalAccount.address]: {
address: mockSelectedInternalAccount.address,
@@ -71,6 +72,7 @@ const render = async (defaultGasParams, contextParams) => {
(result = renderWithProvider(
{
- const store = configureStore(mockState);
+ const store = configureStore(STATE_MOCK);
it('should render function type', async () => {
const { findByText } = renderWithProvider(
{
const { container } = renderWithProvider(
{
const { container } = renderWithProvider(
{
const { getByText } = renderWithProvider(
{
useState(false);
const isBuyableChain = useSelector(getIsNativeTokenBuyable);
const contact = useSelector((state) => getAddressBookEntry(state, toAddress));
- const networkIdentifier = useSelector(getNetworkIdentifier);
const defaultToken = useSelector(getSwapsDefaultToken);
const accountBalance = defaultToken.string;
const internalAccounts = useSelector(getInternalAccounts);
@@ -139,8 +138,13 @@ const ConfirmPageContainer = (props) => {
const shouldDisplayWarning =
contentComponent && disabled && (errorKey || errorMessage);
- const networkName =
- NETWORK_TO_NAME_MAP[currentTransaction.chainId] || networkIdentifier;
+ const { chainId } = currentTransaction;
+
+ const networkIdentifier = useSelector((state) =>
+ selectNetworkIdentifierByChainId(state, chainId),
+ );
+
+ const networkName = NETWORK_TO_NAME_MAP[chainId] || networkIdentifier;
const fetchCollectionBalance = useCallback(async () => {
const tokenBalance = await fetchTokenBalance(
diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js
index db07ad4117e7..23eaa43d44ab 100644
--- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js
+++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js
@@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import {
getAddressBookEntry,
- getNetworkIdentifier,
getSwapsDefaultToken,
getMetadataContractName,
getAccountName,
@@ -12,7 +11,6 @@ import ConfirmPageContainer from './confirm-page-container.component';
function mapStateToProps(state, ownProps) {
const to = ownProps.toAddress;
const contact = getAddressBookEntry(state, to);
- const networkIdentifier = getNetworkIdentifier(state);
const defaultToken = getSwapsDefaultToken(state);
const accountBalance = defaultToken.string;
const internalAccounts = getInternalAccounts(state);
@@ -26,7 +24,6 @@ function mapStateToProps(state, ownProps) {
toMetadataName,
recipientIsOwnedAccount: Boolean(ownedAccountName),
to,
- networkIdentifier,
accountBalance,
};
}
diff --git a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js
index baa2d112b671..fa00f0275350 100644
--- a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js
+++ b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js
@@ -1,11 +1,11 @@
import React from 'react';
import { ERC1155, ERC721 } from '@metamask/controller-utils';
+import { CHAIN_IDS } from '@metamask/transaction-controller';
import mockState from '../../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import configureStore from '../../../../store/store';
import { getSelectedInternalAccountFromMockState } from '../../../../../test/jest/mocks';
-import { getProviderConfig } from '../../../../ducks/metamask/metamask';
import ConfirmSubTitle from './confirm-subtitle';
const mockSelectedInternalAccount =
@@ -51,7 +51,7 @@ describe('ConfirmSubTitle', () => {
mockState.metamask.preferences.showFiatInTestnets = false;
mockState.metamask.allNftContracts = {
[mockSelectedInternalAccount.address]: {
- [getProviderConfig(mockState).chainId]: [{ address: '0x9' }],
+ [CHAIN_IDS.GOERLI]: [{ address: '0x9' }],
},
};
store = configureStore(mockState);
@@ -59,6 +59,7 @@ describe('ConfirmSubTitle', () => {
const { findByText } = renderWithProvider(
{
transactionMeta?.txParams?.to,
transactionMeta?.txParams?.from,
transactionMeta?.txParams?.data,
+ transactionMeta?.chainId,
);
const decimals = initialDecimals || '0';
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx
index fa1766a8aa72..d1d9fbc08364 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx
@@ -30,6 +30,7 @@ const ApproveInfo = () => {
transactionMeta.txParams.to,
transactionMeta.txParams.from,
transactionMeta.txParams.data,
+ transactionMeta.chainId,
);
const { spendingCap, pending } = useApproveTokenSimulation(
diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx
index 961e63ae8f92..1eb0ac2cde05 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx
@@ -54,6 +54,7 @@ export const EditSpendingCapModal = ({
transactionMeta.txParams.to,
transactionMeta.txParams.from,
transactionMeta.txParams.data,
+ transactionMeta.chainId,
);
const accountBalance = calcTokenAmount(
diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx
index 638ff638844c..8e7b522f050a 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx
@@ -87,6 +87,7 @@ export const SpendingCap = ({
transactionMeta.txParams.to,
transactionMeta.txParams.from,
transactionMeta.txParams.data,
+ transactionMeta.chainId,
);
const accountBalance = calcTokenAmount(
diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts
index f416282ee4ef..087a17c473c6 100644
--- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts
+++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts
@@ -17,6 +17,7 @@ export const useTokenValues = (transactionMeta: TransactionMeta) => {
transactionMeta.txParams.to,
transactionMeta.txParams.from,
transactionMeta.txParams.data,
+ transactionMeta.chainId,
);
const decodedResponse = useDecodedTransactionData();
diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts
index 70bd2c0e3af2..5b54dcc710b8 100644
--- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts
+++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts
@@ -12,10 +12,12 @@ import {
multiplyHexes,
} from '../../../../../../../shared/modules/conversion.utils';
import { Numeric } from '../../../../../../../shared/modules/Numeric';
-import { getConversionRate } from '../../../../../../ducks/metamask/metamask';
import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter';
import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates';
-import { getCurrentCurrency } from '../../../../../../selectors';
+import {
+ getCurrentCurrency,
+ selectConversionRateByChainId,
+} from '../../../../../../selectors';
import { getMultichainNetwork } from '../../../../../../selectors/multichain';
import { HEX_ZERO } from '../shared/constants';
import { useEIP1559TxFees } from './useEIP1559TxFees';
@@ -30,9 +32,13 @@ const EMPTY_FEES = {
export function useFeeCalculations(transactionMeta: TransactionMeta) {
const currentCurrency = useSelector(getCurrentCurrency);
- const conversionRate = useSelector(getConversionRate);
+ const { chainId } = transactionMeta;
const fiatFormatter = useFiatFormatter();
+ const conversionRate = useSelector((state) =>
+ selectConversionRateByChainId(state, chainId),
+ );
+
const multichainNetwork = useSelector(getMultichainNetwork);
const ticker = multichainNetwork?.network?.ticker;
diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx
index 8654400a0bfa..aa9ec6cdc87c 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx
@@ -22,6 +22,6 @@ const Story = {
export default Story;
-export const DefaultStory = () => ;
+export const DefaultStory = () => ;
DefaultStory.storyName = 'Default';
diff --git a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx
index bd5c7ba8b4f2..9eb70a856464 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx
@@ -14,7 +14,6 @@ import {
} from '../../../../../../../components/component-library';
import Tooltip from '../../../../../../../components/ui/tooltip';
import { getIntlLocale } from '../../../../../../../ducks/locale/locale';
-import { getConversionRate } from '../../../../../../../ducks/metamask/metamask';
import {
AlignItems,
Display,
@@ -25,7 +24,10 @@ import {
} from '../../../../../../../helpers/constants/design-system';
import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay';
import { useFiatFormatter } from '../../../../../../../hooks/useFiatFormatter';
-import { getPreferences } from '../../../../../../../selectors';
+import {
+ getPreferences,
+ selectConversionRateByChainId,
+} from '../../../../../../../selectors';
import { getMultichainNetwork } from '../../../../../../../selectors/multichain';
import { useConfirmContext } from '../../../../../context/confirm';
import {
@@ -38,11 +40,16 @@ const NativeSendHeading = () => {
const { currentConfirmation: transactionMeta } =
useConfirmContext();
+ const { chainId } = transactionMeta;
+
const nativeAssetTransferValue = new BigNumber(
transactionMeta.txParams.value as string,
).dividedBy(new BigNumber(10).pow(18));
- const conversionRate = useSelector(getConversionRate);
+ const conversionRate = useSelector((state) =>
+ selectConversionRateByChainId(state, chainId),
+ );
+
const fiatValue =
conversionRate &&
nativeAssetTransferValue &&
diff --git a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx
index 006613e206c9..2f36d10ce42c 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx
@@ -24,11 +24,13 @@ const NFTSendHeading = () => {
const tokenAddress = transactionMeta.txParams.to;
const userAddress = transactionMeta.txParams.from;
const { data } = transactionMeta.txParams;
+ const { chainId } = transactionMeta;
const { assetName, tokenImage, tokenId } = useAssetDetails(
tokenAddress,
userAddress,
data,
+ chainId,
);
const TokenImage = ;
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx
index 091185ae23f3..105838af78c2 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx
@@ -24,7 +24,6 @@ function getStore(transactionData?: string, to?: string) {
const confirmation = {
...confirmationTemplate,
- chainId: '0x1',
txParams: {
...confirmationTemplate.txParams,
to: to ?? confirmationTemplate.txParams.to,
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx
index dfec9c8ef4d0..f37317c230c4 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx
@@ -5,7 +5,6 @@ import {
PAYMASTER_AND_DATA,
genUnapprovedContractInteractionConfirmation,
} from '../../../../../../../../test/data/confirmations/contract-interaction';
-import mockState from '../../../../../../../../test/data/mock-state.json';
import { getMockConfirmStateForTransaction } from '../../../../../../../../test/data/confirmations/helper';
import configureStore from '../../../../../../../store/store';
import { ConfirmContextProvider } from '../../../../../context/confirm';
@@ -20,9 +19,7 @@ function getStore() {
return configureStore(
getMockConfirmStateForTransaction(confirmation, {
metamask: {
- ...mockState.metamask,
preferences: {
- ...mockState.metamask.preferences,
petnamesEnabled: true,
},
userOperations: {
diff --git a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts
index 5f588a971561..b50948259734 100644
--- a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts
+++ b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts
@@ -32,8 +32,14 @@ export function useCurrentSpendingCap(currentConfirmation: Confirmation) {
const txParamsData = isTxWithSpendingCap
? currentConfirmation.txParams.data
: null;
+ const chainId = isTxWithSpendingCap ? currentConfirmation.chainId : null;
- const { decimals } = useAssetDetails(txParamsTo, txParamsFrom, txParamsData);
+ const { decimals } = useAssetDetails(
+ txParamsTo,
+ txParamsFrom,
+ txParamsData,
+ chainId,
+ );
const { spendingCap, pending } = useApproveTokenSimulation(
currentConfirmation as TransactionMeta,
diff --git a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js
index 795401673691..f433bf4caee3 100644
--- a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js
+++ b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js
@@ -338,7 +338,7 @@ ContractDetailsModal.propTypes = {
*/
chainId: PropTypes.string,
/**
- * Block explorer URL of the current network
+ * Block explorer URL for the contract chain
*/
blockExplorerUrl: PropTypes.string,
/**
diff --git a/ui/pages/confirmations/components/contract-token-values/contract-token-values.js b/ui/pages/confirmations/components/contract-token-values/contract-token-values.js
index 6858689f77d0..0e8890b607ec 100644
--- a/ui/pages/confirmations/components/contract-token-values/contract-token-values.js
+++ b/ui/pages/confirmations/components/contract-token-values/contract-token-values.js
@@ -25,7 +25,7 @@ export default function ContractTokenValues({
address,
tokenName,
chainId,
- rpcPrefs,
+ blockExplorerUrl,
}) {
const t = useI18nContext();
const [copied, handleCopy] = useCopyToClipboard();
@@ -69,7 +69,7 @@ export default function ContractTokenValues({
address,
chainId,
{
- blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ blockExplorerUrl: blockExplorerUrl ?? null,
},
null,
);
@@ -98,7 +98,7 @@ ContractTokenValues.propTypes = {
*/
chainId: PropTypes.string,
/**
- * RPC prefs
+ * URL for the block explorer
*/
- rpcPrefs: PropTypes.object,
+ blockExplorerUrl: PropTypes.string,
};
diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts
index f77774c6ec4a..5f2e1dcbfd16 100644
--- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts
+++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts
@@ -6,10 +6,10 @@ import {
} from '@metamask/transaction-controller';
import { BigNumber } from 'bignumber.js';
import { TokenStandard } from '../../../../../shared/constants/transaction';
-import { getConversionRate } from '../../../../ducks/metamask/metamask';
import { getTokenStandardAndDetails } from '../../../../store/actions';
import { fetchTokenExchangeRates } from '../../../../helpers/utils/util';
import { memoizedGetTokenStandardAndDetails } from '../../utils/token';
+import { selectConversionRateByChainId } from '../../../../selectors';
import { useBalanceChanges } from './useBalanceChanges';
import { FIAT_UNAVAILABLE } from './types';
@@ -17,13 +17,10 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn((selector) => selector()),
}));
-jest.mock('../../../../ducks/metamask/metamask', () => ({
- getConversionRate: jest.fn(),
-}));
-
jest.mock('../../../../selectors', () => ({
getCurrentChainId: jest.fn(),
getCurrentCurrency: jest.fn(),
+ selectConversionRateByChainId: jest.fn(),
}));
jest.mock('../../../../helpers/utils/util', () => ({
@@ -34,7 +31,9 @@ jest.mock('../../../../store/actions', () => ({
getTokenStandardAndDetails: jest.fn(),
}));
-const mockGetConversionRate = getConversionRate as jest.Mock;
+const mockSelectConversionRateByChainId = jest.mocked(
+ selectConversionRateByChainId,
+);
const mockGetTokenStandardAndDetails = getTokenStandardAndDetails as jest.Mock;
const mockFetchTokenExchangeRates = fetchTokenExchangeRates as jest.Mock;
@@ -85,7 +84,7 @@ describe('useBalanceChanges', () => {
}
return Promise.reject(new Error('Unable to determine token standard'));
});
- mockGetConversionRate.mockReturnValue(ETH_TO_FIAT_RATE);
+ mockSelectConversionRateByChainId.mockReturnValue(ETH_TO_FIAT_RATE);
mockFetchTokenExchangeRates.mockResolvedValue({
[ERC20_TOKEN_ADDRESS_1_MOCK]: ERC20_TO_FIAT_RATE_1_MOCK,
[ERC20_TOKEN_ADDRESS_2_MOCK]: ERC20_TO_FIAT_RATE_2_MOCK,
@@ -344,7 +343,7 @@ describe('useBalanceChanges', () => {
});
it('handles native fiat rate with more than 15 significant digits', async () => {
- mockGetConversionRate.mockReturnValue(0.1234567890123456);
+ mockSelectConversionRateByChainId.mockReturnValue(0.1234567890123456);
const { result, waitForNextUpdate } = setupHook({
...dummyBalanceChange,
difference: DIFFERENCE_ETH_MOCK,
@@ -357,7 +356,7 @@ describe('useBalanceChanges', () => {
});
it('handles unavailable native fiat rate', async () => {
- mockGetConversionRate.mockReturnValue(null);
+ mockSelectConversionRateByChainId.mockReturnValue(null);
const { result, waitForNextUpdate } = setupHook({
...dummyBalanceChange,
difference: DIFFERENCE_ETH_MOCK,
diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts
index 1bbc9cb5eec8..666682c95c76 100644
--- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts
+++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts
@@ -10,8 +10,10 @@ import { BigNumber } from 'bignumber.js';
import { ContractExchangeRates } from '@metamask/assets-controllers';
import { useAsyncResultOrThrow } from '../../../../hooks/useAsyncResult';
import { TokenStandard } from '../../../../../shared/constants/transaction';
-import { getConversionRate } from '../../../../ducks/metamask/metamask';
-import { getCurrentCurrency } from '../../../../selectors';
+import {
+ getCurrentCurrency,
+ selectConversionRateByChainId,
+} from '../../../../selectors';
import { fetchTokenExchangeRates } from '../../../../helpers/utils/util';
import { ERC20_DEFAULT_DECIMALS, fetchErc20Decimals } from '../../utils/token';
@@ -158,7 +160,10 @@ export const useBalanceChanges = ({
simulationData?: SimulationData;
}): { pending: boolean; value: BalanceChange[] } => {
const fiatCurrency = useSelector(getCurrentCurrency);
- const nativeFiatRate = useSelector(getConversionRate);
+
+ const nativeFiatRate = useSelector((state) =>
+ selectConversionRateByChainId(state, chainId),
+ );
const { nativeBalanceChange, tokenBalanceChanges = [] } =
simulationData ?? {};
diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js
index a641ecbab93a..5127003a81e8 100644
--- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js
+++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js
@@ -14,7 +14,10 @@ import {
} from '../../../../components/component-library';
import SimulationErrorMessage from '../simulation-error-message';
import { SEVERITIES } from '../../../../helpers/constants/design-system';
+// eslint-disable-next-line import/no-duplicates
+import { selectNetworkConfigurationByChainId } from '../../../../selectors';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
+// eslint-disable-next-line import/no-duplicates
import { submittedPendingTransactionsSelector } from '../../../../selectors';
import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
///: END:ONLY_INCLUDE_IF
@@ -22,7 +25,6 @@ import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
import { isSuspiciousResponse } from '../../../../../shared/modules/security-provider.utils';
import BlockaidBannerAlert from '../security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert';
import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message';
-import { getNativeCurrency } from '../../../../ducks/metamask/metamask';
import { parseStandardTokenTransactionData } from '../../../../../shared/modules/transaction.utils';
import { getTokenValueParam } from '../../../../../shared/lib/metamask-controller-utils';
import { QueuedRequestsBannerAlert } from '../../confirmation/components/queued-requests-banner-alert';
@@ -47,7 +49,12 @@ const TransactionAlerts = ({
///: END:ONLY_INCLUDE_IF
const t = useI18nContext();
- const nativeCurrency = useSelector(getNativeCurrency);
+ const { chainId } = txData;
+
+ const { nativeCurrency } = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+
const transactionData = txData.txParams.data;
const currentTokenSymbol = tokenSymbol || nativeCurrency;
let currentTokenAmount;
diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js
index ae46a4a83903..99a60a94e25d 100644
--- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js
+++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js
@@ -10,6 +10,8 @@ import { CHAIN_IDS } from '../../../../../shared/constants/network';
import { mockNetworkState } from '../../../../../test/stub/networks';
import TransactionAlerts from '.';
+const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET;
+
const mockSelectedInternalAccount =
getSelectedInternalAccountFromMockState(testData);
@@ -24,7 +26,7 @@ const customTransaction = ({
userFeeLevel: estimateUsed ? 'low' : 'medium',
blockNumber: `${10902987 + i}`,
id: 4678200543090545 + i,
- chainId: '0x1',
+ chainId: CHAIN_ID_MOCK,
status: 'confirmed',
time: 1600654021000,
txParams: {
@@ -48,23 +50,14 @@ const customTransaction = ({
};
// simulate gas fee state
-const customStore = ({
- supportsEIP1559,
- isNetworkBusy,
- pendingCount = 0,
-} = {}) => {
+const customStore = ({ supportsEIP1559, pendingCount = 0 } = {}) => {
const data = cloneDeep({
...testData,
metamask: {
...testData?.metamask,
- // isNetworkBusy
- gasFeeEstimates: {
- ...testData?.metamask?.gasFeeEstimates,
- networkCongestion: isNetworkBusy ? 1 : 0.1,
- },
// supportsEIP1559
...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
+ chainId: CHAIN_ID_MOCK,
metadata: {
EIPS: {
1559: Boolean(supportsEIP1559),
@@ -75,15 +68,12 @@ const customStore = ({
featureFlags: {
...testData?.metamask?.featureFlags,
},
- incomingTransactions: {
- ...testData?.metamask?.incomingTransactions,
- ...Object.fromEntries(
- Array.from({ length: pendingCount }).map((_, i) => {
- const transaction = customTransaction({ i, status: 'submitted' });
- return [transaction?.hash, transaction];
- }),
+ transactions: [
+ ...testData.metamask.transactions,
+ ...Array.from({ length: pendingCount }).map((_, i) =>
+ customTransaction({ i, status: 'submitted' }),
),
- },
+ ],
},
});
return configureStore(data);
@@ -99,6 +89,7 @@ export default {
args: {
userAcknowledgedGasMissing: false,
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -129,6 +120,7 @@ DefaultStory.storyName = 'Default';
DefaultStory.args = {
...DefaultStory.args,
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x0',
},
@@ -176,15 +168,6 @@ export const LowPriority = (args) => (
);
LowPriority.storyName = 'LowPriority';
-export const BusyNetwork = (args) => (
-
-
-
-
-
-);
-BusyNetwork.storyName = 'BusyNetwork';
-
export const SendingZeroAmount = (args) => (
@@ -195,6 +178,7 @@ export const SendingZeroAmount = (args) => (
SendingZeroAmount.storyName = 'SendingZeroAmount';
SendingZeroAmount.args = {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x0',
},
diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js
index 6ae1a59591dd..4695120fbae7 100644
--- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js
+++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js
@@ -1,6 +1,7 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import sinon from 'sinon';
+import { CHAIN_IDS } from '@metamask/transaction-controller';
import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider';
import { renderWithProvider } from '../../../../../test/jest';
import { submittedPendingTransactionsSelector } from '../../../../selectors/transactions';
@@ -9,6 +10,7 @@ import configureStore from '../../../../store/store';
import mockState from '../../../../../test/data/mock-state.json';
import * as txUtil from '../../../../../shared/modules/transaction.utils';
import * as metamaskControllerUtils from '../../../../../shared/lib/metamask-controller-utils';
+import { mockNetworkState } from '../../../../../test/stub/networks';
import TransactionAlerts from './transaction-alerts';
jest.mock('../../../../selectors/transactions', () => {
@@ -22,17 +24,28 @@ jest.mock('../../../../contexts/gasFee');
jest.mock('../../../../selectors/account-abstraction');
+const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET;
+
+const STATE_MOCK = {
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ ...mockNetworkState({
+ chainId: CHAIN_ID_MOCK,
+ }),
+ },
+};
+
function render({
componentProps = {},
useGasFeeContextValue = {},
submittedPendingTransactionsSelectorValue = null,
- mockedStore = mockState,
}) {
useGasFeeContext.mockReturnValue(useGasFeeContextValue);
submittedPendingTransactionsSelector.mockReturnValue(
submittedPendingTransactionsSelectorValue,
);
- const store = configureStore(mockedStore);
+ const store = configureStore(STATE_MOCK);
return renderWithProvider( , store);
}
@@ -41,6 +54,7 @@ describe('TransactionAlerts', () => {
const { getByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
securityAlertResponse: {
resultType: 'Malicious',
reason: 'blur_farming',
@@ -64,6 +78,7 @@ describe('TransactionAlerts', () => {
const { queryByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
securityProviderResponse: {
flagAsDangerous: '?',
reason: 'Some reason...',
@@ -89,6 +104,7 @@ describe('TransactionAlerts', () => {
const { queryByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
securityProviderResponse: {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITY.NOT_MALICIOUS,
},
@@ -118,6 +134,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -141,6 +158,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -160,6 +178,7 @@ describe('TransactionAlerts', () => {
componentProps: {
setUserAcknowledgedGasMissing,
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -181,6 +200,7 @@ describe('TransactionAlerts', () => {
componentProps: {
userAcknowledgedGasMissing: true,
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -199,6 +219,7 @@ describe('TransactionAlerts', () => {
const { queryByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -220,6 +241,7 @@ describe('TransactionAlerts', () => {
submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }],
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -242,6 +264,7 @@ describe('TransactionAlerts', () => {
],
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -261,6 +284,7 @@ describe('TransactionAlerts', () => {
submittedPendingTransactionsSelectorValue: [],
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -282,6 +306,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -301,6 +326,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -322,6 +348,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -345,6 +372,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -367,6 +395,7 @@ describe('TransactionAlerts', () => {
submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }],
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -388,6 +417,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -407,6 +437,7 @@ describe('TransactionAlerts', () => {
},
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
@@ -445,6 +476,7 @@ describe('TransactionAlerts', () => {
const { getByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x0',
},
@@ -461,6 +493,7 @@ describe('TransactionAlerts', () => {
const { getByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x0',
},
@@ -478,6 +511,7 @@ describe('TransactionAlerts', () => {
const { queryByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x5af3107a4000',
},
@@ -492,6 +526,7 @@ describe('TransactionAlerts', () => {
const { queryByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x0',
},
@@ -508,6 +543,7 @@ describe('TransactionAlerts', () => {
const { getByText } = render({
componentProps: {
txData: {
+ chainId: CHAIN_ID_MOCK,
txParams: {
value: '0x1',
},
diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
index ebd57c35a141..007aec372c62 100644
--- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
+++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
@@ -70,7 +70,7 @@ export default class ConfirmApproveContent extends Component {
fromAddressIsLedger: PropTypes.bool,
chainId: PropTypes.string,
tokenAddress: PropTypes.string,
- rpcPrefs: PropTypes.object,
+ blockExplorerUrl: PropTypes.string,
isContract: PropTypes.bool,
hexTransactionTotal: PropTypes.string,
hexMinimumTransactionFee: PropTypes.string,
@@ -375,10 +375,10 @@ export default class ConfirmApproveContent extends Component {
}
getTitleTokenDescription() {
- const { tokenId, tokenAddress, rpcPrefs, chainId, userAddress } =
+ const { tokenId, tokenAddress, blockExplorerUrl, chainId, userAddress } =
this.props;
const useBlockExplorer =
- rpcPrefs?.blockExplorerUrl ||
+ blockExplorerUrl ||
[...TEST_CHAINS, CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET].includes(
chainId,
);
@@ -393,7 +393,7 @@ export default class ConfirmApproveContent extends Component {
null,
userAddress,
{
- blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ blockExplorerUrl: blockExplorerUrl ?? null,
},
);
const blockExplorerElement = (
@@ -529,7 +529,7 @@ export default class ConfirmApproveContent extends Component {
fromAddressIsLedger,
toAddress,
chainId,
- rpcPrefs,
+ blockExplorerUrl,
assetStandard,
tokenId,
tokenAddress,
@@ -612,7 +612,7 @@ export default class ConfirmApproveContent extends Component {
tokenAddress={tokenAddress}
toAddress={toAddress}
chainId={chainId}
- rpcPrefs={rpcPrefs}
+ blockExplorerUrl={blockExplorerUrl}
tokenId={tokenId}
assetName={assetName}
assetStandard={assetStandard}
diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js
index 1b604ec43e6b..e167c15196ba 100644
--- a/ui/pages/confirmations/confirm-approve/confirm-approve.js
+++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js
@@ -12,10 +12,7 @@ import { getTokenApprovedParam } from '../../../helpers/utils/token-util';
import { readAddressAsContract } from '../../../../shared/modules/contract-utils';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { TransactionModalContextProvider } from '../../../contexts/transaction-modal';
-import {
- getNativeCurrency,
- isAddressLedger,
-} from '../../../ducks/metamask/metamask';
+import { isAddressLedger } from '../../../ducks/metamask/metamask';
import ConfirmContractInteraction from '../confirm-contract-interaction';
import {
getCurrentCurrency,
@@ -23,10 +20,9 @@ import {
getUseNonceField,
getCustomNonceValue,
getNextSuggestedNonce,
- getCurrentChainId,
- getRpcPrefsForCurrentProvider,
checkNetworkAndAccountSupports1559,
getUseCurrencyRateCheck,
+ selectNetworkConfigurationByChainId,
} from '../../../selectors';
import { useApproveTransaction } from '../hooks/useApproveTransaction';
import { useSimulationFailureWarning } from '../hooks/useSimulationFailureWarning';
@@ -66,16 +62,23 @@ export default function ConfirmApprove({
isSetApproveForAll,
}) {
const dispatch = useDispatch();
- const { txParams: { data: transactionData, from } = {} } = transaction;
+ const { chainId, txParams: { data: transactionData, from } = {} } =
+ transaction;
const currentCurrency = useSelector(getCurrentCurrency);
- const nativeCurrency = useSelector(getNativeCurrency);
+
+ const { nativeCurrency } = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+
const subjectMetadata = useSelector(getSubjectMetadata);
const useNonceField = useSelector(getUseNonceField);
const nextNonce = useSelector(getNextSuggestedNonce);
const customNonceValue = useSelector(getCustomNonceValue);
- const chainId = useSelector(getCurrentChainId);
- const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
+ const { blockExplorerUrls } = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+ const blockExplorerUrl = blockExplorerUrls?.[0];
const networkAndAccountSupports1559 = useSelector(
checkNetworkAndAccountSupports1559,
);
@@ -291,7 +294,7 @@ export default function ConfirmApprove({
txData={transaction}
fromAddressIsLedger={fromAddressIsLedger}
chainId={chainId}
- rpcPrefs={rpcPrefs}
+ blockExplorerUrl={blockExplorerUrl}
isContract={isContract}
hasLayer1GasFee={layer1GasFee !== undefined}
supportsEIP1559={supportsEIP1559}
@@ -334,6 +337,7 @@ ConfirmApprove.propTypes = {
userAddress: PropTypes.string,
toAddress: PropTypes.string,
transaction: PropTypes.shape({
+ chainId: PropTypes.string,
layer1GasFee: PropTypes.string,
origin: PropTypes.string,
txParams: PropTypes.shape({
diff --git a/ui/pages/confirmations/confirm-send-token/confirm-send-token.js b/ui/pages/confirmations/confirm-send-token/confirm-send-token.js
index 3e24c72541d5..4e68783e9be7 100644
--- a/ui/pages/confirmations/confirm-send-token/confirm-send-token.js
+++ b/ui/pages/confirmations/confirm-send-token/confirm-send-token.js
@@ -8,11 +8,9 @@ import { editExistingTransaction } from '../../../ducks/send';
import {
contractExchangeRateSelector,
getCurrentCurrency,
+ selectConversionRateByChainId,
+ selectNetworkConfigurationByChainId,
} from '../../../selectors';
-import {
- getConversionRate,
- getNativeCurrency,
-} from '../../../ducks/metamask/metamask';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { showSendTokenPage } from '../../../store/actions';
import {
@@ -49,8 +47,17 @@ export default function ConfirmSendToken({
history.push(SEND_ROUTE);
});
};
- const conversionRate = useSelector(getConversionRate);
- const nativeCurrency = useSelector(getNativeCurrency);
+
+ const { chainId } = transaction;
+
+ const conversionRate = useSelector((state) =>
+ selectConversionRateByChainId(state, chainId),
+ );
+
+ const { nativeCurrency } = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+
const currentCurrency = useSelector(getCurrentCurrency);
const contractExchangeRate = useSelector(contractExchangeRateSelector);
@@ -98,6 +105,7 @@ ConfirmSendToken.propTypes = {
toAddress: PropTypes.string,
tokenAddress: PropTypes.string,
transaction: PropTypes.shape({
+ chainId: PropTypes.string,
origin: PropTypes.string,
txParams: PropTypes.shape({
data: PropTypes.string,
diff --git a/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js b/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js
index 7b60bf9ab4fe..077d5c757dcb 100644
--- a/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js
+++ b/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js
@@ -15,16 +15,12 @@ import {
import { PRIMARY } from '../../../helpers/constants/common';
import {
contractExchangeRateSelector,
- getCurrentChainId,
getCurrentCurrency,
- getRpcPrefsForCurrentProvider,
getSelectedInternalAccount,
+ selectConversionRateByChainId,
+ selectNetworkConfigurationByChainId,
+ selectNftContractsByChainId,
} from '../../../selectors';
-import {
- getConversionRate,
- getNativeCurrency,
- getNftContracts,
-} from '../../../ducks/metamask/metamask';
import { TokenStandard } from '../../../../shared/constants/transaction';
import {
getWeiHexFromDecimalValue,
@@ -47,16 +43,28 @@ export default function ConfirmTokenTransactionBase({
ethTransactionTotal,
fiatTransactionTotal,
hexMaximumTransactionFee,
+ transaction,
}) {
const t = useContext(I18nContext);
const contractExchangeRate = useSelector(contractExchangeRateSelector);
- const nativeCurrency = useSelector(getNativeCurrency);
+ const { chainId } = transaction;
+
+ const { blockExplorerUrls, nativeCurrency } = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+
+ const blockExplorerUrl = blockExplorerUrls?.[0];
const currentCurrency = useSelector(getCurrentCurrency);
- const conversionRate = useSelector(getConversionRate);
- const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
- const chainId = useSelector(getCurrentChainId);
+
+ const conversionRate = useSelector((state) =>
+ selectConversionRateByChainId(state, chainId),
+ );
+
const { address: userAddress } = useSelector(getSelectedInternalAccount);
- const nftCollections = useSelector(getNftContracts);
+
+ const nftCollections = useSelector((state) =>
+ selectNftContractsByChainId(state, chainId),
+ );
const ethTransactionTotalMaxAmount = Number(
hexWEIToDecETH(hexMaximumTransactionFee),
@@ -64,7 +72,7 @@ export default function ConfirmTokenTransactionBase({
const getTitleTokenDescription = (renderType) => {
const useBlockExplorer =
- rpcPrefs?.blockExplorerUrl ||
+ blockExplorerUrl ||
[...TEST_CHAINS, CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET].includes(
chainId,
);
@@ -87,7 +95,7 @@ export default function ConfirmTokenTransactionBase({
null,
userAddress,
{
- blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ blockExplorerUrl: blockExplorerUrl ?? null,
},
);
const blockExplorerElement = (
@@ -219,4 +227,5 @@ ConfirmTokenTransactionBase.propTypes = {
ethTransactionTotal: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
hexMaximumTransactionFee: PropTypes.string,
+ transaction: PropTypes.string,
};
diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js
index 25d99e8a9f16..bcc3279752e2 100644
--- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js
@@ -1015,24 +1015,15 @@ export default class ConfirmTransactionBase extends Component {
const {
toAddress,
fromAddress,
- txData: { origin, chainId: txChainId } = {},
+ txData: { origin } = {},
getNextNonce,
tryReverseResolveAddress,
smartTransactionsPreferenceEnabled,
currentChainSupportsSmartTransactions,
setSwapsFeatureFlags,
fetchSmartTransactionsLiveness,
- chainId,
} = this.props;
- // If the user somehow finds themselves seeing a confirmation
- // on a network which is not presently selected, throw
- if (txChainId === undefined || txChainId !== chainId) {
- throw new Error(
- `Currently selected chainId (${chainId}) does not match chainId (${txChainId}) on which the transaction was proposed.`,
- );
- }
-
const { trackEvent } = this.context;
trackEvent({
category: MetaMetricsEventCategory.Transactions,
diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js
index 795dfae0c63a..c34025e2f0c5 100644
--- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js
+++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js
@@ -69,11 +69,8 @@ import {
isAddressLedger,
updateGasFees,
getIsGasEstimatesLoading,
- getNativeCurrency,
getSendToAccounts,
- getProviderConfig,
findKeyringForAddress,
- getConversionRate,
} from '../../../ducks/metamask/metamask';
import {
addHexPrefix,
@@ -97,10 +94,19 @@ import { CUSTOM_GAS_ESTIMATE } from '../../../../shared/constants/gas';
// eslint-disable-next-line import/no-duplicates
import { getIsUsingPaymaster } from '../../../selectors/account-abstraction';
+import {
+ selectConversionRateByChainId,
+ selectNetworkConfigurationByChainId,
+ // eslint-disable-next-line import/no-duplicates
+} from '../../../selectors/selectors';
+
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
+import {
+ getAccountType,
+ selectDefaultRpcEndpointByChainId,
+ // eslint-disable-next-line import/no-duplicates
+} from '../../../selectors/selectors';
// eslint-disable-next-line import/no-duplicates
-import { getAccountType } from '../../../selectors/selectors';
-
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app';
import {
getIsNoteToTraderSupported,
@@ -168,15 +174,16 @@ const mapStateToProps = (state, ownProps) => {
const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state);
const isBuyableChain = getIsNativeTokenBuyable(state);
const { confirmTransaction, metamask } = state;
- const conversionRate = getConversionRate(state);
const { addressBook, nextNonce } = metamask;
const unapprovedTxs = getUnapprovedTransactions(state);
- const { chainId } = getProviderConfig(state);
const { tokenData, txData, tokenProps, nonce } = confirmTransaction;
const { txParams = {}, id: transactionId, type } = txData;
const txId = transactionId || paramsTransactionId;
const transaction = getUnapprovedTransaction(state, txId) ?? {};
+ const { chainId } = transaction;
+ const conversionRate = selectConversionRateByChainId(state, chainId);
+
const {
from: fromAddress,
to: txParamsToAddress,
@@ -184,6 +191,7 @@ const mapStateToProps = (state, ownProps) => {
gas: gasLimit,
data,
} = (transaction && transaction.txParams) || txParams;
+
const accounts = getMetaMaskAccounts(state);
const smartTransactionsPreferenceEnabled =
getSmartTransactionsPreferenceEnabled(state);
@@ -270,7 +278,10 @@ const mapStateToProps = (state, ownProps) => {
fullTxData.userFeeLevel === CUSTOM_GAS_ESTIMATE ||
txParamsAreDappSuggested(fullTxData);
const fromAddressIsLedger = isAddressLedger(state, fromAddress);
- const nativeCurrency = getNativeCurrency(state);
+
+ const { nativeCurrency } =
+ selectNetworkConfigurationByChainId(state, chainId) ?? {};
+
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
const accountType = getAccountType(state, fromAddress);
const fromChecksumHexAddress = toChecksumHexAddress(fromAddress);
@@ -281,7 +292,9 @@ const mapStateToProps = (state, ownProps) => {
const custodianPublishesTransaction =
getIsCustodianPublishesTransactionSupported(state, fromChecksumHexAddress);
const builtinRpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId];
- const { rpcUrl: customRpcUrl } = getProviderConfig(state);
+
+ const { url: customRpcUrl } =
+ selectDefaultRpcEndpointByChainId(state, chainId) ?? {};
const rpcUrl = customRpcUrl || builtinRpcUrl;
diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js
index bea6aef1d84d..5a846874fed3 100644
--- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js
+++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js
@@ -969,45 +969,4 @@ describe('Confirm Transaction Base', () => {
expect(confirmButton).toBeDisabled();
});
});
-
- describe('Preventing transaction submission', () => {
- it('should throw error when on wrong chain', async () => {
- const txParams = {
- ...mockTxParams,
- to: undefined,
- data: '0xa22cb46500000000000000',
- chainId: '0x5',
- };
- const state = {
- ...baseStore,
- metamask: {
- ...baseStore.metamask,
- transactions: [
- {
- id: baseStore.confirmTransaction.txData.id,
- chainId: '0x5',
- status: 'unapproved',
- txParams,
- },
- ],
- ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }),
- },
- confirmTransaction: {
- ...baseStore.confirmTransaction,
- txData: {
- ...baseStore.confirmTransaction.txData,
- value: '0x0',
- isUserOperation: true,
- txParams,
- chainId: '0x5',
- },
- },
- };
-
- // Error will be triggered by componentDidMount
- await expect(render({ state })).rejects.toThrow(
- 'Currently selected chainId (0xaa36a7) does not match chainId (0x5) on which the transaction was proposed.',
- );
- });
- });
});
diff --git a/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js b/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js
index 1bf5c8771b4f..72f570c5b98b 100644
--- a/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js
+++ b/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js
@@ -27,6 +27,7 @@ import { useAssetDetails } from '../hooks/useAssetDetails';
export default function ConfirmTokenTransactionSwitch({ transaction }) {
const {
+ chainId,
txParams: { data, to: tokenAddress, from: userAddress } = {},
layer1GasFee,
} = transaction;
@@ -44,7 +45,7 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) {
tokenAmount,
tokenId,
toAddress,
- } = useAssetDetails(tokenAddress, userAddress, data);
+ } = useAssetDetails(tokenAddress, userAddress, data, chainId);
const {
ethTransactionTotal,
@@ -221,6 +222,7 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) {
ConfirmTokenTransactionSwitch.propTypes = {
transaction: PropTypes.shape({
+ chainId: PropTypes.string,
origin: PropTypes.string,
txParams: PropTypes.shape({
data: PropTypes.string,
diff --git a/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx b/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx
index 33a8c2e67f0b..5a531a97787d 100644
--- a/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx
+++ b/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx
@@ -39,15 +39,16 @@ export const UserOperationStory = () => {
};
const confirmState = getMockConfirmStateForTransaction(confirmation, {
- metamask: {},
- preferences: {
- ...mockState.metamask.preferences,
- petnamesEnabled: true,
- },
- userOperations: {
- [confirmation.id]: {
- userOperation: {
- paymasterAndData: PAYMASTER_AND_DATA,
+ metamask: {
+ preferences: {
+ ...mockState.metamask.preferences,
+ petnamesEnabled: true,
+ },
+ userOperations: {
+ [confirmation.id]: {
+ userOperation: {
+ paymasterAndData: PAYMASTER_AND_DATA,
+ },
},
},
},
diff --git a/ui/pages/confirmations/confirm/stories/utils.tsx b/ui/pages/confirmations/confirm/stories/utils.tsx
index 9c68a392cbd7..18f9ae734e31 100644
--- a/ui/pages/confirmations/confirm/stories/utils.tsx
+++ b/ui/pages/confirmations/confirm/stories/utils.tsx
@@ -1,17 +1,11 @@
import React from 'react';
import { Provider } from 'react-redux';
-import { MemoryRouter, Route } from 'react-router-dom';
import configureStore from '../../../../store/store';
-import { ConfirmContextProvider } from '../../context/confirm';
import ConfirmPage from '../confirm';
export const CONFIRM_PAGE_DECORATOR = [
(story: () => React.ReactFragment) => {
- return (
-
- {story()}
-
- );
+ return {story()}
;
},
];
@@ -36,18 +30,7 @@ export function ConfirmStoryTemplate(
return (
- {/* Adding the MemoryRouter and Route is a workaround to bypass a 404 error in storybook that
- is caused when the 'ui/pages/confirmations/hooks/syncConfirmPath.ts' hook calls
- history.replace. To avoid history.replace, we can provide a param id. */}
-
- } />
-
+
);
}
diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js
index 908f600564f8..5e327b0467c5 100644
--- a/ui/pages/confirmations/hooks/test-utils.js
+++ b/ui/pages/confirmations/hooks/test-utils.js
@@ -3,10 +3,6 @@ import { useSelector } from 'react-redux';
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
import { GasEstimateTypes } from '../../../../shared/constants/gas';
-import {
- getConversionRate,
- getNativeCurrency,
-} from '../../../ducks/metamask/metamask';
import {
getCurrentCurrency,
getShouldShowFiat,
@@ -14,6 +10,7 @@ import {
getCurrentKeyring,
getTokenExchangeRates,
getPreferences,
+ selectConversionRateByChainId,
} from '../../../selectors';
import {
@@ -107,13 +104,10 @@ export const generateUseSelectorRouter =
if (selector === getMultichainIsEvm) {
return true;
}
- if (selector === getConversionRate) {
+ if (selector === selectConversionRateByChainId) {
return MOCK_ETH_USD_CONVERSION_RATE;
}
- if (
- selector === getMultichainNativeCurrency ||
- selector === getNativeCurrency
- ) {
+ if (selector === getMultichainNativeCurrency) {
return EtherDenomination.ETH;
}
if (selector === getPreferences) {
diff --git a/ui/pages/confirmations/hooks/useAssetDetails.js b/ui/pages/confirmations/hooks/useAssetDetails.js
index 4a9afaf05468..bea2f6da89a8 100644
--- a/ui/pages/confirmations/hooks/useAssetDetails.js
+++ b/ui/pages/confirmations/hooks/useAssetDetails.js
@@ -1,7 +1,7 @@
import { isEqual } from 'lodash';
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { getNfts, getTokens } from '../../../ducks/metamask/metamask';
+import { getTokens } from '../../../ducks/metamask/metamask';
import { getAssetDetails } from '../../../helpers/utils/token-util';
import {
hideLoadingIndication,
@@ -10,11 +10,18 @@ import {
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
import { usePrevious } from '../../../hooks/usePrevious';
import { useTokenTracker } from '../../../hooks/useTokenTracker';
+import { selectNftsByChainId } from '../../../selectors';
-export function useAssetDetails(tokenAddress, userAddress, transactionData) {
+export function useAssetDetails(
+ tokenAddress,
+ userAddress,
+ transactionData,
+ chainId,
+) {
const dispatch = useDispatch();
+
// state selectors
- const nfts = useSelector(getNfts);
+ const nfts = useSelector((state) => selectNftsByChainId(state, chainId));
const tokens = useSelector(getTokens, isEqual);
const currentToken = tokens.find((token) =>
isEqualCaseInsensitive(token.address, tokenAddress),
diff --git a/ui/pages/confirmations/hooks/useTransactionFunctionType.js b/ui/pages/confirmations/hooks/useTransactionFunctionType.js
index 559367fff066..10255149eda7 100644
--- a/ui/pages/confirmations/hooks/useTransactionFunctionType.js
+++ b/ui/pages/confirmations/hooks/useTransactionFunctionType.js
@@ -2,8 +2,10 @@ import { useSelector } from 'react-redux';
import { TransactionType } from '@metamask/transaction-controller';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
-import { getKnownMethodData } from '../../../selectors';
-import { getNativeCurrency } from '../../../ducks/metamask/metamask';
+import {
+ getKnownMethodData,
+ selectNetworkConfigurationByChainId,
+} from '../../../selectors';
import { getTransactionTypeTitle } from '../../../helpers/utils/transactions.util';
import { getMethodName } from '../../../helpers/utils/metrics';
@@ -11,8 +13,12 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
export const useTransactionFunctionType = (txData = {}) => {
const t = useI18nContext();
- const nativeCurrency = useSelector(getNativeCurrency);
- const { txParams } = txData;
+ const { chainId, txParams } = txData;
+
+ const networkConfiguration = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+
const methodData = useSelector(
(state) => getKnownMethodData(state, txParams?.data) || {},
);
@@ -21,6 +27,8 @@ export const useTransactionFunctionType = (txData = {}) => {
return {};
}
+ const { nativeCurrency } = networkConfiguration ?? {};
+
const isTokenApproval =
txData.type === TransactionType.tokenMethodSetApprovalForAll ||
txData.type === TransactionType.tokenMethodApprove ||
diff --git a/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js b/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js
index 2bdc008e05dd..da625ad68267 100644
--- a/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js
+++ b/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js
@@ -1,47 +1,64 @@
-import { TransactionType } from '@metamask/transaction-controller';
+import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller';
import { renderHookWithProvider } from '../../../../test/lib/render-helpers';
import mockState from '../../../../test/data/mock-state.json';
+import { mockNetworkState } from '../../../../test/stub/networks';
import { useTransactionFunctionType } from './useTransactionFunctionType';
+const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI;
+
+const STATE_MOCK = {
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ ...mockNetworkState({ chainId: CHAIN_ID_MOCK }),
+ },
+};
+
describe('useTransactionFunctionType', () => {
it('should return functionType depending on transaction data if present', () => {
const { result } = renderHookWithProvider(
() =>
useTransactionFunctionType({
+ chainId: CHAIN_ID_MOCK,
txParams: {
data: '0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000011170',
},
type: TransactionType.tokenMethodApprove,
}),
- mockState,
+ STATE_MOCK,
);
expect(result.current.functionType).toStrictEqual('Approve spend limit');
});
+
it('should return functionType depending on transaction type if method not present in transaction data', () => {
const { result } = renderHookWithProvider(
() =>
useTransactionFunctionType({
+ chainId: CHAIN_ID_MOCK,
txParams: {},
type: TransactionType.tokenMethodTransfer,
}),
- mockState,
+ STATE_MOCK,
);
expect(result.current.functionType).toStrictEqual('Transfer');
});
+
it('should return functionType Contract interaction by default', () => {
const { result } = renderHookWithProvider(
() =>
useTransactionFunctionType({
+ chainId: CHAIN_ID_MOCK,
txParams: {},
}),
- mockState,
+ STATE_MOCK,
);
expect(result.current.functionType).toStrictEqual('Contract interaction');
});
+
it('should return undefined is txData is not present', () => {
const { result } = renderHookWithProvider(
() => useTransactionFunctionType(),
- mockState,
+ STATE_MOCK,
);
expect(result.current.functionType).toBeUndefined();
});
diff --git a/ui/pages/confirmations/hooks/useTransactionInfo.js b/ui/pages/confirmations/hooks/useTransactionInfo.js
index 452e44c83dfb..ef709de50486 100644
--- a/ui/pages/confirmations/hooks/useTransactionInfo.js
+++ b/ui/pages/confirmations/hooks/useTransactionInfo.js
@@ -1,5 +1,4 @@
import { useSelector } from 'react-redux';
-import { getProviderConfig } from '../../../ducks/metamask/metamask';
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
import { getSelectedInternalAccount } from '../../../selectors';
@@ -7,7 +6,7 @@ import { getSelectedInternalAccount } from '../../../selectors';
export const useTransactionInfo = (txData = {}) => {
const { allNftContracts } = useSelector((state) => state.metamask);
const selectedInternalAccount = useSelector(getSelectedInternalAccount);
- const { chainId } = useSelector(getProviderConfig);
+ const { chainId } = txData;
const isNftTransfer = Boolean(
allNftContracts?.[selectedInternalAccount.address]?.[chainId]?.find(
diff --git a/ui/pages/confirmations/hooks/useTransactionInfo.test.js b/ui/pages/confirmations/hooks/useTransactionInfo.test.js
index 61ae036c4000..272c27abbfa4 100644
--- a/ui/pages/confirmations/hooks/useTransactionInfo.test.js
+++ b/ui/pages/confirmations/hooks/useTransactionInfo.test.js
@@ -1,7 +1,7 @@
+import { CHAIN_IDS } from '@metamask/transaction-controller';
import { renderHookWithProvider } from '../../../../test/lib/render-helpers';
import mockState from '../../../../test/data/mock-state.json';
import { getSelectedInternalAccountFromMockState } from '../../../../test/jest/mocks';
-import { getCurrentChainId } from '../../../selectors';
import { useTransactionInfo } from './useTransactionInfo';
const mockSelectedInternalAccount =
@@ -13,22 +13,25 @@ describe('useTransactionInfo', () => {
const { result } = renderHookWithProvider(
() =>
useTransactionInfo({
+ chainId: CHAIN_IDS.GOERLI,
txParams: {},
}),
mockState,
);
expect(result.current.isNftTransfer).toStrictEqual(false);
});
+
it('should return true if transaction is NFT transfer', () => {
mockState.metamask.allNftContracts = {
[mockSelectedInternalAccount.address]: {
- [getCurrentChainId(mockState)]: [{ address: '0x9' }],
+ [CHAIN_IDS.GOERLI]: [{ address: '0x9' }],
},
};
const { result } = renderHookWithProvider(
() =>
useTransactionInfo({
+ chainId: CHAIN_IDS.GOERLI,
txParams: {
to: '0x9',
},
diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js
index 33a011c2966a..312854d168a7 100644
--- a/ui/pages/confirmations/send/gas-display/gas-display.js
+++ b/ui/pages/confirmations/send/gas-display/gas-display.js
@@ -27,14 +27,11 @@ import {
getIsTestnet,
getUseCurrencyRateCheck,
getUnapprovedTransactions,
+ selectNetworkConfigurationByChainId,
} from '../../../../selectors';
import { INSUFFICIENT_TOKENS_ERROR } from '../send.constants';
import { getCurrentDraftTransaction } from '../../../../ducks/send';
-import {
- getNativeCurrency,
- getProviderConfig,
-} from '../../../../ducks/metamask/metamask';
import { showModal } from '../../../../store/actions';
import {
addHexes,
@@ -52,25 +49,26 @@ import { getIsNativeTokenBuyable } from '../../../../ducks/ramps';
export default function GasDisplay({ gasError }) {
const t = useContext(I18nContext);
const dispatch = useDispatch();
- const { estimateUsed } = useGasFeeContext();
+ const { estimateUsed, transaction } = useGasFeeContext();
+ const { chainId } = transaction;
const trackEvent = useContext(MetaMetricsContext);
-
const { openBuyCryptoInPdapp } = useRamps();
- const providerConfig = useSelector(getProviderConfig);
+ const { name: networkNickname, nativeCurrency } = useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ );
+
const isTestnet = useSelector(getIsTestnet);
const isBuyableChain = useSelector(getIsNativeTokenBuyable);
const draftTransaction = useSelector(getCurrentDraftTransaction);
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
const { showFiatInTestnets } = useSelector(getPreferences);
const unapprovedTxs = useSelector(getUnapprovedTransactions);
- const nativeCurrency = useSelector(getNativeCurrency);
- const { chainId } = providerConfig;
const networkName = NETWORK_TO_NAME_MAP[chainId];
const isInsufficientTokenError =
draftTransaction?.amount?.error === INSUFFICIENT_TOKENS_ERROR;
const editingTransaction = unapprovedTxs[draftTransaction.id];
- const currentNetworkName = networkName || providerConfig.nickname;
+ const currentNetworkName = networkName || networkNickname;
const transactionData = {
txParams: {
diff --git a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap
index 91ac3c735f84..bc44e728df86 100644
--- a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap
+++ b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap
@@ -148,14 +148,14 @@ exports[`TokenAllowancePage when mounted should match snapshot 1`] = `
- mainnet
+ Ethereum Mainnet
getTargetAccountWithSendEtherInfo(state, userAddress),
);
- const networkIdentifier = useSelector(getNetworkIdentifier);
- const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
+
+ const { chainId } = txData;
+
+ const networkIdentifier = useSelector((state) =>
+ selectNetworkIdentifierByChainId(state, chainId),
+ );
+
+ const { blockExplorerUrls } =
+ useSelector((state) =>
+ selectNetworkConfigurationByChainId(state, chainId),
+ ) ?? {};
+
+ const blockExplorerUrl = blockExplorerUrls?.[0];
const unapprovedTxCount = useSelector(getUnapprovedTxCount);
const unapprovedTxs = useSelector(getUnapprovedTransactions);
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
@@ -387,7 +403,7 @@ export default function TokenAllowance({
tokenName={tokenSymbol}
address={tokenAddress}
chainId={fullTxData.chainId}
- rpcPrefs={rpcPrefs}
+ blockExplorerUrl={blockExplorerUrl}
/>
);
@@ -710,7 +726,7 @@ export default function TokenAllowance({
tokenAddress={tokenAddress}
toAddress={toAddress}
chainId={fullTxData.chainId}
- rpcPrefs={rpcPrefs}
+ blockExplorerUrl={blockExplorerUrl}
/>
)}
diff --git a/ui/pages/confirmations/token-allowance/token-allowance.test.js b/ui/pages/confirmations/token-allowance/token-allowance.test.js
index ddfc48a2cfaa..68df89777c7e 100644
--- a/ui/pages/confirmations/token-allowance/token-allowance.test.js
+++ b/ui/pages/confirmations/token-allowance/token-allowance.test.js
@@ -203,7 +203,7 @@ describe('TokenAllowancePage', () => {
status: 'unapproved',
originalGasEstimate: '0xea60',
userEditedGasLimit: false,
- chainId: '0x3',
+ chainId: CHAIN_IDS.MAINNET,
loadingDefaults: false,
dappSuggestedGasFees: {
gasPrice: '0x4a817c800',
diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js
index 94d8f39252d8..c4f2928d8ef2 100644
--- a/ui/selectors/selectors.js
+++ b/ui/selectors/selectors.js
@@ -734,6 +734,51 @@ export const selectDefaultRpcEndpointByChainId = createSelector(
},
);
+/**
+ * @type (state: any, chainId: string) => number | undefined
+ */
+export const selectConversionRateByChainId = createSelector(
+ selectNetworkConfigurationByChainId,
+ (state) => state,
+ (networkConfiguration, state) => {
+ if (!networkConfiguration) {
+ return undefined;
+ }
+
+ const { nativeCurrency } = networkConfiguration;
+ return state.metamask.currencyRates[nativeCurrency]?.conversionRate;
+ },
+);
+
+export const selectNftsByChainId = createSelector(
+ getSelectedInternalAccount,
+ (state) => state.metamask.allNfts,
+ (_state, chainId) => chainId,
+ (selectedAccount, nfts, chainId) => {
+ return nfts?.[selectedAccount.address]?.[chainId] ?? [];
+ },
+);
+
+export const selectNftContractsByChainId = createSelector(
+ getSelectedInternalAccount,
+ (state) => state.metamask.allNftContracts,
+ (_state, chainId) => chainId,
+ (selectedAccount, nftContracts, chainId) => {
+ return nftContracts?.[selectedAccount.address]?.[chainId] ?? [];
+ },
+);
+
+export const selectNetworkIdentifierByChainId = createSelector(
+ selectNetworkConfigurationByChainId,
+ selectDefaultRpcEndpointByChainId,
+ (networkConfiguration, defaultRpcEndpoint) => {
+ const { name: nickname } = networkConfiguration ?? {};
+ const { url: rpcUrl, networkClientId } = defaultRpcEndpoint ?? {};
+
+ return nickname || rpcUrl || networkClientId;
+ },
+);
+
export function getRequestingNetworkInfo(state, chainIds) {
// If chainIds is undefined, set it to an empty array
let processedChainIds = chainIds === undefined ? [] : chainIds;
From e882da087d6028a5cd884306a2d4b6c8a59db366 Mon Sep 17 00:00:00 2001
From: Pedro Figueiredo
Date: Fri, 8 Nov 2024 11:19:07 +0000
Subject: [PATCH 14/18] fix: Address design review for ERC20 token send
(#28212)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR implements usage of ENS resolved names existing in state for the
PetNames component. It also tweaks various UI elements as per design
review.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28212?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3585
## **Manual testing steps**
1. Initiate an erc20 token send on the wallet UI
2. Check the confirmation screen
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../info/row/__snapshots__/row.test.tsx.snap | 2 +-
ui/components/app/confirm/info/row/row.tsx | 2 +-
.../__snapshots__/name-details.test.tsx.snap | 50 +++-
.../name/name-details/name-details.test.tsx | 283 +++++++++++++++++-
ui/hooks/useDisplayName.test.ts | 70 ++++-
ui/hooks/useDisplayName.ts | 39 ++-
.../__snapshots__/approve.test.tsx.snap | 2 +-
.../native-transfer.test.tsx.snap | 193 ++++++------
.../native-transfer/native-transfer.test.tsx | 8 +-
.../nft-token-transfer.test.tsx.snap | 197 ++++++------
.../nft-token-transfer.test.tsx | 8 +-
.../transaction-data.test.tsx.snap | 8 +-
.../token-details-section.test.tsx.snap | 17 +-
.../token-transfer.test.tsx.snap | 242 ++++++++-------
.../token-transfer/token-details-section.tsx | 7 +-
.../token-transfer/token-transfer.test.tsx | 8 +-
16 files changed, 770 insertions(+), 366 deletions(-)
diff --git a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap
index 545d548fa3b7..7dad4ee50357 100644
--- a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap
+++ b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap
@@ -37,7 +37,7 @@ exports[`ConfirmInfoRow should match snapshot when copy is enabled 1`] = `
= ({
{copyEnabled && (
)}
diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
index cfa08b3eee01..da6f598dfa5e 100644
--- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
+++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
@@ -706,12 +706,50 @@ exports[`NameDetails renders with recognized name 1`] = `
-
+
diff --git a/ui/components/app/name/name-details/name-details.test.tsx b/ui/components/app/name/name-details/name-details.test.tsx
index 0f0df9f5b5f6..8043d432d526 100644
--- a/ui/components/app/name/name-details/name-details.test.tsx
+++ b/ui/components/app/name/name-details/name-details.test.tsx
@@ -1,18 +1,21 @@
-import React from 'react';
import { NameType } from '@metamask/name-controller';
-import configureStore from 'redux-mock-store';
import { fireEvent } from '@testing-library/react';
+import React from 'react';
import { act } from 'react-dom/test-utils';
-import { useDispatch } from 'react-redux';
-import { renderWithProvider } from '../../../../../test/lib/render-helpers';
-import { setName, updateProposedNames } from '../../../../store/actions';
-import { MetaMetricsContext } from '../../../../contexts/metametrics';
+import { useDispatch, useSelector } from 'react-redux';
+import configureStore from 'redux-mock-store';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
+import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import { mockNetworkState } from '../../../../../test/stub/networks';
+import { MetaMetricsContext } from '../../../../contexts/metametrics';
+import { getDomainResolutions } from '../../../../ducks/domains';
+import { getNames, getNameSources } from '../../../../selectors';
+import { getNftContractsByAddressByChain } from '../../../../selectors/nft';
+import { setName, updateProposedNames } from '../../../../store/actions';
import NameDetails from './name-details';
jest.mock('../../../../store/actions', () => ({
@@ -23,6 +26,7 @@ jest.mock('../../../../store/actions', () => ({
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(),
+ useSelector: jest.fn(),
}));
jest.useFakeTimers();
@@ -150,10 +154,74 @@ describe('NameDetails', () => {
const setNameMock = jest.mocked(setName);
const updateProposedNamesMock = jest.mocked(updateProposedNames);
const useDispatchMock = jest.mocked(useDispatch);
+ const useSelectorMock = jest.mocked(useSelector);
beforeEach(() => {
jest.resetAllMocks();
useDispatchMock.mockReturnValue(jest.fn());
+
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getNames) {
+ return {
+ [NameType.ETHEREUM_ADDRESS]: {
+ [ADDRESS_SAVED_NAME_MOCK]: {
+ [VARIATION_MOCK]: {
+ name: SAVED_NAME_MOCK,
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ },
+ },
+ [ADDRESS_NO_NAME_MOCK]: {
+ [VARIATION_MOCK]: {
+ name: SAVED_NAME_MOCK,
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ },
+ },
+ },
+ };
+ } else if (selector === getNftContractsByAddressByChain) {
+ return {
+ [VARIATION_MOCK]: {
+ [ADDRESS_RECOGNIZED_MOCK]: {
+ name: 'iZUMi Bond USD',
+ },
+ },
+ };
+ } else if (selector === getDomainResolutions) {
+ return [
+ {
+ resolvedAddress: ADDRESS_SAVED_NAME_MOCK,
+ domainName: 'Domain name',
+ },
+ ];
+ } else if (selector === getNameSources) {
+ return {
+ [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' },
+ };
+ }
+ return undefined;
+ });
});
it('renders when no address value is passed', () => {
@@ -171,6 +239,33 @@ describe('NameDetails', () => {
});
it('renders with no saved name', () => {
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getNames) {
+ return {
+ [NameType.ETHEREUM_ADDRESS]: {},
+ };
+ } else if (selector === getNftContractsByAddressByChain) {
+ return {
+ [VARIATION_MOCK]: {
+ [ADDRESS_RECOGNIZED_MOCK]: {
+ name: 'iZUMi Bond USD',
+ },
+ },
+ };
+ } else if (selector === getDomainResolutions) {
+ return [
+ {
+ resolvedAddress: ADDRESS_SAVED_NAME_MOCK,
+ },
+ ];
+ } else if (selector === getNameSources) {
+ return {
+ [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' },
+ };
+ }
+ return undefined;
+ });
+
const { baseElement } = renderWithProvider(
{
});
it('sends created event', async () => {
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getNames) {
+ return {
+ [NameType.ETHEREUM_ADDRESS]: {
+ [ADDRESS_NO_NAME_MOCK]: {
+ [VARIATION_MOCK]: {
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ },
+ },
+ },
+ };
+ } else if (selector === getNftContractsByAddressByChain) {
+ return {
+ [VARIATION_MOCK]: {
+ [ADDRESS_RECOGNIZED_MOCK]: {
+ name: 'iZUMi Bond USD',
+ },
+ },
+ };
+ } else if (selector === getDomainResolutions) {
+ return [
+ {
+ resolvedAddress: ADDRESS_SAVED_NAME_MOCK,
+ },
+ ];
+ } else if (selector === getNameSources) {
+ return {
+ [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' },
+ };
+ }
+ return undefined;
+ });
+
const trackEventMock = jest.fn();
const component = renderWithProvider(
@@ -424,7 +563,7 @@ describe('NameDetails', () => {
await saveNameUsingDropdown(component, PROPOSED_NAME_MOCK);
- expect(trackEventMock).toHaveBeenCalledWith({
+ expect(trackEventMock).toHaveBeenNthCalledWith(2, {
event: MetaMetricsEventName.PetnameCreated,
category: MetaMetricsEventCategory.Petnames,
properties: {
@@ -436,6 +575,69 @@ describe('NameDetails', () => {
});
it('sends updated event', async () => {
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getNames) {
+ return {
+ [NameType.ETHEREUM_ADDRESS]: {
+ [ADDRESS_SAVED_NAME_MOCK]: {
+ [CHAIN_ID_MOCK]: {
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ name: SAVED_NAME_MOCK,
+ sourceId: SOURCE_ID_MOCK,
+ },
+ },
+ [ADDRESS_NO_NAME_MOCK]: {
+ [CHAIN_ID_MOCK]: {
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ name: null,
+ },
+ },
+ },
+ };
+ } else if (selector === getNftContractsByAddressByChain) {
+ return {
+ [VARIATION_MOCK]: {
+ [ADDRESS_RECOGNIZED_MOCK]: {
+ name: 'iZUMi Bond USD',
+ },
+ },
+ };
+ } else if (selector === getDomainResolutions) {
+ return [
+ {
+ resolvedAddress: ADDRESS_SAVED_NAME_MOCK,
+ },
+ ];
+ } else if (selector === getNameSources) {
+ return {
+ [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' },
+ };
+ }
+ return undefined;
+ });
+
const trackEventMock = jest.fn();
const component = renderWithProvider(
@@ -452,7 +654,7 @@ describe('NameDetails', () => {
await saveNameUsingDropdown(component, PROPOSED_NAME_2_MOCK);
- expect(trackEventMock).toHaveBeenCalledWith({
+ expect(trackEventMock).toHaveBeenNthCalledWith(2, {
event: MetaMetricsEventName.PetnameUpdated,
category: MetaMetricsEventCategory.Petnames,
properties: {
@@ -465,6 +667,69 @@ describe('NameDetails', () => {
});
it('sends deleted event', async () => {
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getNames) {
+ return {
+ [NameType.ETHEREUM_ADDRESS]: {
+ [ADDRESS_SAVED_NAME_MOCK]: {
+ [CHAIN_ID_MOCK]: {
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ name: SAVED_NAME_MOCK,
+ sourceId: SOURCE_ID_MOCK,
+ },
+ },
+ [ADDRESS_NO_NAME_MOCK]: {
+ [CHAIN_ID_MOCK]: {
+ proposedNames: {
+ [SOURCE_ID_MOCK]: {
+ proposedNames: [PROPOSED_NAME_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ [SOURCE_ID_2_MOCK]: {
+ proposedNames: [PROPOSED_NAME_2_MOCK],
+ lastRequestTime: null,
+ retryDelay: null,
+ },
+ },
+ name: null,
+ },
+ },
+ },
+ };
+ } else if (selector === getNftContractsByAddressByChain) {
+ return {
+ [VARIATION_MOCK]: {
+ [ADDRESS_RECOGNIZED_MOCK]: {
+ name: 'iZUMi Bond USD',
+ },
+ },
+ };
+ } else if (selector === getDomainResolutions) {
+ return [
+ {
+ resolvedAddress: ADDRESS_SAVED_NAME_MOCK,
+ },
+ ];
+ } else if (selector === getNameSources) {
+ return {
+ [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' },
+ };
+ }
+ return undefined;
+ });
+
const trackEventMock = jest.fn();
const component = renderWithProvider(
@@ -481,7 +746,7 @@ describe('NameDetails', () => {
await saveNameUsingTextField(component, '');
- expect(trackEventMock).toHaveBeenCalledWith({
+ expect(trackEventMock).toHaveBeenNthCalledWith(2, {
event: MetaMetricsEventName.PetnameDeleted,
category: MetaMetricsEventCategory.Petnames,
properties: {
diff --git a/ui/hooks/useDisplayName.test.ts b/ui/hooks/useDisplayName.test.ts
index 5c36b0a97ed2..896e974c0025 100644
--- a/ui/hooks/useDisplayName.test.ts
+++ b/ui/hooks/useDisplayName.test.ts
@@ -1,19 +1,23 @@
import { NameType } from '@metamask/name-controller';
import { CHAIN_IDS } from '@metamask/transaction-controller';
-import { cloneDeep } from 'lodash';
import { Hex } from '@metamask/utils';
-import { renderHookWithProvider } from '../../test/lib/render-helpers';
-import mockState from '../../test/data/mock-state.json';
+import { cloneDeep } from 'lodash';
import {
EXPERIENCES_TYPE,
FIRST_PARTY_CONTRACT_NAMES,
} from '../../shared/constants/first-party-contracts';
+import mockState from '../../test/data/mock-state.json';
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import { getDomainResolutions } from '../ducks/domains';
import { useDisplayName } from './useDisplayName';
-import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
import { useNames } from './useName';
+import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
jest.mock('./useName');
jest.mock('./useNftCollectionsMetadata');
+jest.mock('../ducks/domains', () => ({
+ getDomainResolutions: jest.fn(),
+}));
const VALUE_MOCK = 'testvalue';
const VARIATION_MOCK = CHAIN_IDS.GOERLI;
@@ -22,6 +26,7 @@ const ERC20_TOKEN_NAME_MOCK = 'testName2';
const WATCHED_NFT_NAME_MOCK = 'testName3';
const NFT_NAME_MOCK = 'testName4';
const FIRST_PARTY_CONTRACT_NAME_MOCK = 'testName5';
+const ENS_NAME_MOCK = 'vitalik.eth';
const SYMBOL_MOCK = 'tes';
const NFT_IMAGE_MOCK = 'testNftImage';
const ERC20_IMAGE_MOCK = 'testImage';
@@ -30,6 +35,7 @@ const OTHER_NAME_TYPE = 'test' as NameType;
describe('useDisplayName', () => {
const useNamesMock = jest.mocked(useNames);
const useNftCollectionsMetadataMock = jest.mocked(useNftCollectionsMetadata);
+ const domainResolutionsMock = jest.mocked(getDomainResolutions);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let state: any;
@@ -87,6 +93,18 @@ describe('useDisplayName', () => {
});
}
+ function mockDomainResolutions(address: string, ensName: string) {
+ domainResolutionsMock.mockReturnValue([
+ {
+ addressBookEntryName: undefined,
+ domainName: ensName,
+ protocol: 'Ethereum Name Service',
+ resolvedAddress: address,
+ resolvingSnap: 'Ethereum Name Service resolver',
+ },
+ ]);
+ }
+
function mockFirstPartyContractName(
value: string,
variation: string,
@@ -403,6 +421,50 @@ describe('useDisplayName', () => {
});
});
+ describe('Domain Resolutions', () => {
+ it('returns ENS name if domain resolution for that address exists', () => {
+ mockDomainResolutions(VALUE_MOCK, ENS_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: ENS_NAME_MOCK,
+ });
+ });
+
+ it('returns no name if type not address', () => {
+ mockDomainResolutions(VALUE_MOCK, ENS_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: OTHER_NAME_TYPE,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: null,
+ });
+ });
+ });
+
describe('Priority', () => {
it('uses petname as first priority', () => {
mockPetname(PETNAME_MOCK);
diff --git a/ui/hooks/useDisplayName.ts b/ui/hooks/useDisplayName.ts
index 7b7429c7a0d4..66463b884338 100644
--- a/ui/hooks/useDisplayName.ts
+++ b/ui/hooks/useDisplayName.ts
@@ -1,12 +1,14 @@
import { NameType } from '@metamask/name-controller';
-import { useSelector } from 'react-redux';
import { Hex } from '@metamask/utils';
-import { selectERC20TokensByChain } from '../selectors';
-import { getNftContractsByAddressByChain } from '../selectors/nft';
+import { useSelector } from 'react-redux';
import {
EXPERIENCES_TYPE,
FIRST_PARTY_CONTRACT_NAMES,
} from '../../shared/constants/first-party-contracts';
+import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
+import { getDomainResolutions } from '../ducks/domains';
+import { selectERC20TokensByChain } from '../selectors';
+import { getNftContractsByAddressByChain } from '../selectors/nft';
import { useNames } from './useName';
import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
@@ -29,9 +31,11 @@ export function useDisplayNames(
): UseDisplayNameResponse[] {
const nameEntries = useNames(requests);
const firstPartyContractNames = useFirstPartyContractNames(requests);
+
const erc20Tokens = useERC20Tokens(requests);
const watchedNFTNames = useWatchedNFTNames(requests);
const nfts = useNFTs(requests);
+ const ens = useDomainResolutions(requests);
return requests.map((_request, index) => {
const nameEntry = nameEntries[index];
@@ -39,6 +43,7 @@ export function useDisplayNames(
const erc20Token = erc20Tokens[index];
const watchedNftName = watchedNFTNames[index];
const nft = nfts[index];
+ const ensName = ens[index];
const name =
nameEntry?.name ||
@@ -46,6 +51,7 @@ export function useDisplayNames(
nft?.name ||
erc20Token?.name ||
watchedNftName ||
+ ensName ||
null;
const image = nft?.image || erc20Token?.image;
@@ -107,6 +113,7 @@ function useWatchedNFTNames(
const contractAddress = value.toLowerCase();
const watchedNftNamesByAddress = watchedNftNamesByAddressByChain[variation];
+
return watchedNftNamesByAddress?.[contractAddress]?.name;
});
}
@@ -147,6 +154,32 @@ function useNFTs(
);
}
+function useDomainResolutions(nameRequests: UseDisplayNameRequest[]) {
+ const domainResolutions = useSelector(getDomainResolutions);
+
+ return nameRequests.map(({ type, value }) => {
+ if (type !== NameType.ETHEREUM_ADDRESS) {
+ return undefined;
+ }
+
+ const matchedResolution = domainResolutions?.find(
+ (resolution: {
+ addressBookEntryName: string;
+ domainName: string;
+ protocol: string;
+ resolvedAddress: string;
+ resolvingSnap: string;
+ }) =>
+ toChecksumHexAddress(resolution.resolvedAddress) ===
+ toChecksumHexAddress(value),
+ );
+
+ const ensName = matchedResolution?.domainName;
+
+ return ensName;
+ });
+}
+
function useFirstPartyContractNames(nameRequests: UseDisplayNameRequest[]) {
return nameRequests.map(({ type, value, variation }) => {
if (type !== NameType.ETHEREUM_ADDRESS) {
diff --git a/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap
index e8341e9a6014..8b5e0c683b51 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap
@@ -652,7 +652,7 @@ exports[` renders component for approve request 1`] = `
-
-
-
-
-
- 0x2e0D7...5d09B
-
-
-
-
-
+
+
+
+
+
+
+
-
-
@@ -195,7 +173,7 @@ exports[`NativeTransferInfo renders correctly 1`] = `
class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0"
>
G
@@ -221,6 +199,21 @@ exports[`NativeTransferInfo renders correctly 1`] = `
>
Interacting with
+
({
}));
describe('NativeTransferInfo', () => {
- it('renders correctly', async () => {
+ it('renders correctly', () => {
const state = getMockTokenTransferConfirmState({});
const mockStore = configureMockStore([])(state);
const { container } = renderWithConfirmContextProvider(
@@ -32,10 +30,6 @@ describe('NativeTransferInfo', () => {
mockStore,
);
- await waitFor(() => {
- expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument();
- });
-
expect(container).toMatchSnapshot();
});
});
diff --git a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap
index 8bd4a73fe440..77794c9e70a4 100644
--- a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap
@@ -15,39 +15,50 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = `
/>
- 1
-
+ />
-
-
-
-
-
- 0x2e0D7...5d09B
-
-
-
-
-
+
+
+
+
+
+
+
-
-
@@ -198,7 +174,7 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = `
class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0"
>
G
@@ -224,6 +200,21 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = `
>
Interacting with
+
({
}));
describe('NFTTokenTransferInfo', () => {
- it('renders correctly', async () => {
+ it('renders correctly', () => {
const state = getMockTokenTransferConfirmState({});
const mockStore = configureMockStore([])(state);
const { container } = renderWithConfirmContextProvider(
@@ -32,10 +30,6 @@ describe('NFTTokenTransferInfo', () => {
mockStore,
);
- await waitFor(() => {
- expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument();
- });
-
expect(container).toMatchSnapshot();
});
});
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap
index d03e04d2c673..1b7fd2aeb460 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap
@@ -13,7 +13,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
G
@@ -53,6 +53,21 @@ exports[`TokenDetailsSection renders correctly 1`] = `
>
Interacting with
+
-
- ?
-
-
- 0 Unknown
-
+
+
+
+
+
+
+
+
-
-
-
-
-
- 0x2e0D7...5d09B
-
-
-
-
-
+
+
+
+
+
+
+
-
-
@@ -195,7 +202,7 @@ exports[`TokenTransferInfo renders correctly 1`] = `
class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0"
>
G
@@ -221,6 +228,21 @@ exports[`TokenTransferInfo renders correctly 1`] = `
>
Interacting with
+
{
>
{
);
const tokenRow = transactionMeta.type !== TransactionType.simpleSend && (
-
+
({
}));
describe('TokenTransferInfo', () => {
- it('renders correctly', async () => {
+ it('renders correctly', () => {
const state = getMockTokenTransferConfirmState({});
const mockStore = configureMockStore([])(state);
const { container } = renderWithConfirmContextProvider(
@@ -32,10 +30,6 @@ describe('TokenTransferInfo', () => {
mockStore,
);
- await waitFor(() => {
- expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument();
- });
-
expect(container).toMatchSnapshot();
});
});
From 1d4fcc0c31b497b9c4d3a867d31b37c6dd38e32d Mon Sep 17 00:00:00 2001
From: Monte Lai
Date: Fri, 8 Nov 2024 19:22:56 +0800
Subject: [PATCH 15/18] fix: disable buy for btc testnet accounts (#28341)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR disables the Buy / sell button for btc testnet accounts
## **Related issues**
Fixes: https://github.com/MetaMask/accounts-planning/issues/655
## **Manual testing steps**
1. Create a testnet btc account
2. Go to the overview page
3. See that the buy/sell button is disabled.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../app/wallet-overview/btc-overview.test.tsx | 30 +++++++++++++++++++
.../app/wallet-overview/btc-overview.tsx | 12 +++++++-
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx
index abff2cb2b239..ffaed244e958 100644
--- a/ui/components/app/wallet-overview/btc-overview.test.tsx
+++ b/ui/components/app/wallet-overview/btc-overview.test.tsx
@@ -233,4 +233,34 @@ describe('BtcOverview', () => {
const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE);
expect(receiveButton).toBeInTheDocument();
});
+
+ it('"Buy & Sell" button is disabled for testnet accounts', () => {
+ const storeWithBtcBuyable = getStore({
+ metamask: {
+ ...mockMetamaskStore,
+ internalAccounts: {
+ ...mockMetamaskStore.internalAccounts,
+ accounts: {
+ [mockNonEvmAccount.id]: {
+ ...mockNonEvmAccount,
+ address: 'tb1q9lakrt5sw0w0twnc6ww4vxs7hm0q23e03286k8',
+ },
+ },
+ },
+ },
+ ramps: {
+ buyableChains: mockBuyableChainsWithBtc,
+ },
+ });
+
+ const { queryByTestId } = renderWithProvider(
+ ,
+ storeWithBtcBuyable,
+ );
+
+ const buyButton = queryByTestId(BTC_OVERVIEW_BUY);
+
+ expect(buyButton).toBeInTheDocument();
+ expect(buyButton).toBeDisabled();
+ });
});
diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx
index dc47df7567b5..2ddaefd92f58 100644
--- a/ui/components/app/wallet-overview/btc-overview.tsx
+++ b/ui/components/app/wallet-overview/btc-overview.tsx
@@ -1,11 +1,16 @@
import React from 'react';
import { useSelector } from 'react-redux';
import {
+ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
+ getMultichainIsMainnet,
+ ///: END:ONLY_INCLUDE_IF
getMultichainProviderConfig,
getMultichainSelectedAccountCachedBalance,
} from '../../../selectors/multichain';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
import { getIsBitcoinBuyable } from '../../../ducks/ramps';
+import { getSelectedInternalAccount } from '../../../selectors';
+import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
///: END:ONLY_INCLUDE_IF
import { CoinOverview } from './coin-overview';
@@ -17,6 +22,11 @@ const BtcOverview = ({ className }: BtcOverviewProps) => {
const { chainId } = useSelector(getMultichainProviderConfig);
const balance = useSelector(getMultichainSelectedAccountCachedBalance);
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
+ const selectedAccount = useSelector(getSelectedInternalAccount);
+ const isBtcMainnetAccount = useMultichainSelector(
+ getMultichainIsMainnet,
+ selectedAccount,
+ );
const isBtcBuyable = useSelector(getIsBitcoinBuyable);
///: END:ONLY_INCLUDE_IF
@@ -31,7 +41,7 @@ const BtcOverview = ({ className }: BtcOverviewProps) => {
isSwapsChain={false}
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
isBridgeChain={false}
- isBuyableChain={isBtcBuyable}
+ isBuyableChain={isBtcBuyable && isBtcMainnetAccount}
///: END:ONLY_INCLUDE_IF
/>
);
From 152a50ef8fec9d8a038313c50df107f97ec80c20 Mon Sep 17 00:00:00 2001
From: Danica Shen
Date: Fri, 8 Nov 2024 14:13:07 +0000
Subject: [PATCH 16/18] feat(1852): Implement sentry user report on error
screen (#27857)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Sentry provides a feature to collect user feedback to users when an
issue occurs. This could be a great help to fix Sentry issues which
sometimes come without a lot of context and are difficult to reproduce.
Hence we use out-of-box solution from sentry to implement [User Feedback
Widget](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/#user-feedback-widget)
via `Sentry.feedbackIntegration`. You can find more technical details in
[this
comment](https://github.com/MetaMask/MetaMask-planning/issues/1852#issuecomment-2392480544).
Design Figma link
[here](https://www.figma.com/design/DM5G1pyp74sMJwyKbw1KiR/Error-message-and-bug-report?node-id=1-4243&node-type=frame&t=iZI13qsxataukM4a-0).
### What's expected in this PR:
- Refactor original `ui/pages/error/error.component.js` to typescript,
clean up / update language content, and improve the layout based on new
design (see above Figma link) that would be consistent as mobile
implementation
- Add a new option in `develop options` to cause a page crash by remove
one language file (for me was easiest way to trigger), which will bring
us to error page
- In new error page, we have 3 options:
1. Describe what happened - open a form to sent a message to sentry
2. Contact support - existing link to redirect to
`process.env.SUPPORT_REQUEST_LINK`
3. Try again - close the extension and allow user to open again
- Convert `ui/ducks/locale/locale.js` to typescript and add related
tests
- Add e2e tests with POM pattern
**This is the scenario for extension:**
- GIVEN a user has MM installed
- AND Sentry is enabled (user enabled MetaMetrics)
- WHEN an unhandled issue occurs in MM
- THEN MM crashes
- AND an event is sent to Sentry
- AND user is given the possibility to describe what happened to him by
filling a form
- AND his feedback gets paired to the Sentry event once user presses the
"submit" button at the bottom of the form
- AND user is given more comprehensive error screen when it crashes
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27857?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/MetaMask-planning/issues/1852
## **Manual testing steps**
1. Set up sentry
(https://github.com/MetaMask/metamask-extension/blob/develop/development/README.md)
2. Add DSN to `SENTRY_DSN_DEV` in local env set up, and mark
`ENABLE_SETTINGS_PAGE_DEV_OPTIONS`=true
3. Run `yarn webpack --watch --sentry`
4. Ensure `Participate in MetaMetrics` is opt in
5. Click "develop options" in settings
6. Click "Generate A Page Crash" button
7. User is redirected to new error page
8. Click `Describe what happened` can open a sentry feedback form, then
in your sentry project you can find the submitted form within `User
Feedback` section
9. Click `Contact support` will redirect user to metamask support page
10. Click `Try again` will close the extension and ready for reload
## **Screenshots/Recordings**
### **Before**
### **After**
https://github.com/user-attachments/assets/56145a44-82d3-4d07-be03-87d81ee6a9d7
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/_locales/de/messages.json | 12 -
app/_locales/el/messages.json | 12 -
app/_locales/en/messages.json | 40 ++-
app/_locales/en_GB/messages.json | 12 -
app/_locales/es/messages.json | 12 -
app/_locales/es_419/messages.json | 12 -
app/_locales/fr/messages.json | 12 -
app/_locales/hi/messages.json | 12 -
app/_locales/id/messages.json | 12 -
app/_locales/it/messages.json | 4 -
app/_locales/ja/messages.json | 12 -
app/_locales/ko/messages.json | 12 -
app/_locales/ph/messages.json | 12 -
app/_locales/pt/messages.json | 12 -
app/_locales/pt_BR/messages.json | 12 -
app/_locales/ru/messages.json | 12 -
app/_locales/tl/messages.json | 12 -
app/_locales/tr/messages.json | 12 -
app/_locales/vi/messages.json | 12 -
app/_locales/zh_CN/messages.json | 12 -
app/_locales/zh_TW/messages.json | 12 -
privacy-snapshot.json | 2 +
shared/constants/metametrics.ts | 4 +
shared/modules/i18n.test.ts | 16 +-
shared/modules/i18n.ts | 3 +-
test/data/mock-state.json | 8 +-
test/e2e/helpers.js | 5 +-
.../pages/developer-options-page.ts | 38 +++
test/e2e/page-objects/pages/error-page.ts | 95 ++++++
test/e2e/page-objects/pages/settings-page.ts | 10 +
.../metrics/developer-options-sentry.spec.ts | 88 +++++
test/e2e/tests/metrics/errors.spec.js | 3 +-
ui/ducks/locale/locale.js | 42 ---
ui/ducks/locale/locale.test.ts | 80 ++++-
ui/ducks/locale/locale.ts | 108 ++++++
ui/pages/error-page/error-component.test.tsx | 192 +++++++++++
ui/pages/error-page/error-page.component.tsx | 312 ++++++++++++++++++
ui/pages/error-page/index.scss | 41 +++
ui/pages/error-page/index.ts | 1 +
ui/pages/error/error.component.js | 106 ------
ui/pages/error/index.js | 1 -
ui/pages/error/index.scss | 47 ---
ui/pages/index.js | 7 +-
ui/pages/pages.scss | 2 +-
.../developer-options-tab.test.tsx.snap | 46 ++-
.../developer-options-tab.tsx | 2 +-
.../developer-options-tab/sentry-test.tsx | 66 +++-
ui/pages/settings/settings.component.js | 5 +-
48 files changed, 1124 insertions(+), 478 deletions(-)
create mode 100644 test/e2e/page-objects/pages/developer-options-page.ts
create mode 100644 test/e2e/page-objects/pages/error-page.ts
create mode 100644 test/e2e/tests/metrics/developer-options-sentry.spec.ts
delete mode 100644 ui/ducks/locale/locale.js
create mode 100644 ui/ducks/locale/locale.ts
create mode 100644 ui/pages/error-page/error-component.test.tsx
create mode 100644 ui/pages/error-page/error-page.component.tsx
create mode 100644 ui/pages/error-page/index.scss
create mode 100644 ui/pages/error-page/index.ts
delete mode 100644 ui/pages/error/error.component.js
delete mode 100644 ui/pages/error/index.js
delete mode 100644 ui/pages/error/index.scss
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index 19c7731c778f..f62b15c08c93 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -1696,10 +1696,6 @@
"message": "Code: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Fehlerdetails",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Fehler beim Abrufen der Liste sicherer Ketten, bitte mit Vorsicht fortfahren."
},
@@ -1711,14 +1707,6 @@
"message": "Code: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Versuchen Sie es erneut, indem Sie die Seite neu laden oder kontaktieren Sie den Support $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Versuchen Sie es erneut, indem das Popup schließen und neu laden oder kontaktieren Sie den Support $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask hat einen Fehler festgestellt.",
"description": "Title of generic error page"
diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json
index ac4542893959..3bede7c85edb 100644
--- a/app/_locales/el/messages.json
+++ b/app/_locales/el/messages.json
@@ -1696,10 +1696,6 @@
"message": "Κωδικός: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Λεπτομέρειες σφάλματος",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Σφάλμα κατά τη λήψη της λίστας ασφαλών αλυσίδων, συνεχίστε με προσοχή."
},
@@ -1711,14 +1707,6 @@
"message": "Κωδικός: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Προσπαθήστε ξανά με επαναφόρτωση της σελίδας ή επικοινωνήστε με την υποστήριξη $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Προσπαθήστε ξανά κλείνοντας και ανοίγοντας ξανά το αναδυόμενο παράθυρο ή επικοινωνήστε με την υποστήριξη $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "Το MetaMask αντιμετώπισε ένα σφάλμα",
"description": "Title of generic error page"
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 202dc04c2fa8..56e3614e3f39 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -1940,10 +1940,6 @@
"message": "Code: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Error details",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Error while getting safe chain list, please continue with caution."
},
@@ -1955,18 +1951,42 @@
"message": "Code: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Try again by reloading the page, or contact support $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
+ "errorPageContactSupport": {
+ "message": "Contact support",
+ "description": "Button for contact MM support"
+ },
+ "errorPageDescribeUsWhatHappened": {
+ "message": "Describe what happened",
+ "description": "Button for submitting report to sentry"
+ },
+ "errorPageInfo": {
+ "message": "Your information can’t be shown. Don’t worry, your wallet and funds are safe.",
+ "description": "Information banner shown in the error page"
+ },
+ "errorPageMessageTitle": {
+ "message": "Error message",
+ "description": "Title for description, which is displayed for debugging purposes"
},
- "errorPagePopupMessage": {
- "message": "Try again by closing and reopening the popup, or contact support $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
+ "errorPageSentryFormTitle": {
+ "message": "Describe what happened",
+ "description": "In sentry feedback form, The title at the top of the feedback form."
+ },
+ "errorPageSentryMessagePlaceholder": {
+ "message": "Sharing details like how we can reproduce the bug will help us fix the problem.",
+ "description": "In sentry feedback form, The placeholder for the feedback description input field."
+ },
+ "errorPageSentrySuccessMessageText": {
+ "message": "Thanks! We will take a look soon.",
+ "description": "In sentry feedback form, The message displayed after a successful feedback submission."
},
"errorPageTitle": {
"message": "MetaMask encountered an error",
"description": "Title of generic error page"
},
+ "errorPageTryAgain": {
+ "message": "Try again",
+ "description": "Button for try again"
+ },
"errorStack": {
"message": "Stack:",
"description": "Title for error stack, which is displayed for debugging purposes"
diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json
index 8dd2e32dac39..9ee7771947ba 100644
--- a/app/_locales/en_GB/messages.json
+++ b/app/_locales/en_GB/messages.json
@@ -1792,10 +1792,6 @@
"message": "Code: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Error details",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Error while getting safe chain list, please continue with caution."
},
@@ -1807,14 +1803,6 @@
"message": "Code: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Try again by reloading the page, or contact support $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Try again by closing and reopening the popup, or contact support $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask encountered an error",
"description": "Title of generic error page"
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index 1d23b736fbfd..f13313976e74 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -1693,10 +1693,6 @@
"message": "Código: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Detalles del error",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Error al obtener la lista de cadenas seguras, por favor continúe con precaución."
},
@@ -1708,14 +1704,6 @@
"message": "Código: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask encontró un error",
"description": "Title of generic error page"
diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json
index cebfc3cef106..511ee6cbef71 100644
--- a/app/_locales/es_419/messages.json
+++ b/app/_locales/es_419/messages.json
@@ -686,10 +686,6 @@
"message": "Código: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Detalles del error",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorMessage": {
"message": "Mensaje: $1",
"description": "Displayed error message for debugging purposes. $1 is the error message"
@@ -698,14 +694,6 @@
"message": "Código: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask encontró un error",
"description": "Title of generic error page"
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index e9b8fd588b89..c785d2e0a00a 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -1696,10 +1696,6 @@
"message": "Code : $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Détails de l’erreur",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Erreur lors de l’obtention de la liste des chaînes sécurisées, veuillez continuer avec précaution."
},
@@ -1711,14 +1707,6 @@
"message": "Code : $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Essayez à nouveau en rechargeant la page, ou contactez le service d’assistance $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Réessayez en fermant puis en rouvrant le pop-up, ou contactez le service d’assistance $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask a rencontré une erreur",
"description": "Title of generic error page"
diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json
index 9b2c06f96c11..554bb2e91b84 100644
--- a/app/_locales/hi/messages.json
+++ b/app/_locales/hi/messages.json
@@ -1696,10 +1696,6 @@
"message": "कोड: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "गड़बड़ी की जानकारी",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "सेफ चेन लिस्ट पाते समय गड़बड़ी हुई, कृपया सावधानी के साथ जारी रखें।"
},
@@ -1711,14 +1707,6 @@
"message": "कोड: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "पेज को दोबारा लोड करके फिर से कोशिश करें या सपोर्ट $1 से कॉन्टेक्ट करें।",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "पॉपअप को बंद करके और फिर से खोलने की कोशिश करें या $1 पर सपोर्ट से कॉन्टेक्ट करें।",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask में कोई गड़बड़ी हुई",
"description": "Title of generic error page"
diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json
index 1331d7b9c482..8e60e6fe5a50 100644
--- a/app/_locales/id/messages.json
+++ b/app/_locales/id/messages.json
@@ -1696,10 +1696,6 @@
"message": "Kode: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Detail kesalahan",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Terjadi kesalahan saat mendapatkan daftar rantai aman, lanjutkan dengan hati-hati."
},
@@ -1711,14 +1707,6 @@
"message": "Kode: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Coba lagi dengan memuat kembali halaman, atau hubungi dukungan $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Coba lagi dengan menutup dan membuka kembali sembulan, atau hubungi dukungan $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask mengalami kesalahan",
"description": "Title of generic error page"
diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json
index 0c79dd8fc6fc..05cccdac0359 100644
--- a/app/_locales/it/messages.json
+++ b/app/_locales/it/messages.json
@@ -736,10 +736,6 @@
"message": "Codice: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Dettagli Errore",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorMessage": {
"message": "Messaggio: $1",
"description": "Displayed error message for debugging purposes. $1 is the error message"
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 3107eb3cb152..69824ae33b52 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -1696,10 +1696,6 @@
"message": "コード: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "エラーの詳細",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "安全なチェーンリストの取得中にエラーが発生しました。慎重に続けてください。"
},
@@ -1711,14 +1707,6 @@
"message": "コード: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "ページを再ロードしてもう一度実行するか、$1からサポートまでお問い合わせください。",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "ポップアップを閉じてから再び開いてもう一度実行するか、$1からサポートまでお問い合わせください。",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMaskにエラーが発生しました",
"description": "Title of generic error page"
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index 6dc468cdd5ef..f160b3dccbc2 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -1696,10 +1696,6 @@
"message": "코드: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "오류 세부 정보",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "안전 체인 목록을 가져오는 동안 오류가 발생했습니다. 주의하여 계속 진행하세요."
},
@@ -1711,14 +1707,6 @@
"message": "코드: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "페이지를 새로고침하여 다시 시도하거나 $1로 지원을 요청하여 도움을 받으세요.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "팝업을 닫은 후 다시 열어 다시 시도하거나 $1에서 지원을 요청하여 도움을 받으세요.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask 오류 발생",
"description": "Title of generic error page"
diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json
index e6b4bc3e7811..3c08d76cb186 100644
--- a/app/_locales/ph/messages.json
+++ b/app/_locales/ph/messages.json
@@ -423,10 +423,6 @@
"message": "Code: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Mga Detalye ng Error",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorMessage": {
"message": "Mensahe: $1",
"description": "Displayed error message for debugging purposes. $1 is the error message"
@@ -435,14 +431,6 @@
"message": "Code: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Subukan ulit sa pamamagitan ng pag-reload ng page, o makipag-ugnayan sa suporta sa $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Subukan ulit sa pamamagitan ng pagsara at pagbukas ulit ng popup, o makipag-ugnayan sa suporta sa $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "Nagkaroon ng error sa MetaMask",
"description": "Title of generic error page"
diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json
index 03f41c1c62b8..589a58e94907 100644
--- a/app/_locales/pt/messages.json
+++ b/app/_locales/pt/messages.json
@@ -1696,10 +1696,6 @@
"message": "Código: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Detalhes do erro",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Erro ao obter uma lista segura da cadeia. Por favor, prossiga com cautela."
},
@@ -1711,14 +1707,6 @@
"message": "Código: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "A MetaMask encontrou um erro",
"description": "Title of generic error page"
diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json
index 58d7f6ea8718..ebf2af88c186 100644
--- a/app/_locales/pt_BR/messages.json
+++ b/app/_locales/pt_BR/messages.json
@@ -686,10 +686,6 @@
"message": "Código: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Detalhes do erro",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorMessage": {
"message": "Mensagem: $1",
"description": "Displayed error message for debugging purposes. $1 is the error message"
@@ -698,14 +694,6 @@
"message": "Código: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "A MetaMask encontrou um erro",
"description": "Title of generic error page"
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index e56afc418785..9462d9c6eb4a 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -1696,10 +1696,6 @@
"message": "Код: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Сведения об ошибке",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Ошибка при получении списка безопасных блокчейнов. Продолжайте с осторожностью."
},
@@ -1711,14 +1707,6 @@
"message": "Код: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Повторите попытку, перезагрузив страницу, или обратитесь в поддержку $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Повторите попытку, закрыв и вновь открыв всплывающее окно, или обратитесь в поддержку $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask обнаружил ошибку",
"description": "Title of generic error page"
diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json
index af56d958c313..41612a6ce177 100644
--- a/app/_locales/tl/messages.json
+++ b/app/_locales/tl/messages.json
@@ -1696,10 +1696,6 @@
"message": "Code: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Mga Detalye ng Error",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "May error habang kinukuha ang ligtas na chain list, mangyaring magpatuloy nang may pag-iingat."
},
@@ -1711,14 +1707,6 @@
"message": "Code: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Subukang muling i-reload ang page, o kontakin ang support $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Subukan muli sa pamamagitan ng pagsasara o muling pagbubukas ng pop-up, kontakin ang support $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "Nagkaroon ng error sa MetaMask",
"description": "Title of generic error page"
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index 6732513395d8..f11cc0d17523 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -1696,10 +1696,6 @@
"message": "Kod: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Hata ayrıntıları",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Güvenli zincir listesi alınırken hata oluştu, lütfen dikkatli bir şekilde devam edin."
},
@@ -1711,14 +1707,6 @@
"message": "Kod: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Sayfayı yeniden yükleyerek tekrar deneyin veya $1 destek bölümümüze ulaşın.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Açılır pencereyi kapatarak ve yeniden açarak tekrar deneyin $1 destek bölümümüze ulaşın.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask bir hata ile karşılaştı",
"description": "Title of generic error page"
diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json
index 9f6ff5119366..dd518df1bf22 100644
--- a/app/_locales/vi/messages.json
+++ b/app/_locales/vi/messages.json
@@ -1696,10 +1696,6 @@
"message": "Mã: $1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "Chi tiết về lỗi",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "Lỗi khi lấy danh sách chuỗi an toàn, vui lòng tiếp tục một cách thận trọng."
},
@@ -1711,14 +1707,6 @@
"message": "Mã: $1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "Hãy thử lại bằng cách tải lại trang hoặc liên hệ với bộ phận hỗ trợ $1.",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "Hãy thử lại bằng cách đóng và mở lại cửa sổ bật lên hoặc liên hệ với bộ phận hỗ trợ $1.",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask đã gặp lỗi",
"description": "Title of generic error page"
diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json
index eef758dacdad..818f1ffdb82b 100644
--- a/app/_locales/zh_CN/messages.json
+++ b/app/_locales/zh_CN/messages.json
@@ -1696,10 +1696,6 @@
"message": "代码:$1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "错误详情",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorGettingSafeChainList": {
"message": "获取安全链列表时出错,请谨慎继续。"
},
@@ -1711,14 +1707,6 @@
"message": "代码:$1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "通过重新加载页面再试一次,或联系支持团队 $1。",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "通过关闭并重新打开弹出窗口再试一次,或联系支持团队 $1。",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask 遇到了一个错误",
"description": "Title of generic error page"
diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json
index 7a7fdb68cf1f..29953f67c941 100644
--- a/app/_locales/zh_TW/messages.json
+++ b/app/_locales/zh_TW/messages.json
@@ -425,10 +425,6 @@
"message": "代碼:$1",
"description": "Displayed error code for debugging purposes. $1 is the error code"
},
- "errorDetails": {
- "message": "錯誤詳細資訊",
- "description": "Title for collapsible section that displays error details for debugging purposes"
- },
"errorMessage": {
"message": "訊息:$1",
"description": "Displayed error message for debugging purposes. $1 is the error message"
@@ -437,14 +433,6 @@
"message": "代碼:$1",
"description": "Displayed error name for debugging purposes. $1 is the error name"
},
- "errorPageMessage": {
- "message": "重新整理頁面然後再試一次,或從$1聯絡我們尋求支援。",
- "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
- "errorPagePopupMessage": {
- "message": "重新開啟彈跳視窗然後再試一次,或從$1聯絡我們尋求支援。",
- "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets."
- },
"errorPageTitle": {
"message": "MetaMask 遭遇錯誤",
"description": "Title of generic error page"
diff --git a/privacy-snapshot.json b/privacy-snapshot.json
index 6e041ea3d71b..817d1e102bff 100644
--- a/privacy-snapshot.json
+++ b/privacy-snapshot.json
@@ -56,6 +56,8 @@
"sourcify.dev",
"start.metamask.io",
"static.cx.metamask.io",
+ "support.metamask.io",
+ "support.metamask-institutional.io",
"swap.api.cx.metamask.io",
"test.metamask-phishing.io",
"token.api.cx.metamask.io",
diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts
index 4096d04b11d6..103a24c2463d 100644
--- a/shared/constants/metametrics.ts
+++ b/shared/constants/metametrics.ts
@@ -158,6 +158,10 @@ export type MetaMetricsEventOptions = {
* as not conforming to our schema.
*/
matomoEvent?: boolean;
+ /**
+ * Values that can used in the "properties" tracking object as keys,
+ */
+ contextPropsIntoEventProperties?: string | string[];
};
export type MetaMetricsEventFragment = {
diff --git a/shared/modules/i18n.test.ts b/shared/modules/i18n.test.ts
index 8452ef48238c..7a2f49bd9f68 100644
--- a/shared/modules/i18n.test.ts
+++ b/shared/modules/i18n.test.ts
@@ -109,7 +109,21 @@ describe('I18N Module', () => {
);
});
- it('throws if test env set', () => {
+ it('throws if IN_TEST is set true', () => {
+ expect(() =>
+ getMessage(
+ FALLBACK_LOCALE,
+ {} as unknown as I18NMessageDict,
+ keyMock,
+ ),
+ ).toThrow(
+ `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
+ );
+ });
+
+ it('throws if ENABLE_SETTINGS_PAGE_DEV_OPTIONS is set true', () => {
+ process.env.IN_TEST = String(false);
+ process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS = String(true);
expect(() =>
getMessage(
FALLBACK_LOCALE,
diff --git a/shared/modules/i18n.ts b/shared/modules/i18n.ts
index d19cfa4b3b11..b5c22c869b54 100644
--- a/shared/modules/i18n.ts
+++ b/shared/modules/i18n.ts
@@ -177,7 +177,7 @@ function missingKeyError(
onError?.(error);
log.error(error);
- if (process.env.IN_TEST) {
+ if (process.env.IN_TEST || process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) {
throw error;
}
}
@@ -188,7 +188,6 @@ function missingKeyError(
warned[localeCode] = warned[localeCode] ?? {};
warned[localeCode][key] = true;
-
log.warn(
`Translator - Unable to find value of key "${key}" for locale "${localeCode}"`,
);
diff --git a/test/data/mock-state.json b/test/data/mock-state.json
index 184787b07836..80e5499447d7 100644
--- a/test/data/mock-state.json
+++ b/test/data/mock-state.json
@@ -46,7 +46,13 @@
"mostRecentOverviewPage": "/mostRecentOverviewPage"
},
"localeMessages": {
- "currentLocale": "en"
+ "currentLocale": "en",
+ "current": {
+ "user": "user"
+ },
+ "en": {
+ "user": "user"
+ }
},
"metamask": {
"use4ByteResolution": true,
diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js
index c3705c1ebf6c..1c3f53b30224 100644
--- a/test/e2e/helpers.js
+++ b/test/e2e/helpers.js
@@ -55,7 +55,7 @@ const convertETHToHexGwei = (eth) => convertToHexValue(eth * 10 ** 18);
/**
*
* @param {object} options
- * @param {(fixtures: Fixtures) => Promise} testSuite
+ * @param {({driver: Driver, mockedEndpoint: MockedEndpoint}: TestSuiteArguments) => Promise} testSuite
*/
async function withFixtures(options, testSuite) {
const {
@@ -1308,6 +1308,8 @@ async function openMenuSafe(driver) {
}
}
+const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu;
+
module.exports = {
DAPP_HOST_ADDRESS,
DAPP_URL,
@@ -1379,4 +1381,5 @@ module.exports = {
getSelectedAccountAddress,
tempToggleSettingRedesignedConfirmations,
openMenuSafe,
+ sentryRegEx,
};
diff --git a/test/e2e/page-objects/pages/developer-options-page.ts b/test/e2e/page-objects/pages/developer-options-page.ts
new file mode 100644
index 000000000000..c15f6c767a82
--- /dev/null
+++ b/test/e2e/page-objects/pages/developer-options-page.ts
@@ -0,0 +1,38 @@
+import { Driver } from '../../webdriver/driver';
+
+class DevelopOptions {
+ private readonly driver: Driver;
+
+ // Locators
+ private readonly generatePageCrashButton: string =
+ '[data-testid="developer-options-generate-page-crash-button"]';
+
+ private readonly developOptionsPageTitle: object = {
+ text: 'Developer Options',
+ css: 'h4',
+ };
+
+ constructor(driver: Driver) {
+ this.driver = driver;
+ }
+
+ async check_pageIsLoaded(): Promise {
+ try {
+ await this.driver.waitForSelector(this.developOptionsPageTitle);
+ } catch (e) {
+ console.log(
+ 'Timeout while waiting for Developer options page to be loaded',
+ e,
+ );
+ throw e;
+ }
+ console.log('Developer option page is loaded');
+ }
+
+ async clickGenerateCrashButton(): Promise {
+ console.log('Generate a page crash in Developer option page');
+ await this.driver.clickElement(this.generatePageCrashButton);
+ }
+}
+
+export default DevelopOptions;
diff --git a/test/e2e/page-objects/pages/error-page.ts b/test/e2e/page-objects/pages/error-page.ts
new file mode 100644
index 000000000000..76acacdcf9dd
--- /dev/null
+++ b/test/e2e/page-objects/pages/error-page.ts
@@ -0,0 +1,95 @@
+import { Driver } from '../../webdriver/driver';
+import HeaderNavbar from './header-navbar';
+import SettingsPage from './settings-page';
+import DevelopOptionsPage from './developer-options-page';
+
+const FEEDBACK_MESSAGE =
+ 'Message: Unable to find value of key "developerOptions" for locale "en"';
+
+class ErrorPage {
+ private readonly driver: Driver;
+
+ // Locators
+ private readonly errorPageTitle: object = {
+ text: 'MetaMask encountered an error',
+ css: 'h3',
+ };
+
+ private readonly errorMessage = '[data-testid="error-page-error-message"]';
+
+ private readonly sendReportToSentryButton =
+ '[data-testid="error-page-describe-what-happened-button"]';
+
+ private readonly sentryReportForm =
+ '[data-testid="error-page-sentry-feedback-modal"]';
+
+ private readonly contactSupportButton =
+ '[data-testid="error-page-contact-support-button"]';
+
+ private readonly sentryFeedbackTextarea =
+ '[data-testid="error-page-sentry-feedback-textarea"]';
+
+ private readonly sentryFeedbackSubmitButton =
+ '[data-testid="error-page-sentry-feedback-submit-button"]';
+
+ private readonly sentryFeedbackSuccessModal =
+ '[data-testid="error-page-sentry-feedback-success-modal"]';
+
+ constructor(driver: Driver) {
+ this.driver = driver;
+ }
+
+ async check_pageIsLoaded(): Promise {
+ try {
+ await this.driver.waitForSelector(this.errorPageTitle);
+ } catch (e) {
+ console.log('Timeout while waiting for Error page to be loaded', e);
+ throw e;
+ }
+ console.log('Error page is loaded');
+ }
+
+ async triggerPageCrash(): Promise {
+ const headerNavbar = new HeaderNavbar(this.driver);
+ await headerNavbar.openSettingsPage();
+ const settingsPage = new SettingsPage(this.driver);
+ await settingsPage.check_pageIsLoaded();
+ await settingsPage.goToDevelopOptionSettings();
+
+ const developOptionsPage = new DevelopOptionsPage(this.driver);
+ await developOptionsPage.check_pageIsLoaded();
+ await developOptionsPage.clickGenerateCrashButton();
+ }
+
+ async validate_errorMessage(): Promise {
+ await this.driver.waitForSelector({
+ text: `Message: Unable to find value of key "developerOptions" for locale "en"`,
+ css: this.errorMessage,
+ });
+ }
+
+ async submitToSentryUserFeedbackForm(): Promise {
+ console.log(`Open sentry user feedback form in error page`);
+ await this.driver.clickElement(this.sendReportToSentryButton);
+ await this.driver.waitForSelector(this.sentryReportForm);
+ await this.driver.fill(this.sentryFeedbackTextarea, FEEDBACK_MESSAGE);
+ await this.driver.clickElementAndWaitToDisappear(
+ this.sentryFeedbackSubmitButton,
+ );
+ }
+
+ async contactAndValidateMetaMaskSupport(): Promise {
+ console.log(`Contact metamask support form in a separate page`);
+ await this.driver.waitUntilXWindowHandles(1);
+ await this.driver.clickElement(this.contactSupportButton);
+ // metamask, help page
+ await this.driver.waitUntilXWindowHandles(2);
+ }
+
+ async waitForSentrySuccessModal(): Promise {
+ await this.driver.waitForSelector(this.sentryFeedbackSuccessModal);
+ await this.driver.assertElementNotPresent(this.sentryFeedbackSuccessModal);
+ }
+}
+
+export default ErrorPage;
diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts
index 547f9e43a34e..c029e34efc7e 100644
--- a/test/e2e/page-objects/pages/settings-page.ts
+++ b/test/e2e/page-objects/pages/settings-page.ts
@@ -9,6 +9,11 @@ class SettingsPage {
css: '.tab-bar__tab__content__title',
};
+ private readonly developerOptionsButton: object = {
+ text: 'Developer Options',
+ css: '.tab-bar__tab__content__title',
+ };
+
private readonly settingsPageTitle: object = {
text: 'Settings',
css: 'h3',
@@ -32,6 +37,11 @@ class SettingsPage {
console.log('Navigating to Experimental Settings page');
await this.driver.clickElement(this.experimentalSettingsButton);
}
+
+ async goToDevelopOptionSettings(): Promise {
+ console.log('Navigating to Develop options page');
+ await this.driver.clickElement(this.developerOptionsButton);
+ }
}
export default SettingsPage;
diff --git a/test/e2e/tests/metrics/developer-options-sentry.spec.ts b/test/e2e/tests/metrics/developer-options-sentry.spec.ts
new file mode 100644
index 000000000000..3c20f931f302
--- /dev/null
+++ b/test/e2e/tests/metrics/developer-options-sentry.spec.ts
@@ -0,0 +1,88 @@
+import { Suite } from 'mocha';
+import { MockttpServer } from 'mockttp';
+import { withFixtures, sentryRegEx } from '../../helpers';
+import FixtureBuilder from '../../fixture-builder';
+import { Driver } from '../../webdriver/driver';
+import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
+import SettingsPage from '../../page-objects/pages/settings-page';
+import HeaderNavbar from '../../page-objects/pages/header-navbar';
+import DevelopOptions from '../../page-objects/pages/developer-options-page';
+import ErrorPage from '../../page-objects/pages/error-page';
+
+const triggerCrash = async (driver: Driver): Promise => {
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.openSettingsPage();
+ const settingsPage = new SettingsPage(driver);
+ await settingsPage.check_pageIsLoaded();
+ await settingsPage.goToDevelopOptionSettings();
+
+ const developOptionsPage = new DevelopOptions(driver);
+ await developOptionsPage.check_pageIsLoaded();
+ await developOptionsPage.clickGenerateCrashButton();
+};
+
+async function mockSentryError(mockServer: MockttpServer) {
+ return [
+ await mockServer
+ .forPost(sentryRegEx)
+ .withBodyIncluding('feedback')
+ .thenCallback(() => {
+ return {
+ statusCode: 200,
+ json: {},
+ };
+ }),
+ ];
+}
+
+describe('Developer Options - Sentry', function (this: Suite) {
+ it('gives option to cause a page crash and provides sentry form to report', async function () {
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder()
+ .withMetaMetricsController({
+ metaMetricsId: 'fake-metrics-id',
+ participateInMetaMetrics: true,
+ })
+ .build(),
+ title: this.test?.fullTitle(),
+ testSpecificMock: mockSentryError,
+ ignoredConsoleErrors: [
+ 'Error#1: Unable to find value of key "developerOptions" for locale "en"',
+ 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.',
+ ],
+ },
+ async ({ driver }: { driver: Driver }) => {
+ await loginWithBalanceValidation(driver);
+ await triggerCrash(driver);
+ const errorPage = new ErrorPage(driver);
+ await errorPage.check_pageIsLoaded();
+ await errorPage.validate_errorMessage();
+ await errorPage.submitToSentryUserFeedbackForm();
+ await errorPage.waitForSentrySuccessModal();
+ },
+ );
+ });
+
+ it('gives option to cause a page crash and offer contact support option', async function () {
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder().build(),
+ title: this.test?.fullTitle(),
+ ignoredConsoleErrors: [
+ 'Error#1: Unable to find value of key "developerOptions" for locale "en"',
+ 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.',
+ ],
+ },
+ async ({ driver }: { driver: Driver }) => {
+ await loginWithBalanceValidation(driver);
+ await triggerCrash(driver);
+
+ const errorPage = new ErrorPage(driver);
+ await errorPage.check_pageIsLoaded();
+
+ await errorPage.contactAndValidateMetaMaskSupport();
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js
index 3b003b044b5a..d4c27a87aba6 100644
--- a/test/e2e/tests/metrics/errors.spec.js
+++ b/test/e2e/tests/metrics/errors.spec.js
@@ -13,6 +13,7 @@ const {
convertToHexValue,
logInWithBalanceValidation,
withFixtures,
+ sentryRegEx,
} = require('../../helpers');
const { PAGES } = require('../../webdriver/driver');
@@ -181,8 +182,6 @@ function getMissingProperties(complete, object) {
}
describe('Sentry errors', function () {
- const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu;
-
const migrationError =
process.env.SELENIUM_BROWSER === Browser.CHROME
? `"type":"TypeError","value":"Cannot read properties of undefined (reading 'version')`
diff --git a/ui/ducks/locale/locale.js b/ui/ducks/locale/locale.js
deleted file mode 100644
index 5118a749ab58..000000000000
--- a/ui/ducks/locale/locale.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { createSelector } from 'reselect';
-import * as actionConstants from '../../store/actionConstants';
-
-export default function reduceLocaleMessages(state = {}, { type, payload }) {
- switch (type) {
- case actionConstants.SET_CURRENT_LOCALE:
- return {
- ...state,
- current: payload.messages,
- currentLocale: payload.locale,
- };
- default:
- return state;
- }
-}
-
-/**
- * This selector returns a code from file://./../../../app/_locales/index.json.
- *
- * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that.
- *
- * @param state
- * @returns {string} the user's selected locale.
- * These codes are not safe to use with the Intl API.
- */
-export const getCurrentLocale = (state) => state.localeMessages.currentLocale;
-
-/**
- * This selector returns a
- * [BCP 47 Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag)
- * for use with the Intl API.
- *
- * @returns {Intl.UnicodeBCP47LocaleIdentifier} the user's selected locale.
- */
-export const getIntlLocale = createSelector(
- getCurrentLocale,
- (locale) => Intl.getCanonicalLocales(locale?.replace(/_/gu, '-'))[0],
-);
-
-export const getCurrentLocaleMessages = (state) => state.localeMessages.current;
-
-export const getEnLocaleMessages = (state) => state.localeMessages.en;
diff --git a/ui/ducks/locale/locale.test.ts b/ui/ducks/locale/locale.test.ts
index 37fc1f99e29a..67627bb73423 100644
--- a/ui/ducks/locale/locale.test.ts
+++ b/ui/ducks/locale/locale.test.ts
@@ -1,32 +1,80 @@
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import locales from '../../../app/_locales/index.json';
-import { getIntlLocale } from './locale';
+import testData from '../../../test/data/mock-state.json';
+import {
+ getCurrentLocale,
+ getIntlLocale,
+ getCurrentLocaleMessages,
+ getEnLocaleMessages,
+} from './locale';
+
+// Mock state creation functions
const createMockStateWithLocale = (locale: string) => ({
localeMessages: { currentLocale: locale },
});
-describe('getIntlLocale', () => {
- it('returns the canonical BCP 47 language tag for the currently selected locale', () => {
- const mockState = createMockStateWithLocale('ab-cd');
+describe('Locale Selectors', () => {
+ describe('getCurrentLocale', () => {
+ it('returns the current locale from the state', () => {
+ expect(getCurrentLocale(testData)).toBe('en');
+ });
- expect(getIntlLocale(mockState)).toBe('ab-CD');
+ it('returns undefined if no current locale is set', () => {
+ const newAppState = {
+ ...testData,
+ localeMessages: {
+ currentLocale: undefined,
+ },
+ };
+ expect(getCurrentLocale(newAppState)).toBeUndefined();
+ });
});
- it('throws an error if locale cannot be made into BCP 47 language tag', () => {
- const mockState = createMockStateWithLocale('xxxinvalid-locale');
+ describe('getIntlLocale', () => {
+ it('returns the canonical BCP 47 language tag for the currently selected locale', () => {
+ const mockState = createMockStateWithLocale('en_US');
+ expect(getIntlLocale(mockState)).toBe('en-US');
+ });
- expect(() => getIntlLocale(mockState)).toThrow();
+ locales.forEach((locale: { code: string; name: string }) => {
+ it(`handles supported locale - "${locale.code}"`, () => {
+ const mockState = createMockStateWithLocale(locale.code);
+ expect(() => getIntlLocale(mockState)).not.toThrow();
+ });
+ });
});
- // @ts-expect-error This is missing from the Mocha type definitions
- it.each(locales)(
- 'handles all supported locales – "%s"',
- (locale: { code: string; name: string }) => {
- const mockState = createMockStateWithLocale(locale.code);
+ describe('getCurrentLocaleMessages', () => {
+ it('returns the current locale messages from the state', () => {
+ expect(getCurrentLocaleMessages(testData)).toEqual({ user: 'user' });
+ });
+
+ it('returns undefined if there are no current locale messages', () => {
+ const newAppState = {
+ ...testData,
+ localeMessages: {
+ current: undefined,
+ },
+ };
+ expect(getCurrentLocaleMessages(newAppState)).toEqual(undefined);
+ });
+ });
- expect(() => getIntlLocale(mockState)).not.toThrow();
- },
- );
+ describe('getEnLocaleMessages', () => {
+ it('returns the English locale messages from the state', () => {
+ expect(getEnLocaleMessages(testData)).toEqual({ user: 'user' });
+ });
+
+ it('returns undefined if there are no English locale messages', () => {
+ const newAppState = {
+ ...testData,
+ localeMessages: {
+ en: undefined,
+ },
+ };
+ expect(getEnLocaleMessages(newAppState)).toBeUndefined();
+ });
+ });
});
diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts
new file mode 100644
index 000000000000..2ca0cbaac4dc
--- /dev/null
+++ b/ui/ducks/locale/locale.ts
@@ -0,0 +1,108 @@
+import { createSelector } from 'reselect';
+import { Action } from 'redux'; // Import types for actions
+import * as actionConstants from '../../store/actionConstants';
+import { FALLBACK_LOCALE } from '../../../shared/modules/i18n';
+
+/**
+ * Type for the locale messages part of the state
+ */
+type LocaleMessagesState = {
+ current?: { [key: string]: string }; // Messages for the current locale
+ currentLocale?: string; // User's selected locale (unsafe for Intl API)
+ en?: { [key: string]: string }; // English locale messages
+};
+
+/**
+ * Payload for the SET_CURRENT_LOCALE action
+ */
+type SetCurrentLocaleAction = Action & {
+ type: typeof actionConstants.SET_CURRENT_LOCALE;
+ payload: {
+ messages: { [key: string]: string };
+ locale: string;
+ };
+};
+
+/**
+ * Type for actions that can be handled by reduceLocaleMessages
+ */
+type LocaleMessagesActions = SetCurrentLocaleAction;
+
+/**
+ * Initial state for localeMessages reducer
+ */
+const initialState: LocaleMessagesState = {};
+
+/**
+ * Reducer for localeMessages
+ *
+ * @param state - The current state
+ * @param action - The action being dispatched
+ * @returns The updated locale messages state
+ */
+export default function reduceLocaleMessages(
+ // eslint-disable-next-line @typescript-eslint/default-param-last
+ state: LocaleMessagesState = initialState,
+ action: LocaleMessagesActions,
+): LocaleMessagesState {
+ switch (action.type) {
+ case actionConstants.SET_CURRENT_LOCALE:
+ return {
+ ...state,
+ current: action.payload.messages,
+ currentLocale: action.payload.locale,
+ };
+ default:
+ return state;
+ }
+}
+
+/**
+ * Type for the overall Redux state
+ */
+type AppState = {
+ localeMessages: LocaleMessagesState;
+};
+
+/**
+ * This selector returns a code from file://./../../../app/_locales/index.json.
+ * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that.
+ *
+ * @param state - The overall state
+ * @returns The user's selected locale
+ */
+export const getCurrentLocale = (state: AppState): string | undefined =>
+ state.localeMessages.currentLocale;
+
+/**
+ * This selector returns a BCP 47 Language Tag for use with the Intl API.
+ *
+ * @returns The user's selected locale in BCP 47 format
+ */
+export const getIntlLocale = createSelector(
+ getCurrentLocale,
+ (locale): string =>
+ Intl.getCanonicalLocales(
+ locale ? locale.replace(/_/gu, '-') : FALLBACK_LOCALE,
+ )[0],
+);
+
+/**
+ * This selector returns the current locale messages.
+ *
+ * @param state - The overall state
+ * @returns The current locale's messages
+ */
+export const getCurrentLocaleMessages = (
+ state: AppState,
+): Record | undefined => state.localeMessages.current;
+
+/**
+ * This selector returns the English locale messages.
+ *
+ * @param state - The overall state
+ * @returns The English locale's messages
+ */
+export const getEnLocaleMessages = (
+ state: AppState,
+): Record | undefined => state.localeMessages.en;
diff --git a/ui/pages/error-page/error-component.test.tsx b/ui/pages/error-page/error-component.test.tsx
new file mode 100644
index 000000000000..16188c30881a
--- /dev/null
+++ b/ui/pages/error-page/error-component.test.tsx
@@ -0,0 +1,192 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import '@testing-library/jest-dom/extend-expect';
+import browser from 'webextension-polyfill';
+import { fireEvent } from '@testing-library/react';
+import { renderWithProvider } from '../../../test/lib/render-helpers';
+import { useI18nContext } from '../../hooks/useI18nContext';
+import { MetaMetricsContext } from '../../contexts/metametrics';
+import { getParticipateInMetaMetrics } from '../../selectors';
+import { getMessage } from '../../helpers/utils/i18n-helper';
+// eslint-disable-next-line import/no-restricted-paths
+import messages from '../../../app/_locales/en/messages.json';
+import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common';
+import {
+ MetaMetricsContextProp,
+ MetaMetricsEventCategory,
+ MetaMetricsEventName,
+} from '../../../shared/constants/metametrics';
+import ErrorPage from './error-page.component';
+
+jest.mock('../../hooks/useI18nContext', () => ({
+ useI18nContext: jest.fn(),
+}));
+
+jest.mock('webextension-polyfill', () => ({
+ runtime: {
+ reload: jest.fn(),
+ },
+}));
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn(),
+}));
+
+describe('ErrorPage', () => {
+ const useSelectorMock = useSelector as jest.Mock;
+ const mockTrackEvent = jest.fn();
+ const MockError = new Error(
+ "Cannot read properties of undefined (reading 'message')",
+ ) as Error & { code?: string };
+ MockError.code = '500';
+
+ const mockI18nContext = jest
+ .fn()
+ .mockReturnValue((key: string, variables: string[]) =>
+ getMessage('en', messages, key, variables),
+ );
+
+ beforeEach(() => {
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getParticipateInMetaMetrics) {
+ return true;
+ }
+ return undefined;
+ });
+ (useI18nContext as jest.Mock).mockImplementation(mockI18nContext);
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('should render the error message, code, and name if provided', () => {
+ const { getByTestId } = renderWithProvider(
+
+
+ ,
+ );
+
+ expect(
+ getByTestId('error-page-error-message').textContent,
+ ).toMatchInlineSnapshot(
+ `"Message: Cannot read properties of undefined (reading 'message')"`,
+ );
+ expect(
+ getByTestId('error-page-error-code').textContent,
+ ).toMatchInlineSnapshot(`"Code: 500"`);
+ expect(
+ getByTestId('error-page-error-name').textContent,
+ ).toMatchInlineSnapshot(`"Code: Error"`);
+ });
+
+ it('should not render error details if no error information is provided', () => {
+ const error = {};
+
+ const { queryByTestId } = renderWithProvider(
+
+
+ ,
+ );
+
+ expect(queryByTestId('error-page-error-message')).toBeNull();
+ expect(queryByTestId('error-page-error-code')).toBeNull();
+ expect(queryByTestId('error-page-error-name')).toBeNull();
+ expect(queryByTestId('error-page-error-stack')).toBeNull();
+ });
+
+ it('should render sentry user feedback form and submit sentry report successfully when metrics is opted in', () => {
+ const { getByTestId, queryByTestId } = renderWithProvider(
+
+
+ ,
+ );
+ const describeButton = getByTestId(
+ 'error-page-describe-what-happened-button',
+ );
+ fireEvent.click(describeButton);
+ expect(
+ queryByTestId('error-page-sentry-feedback-modal'),
+ ).toBeInTheDocument();
+ const textarea = getByTestId('error-page-sentry-feedback-textarea');
+ fireEvent.change(textarea, {
+ target: { value: 'Something went wrong on develop option page' },
+ });
+ const submitButton = getByTestId(
+ 'error-page-sentry-feedback-submit-button',
+ );
+ fireEvent.click(submitButton);
+ expect(
+ queryByTestId('error-page-sentry-feedback-modal'),
+ ).not.toBeInTheDocument();
+ expect(
+ queryByTestId('error-page-sentry-feedback-success-modal'),
+ ).toBeInTheDocument();
+ jest.advanceTimersByTime(5000);
+ expect(
+ queryByTestId('error-page-sentry-feedback-modal'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should render not sentry user feedback option when metrics is not opted in', () => {
+ useSelectorMock.mockImplementation((selector) => {
+ if (selector === getParticipateInMetaMetrics) {
+ return false;
+ }
+ return undefined;
+ });
+ const { queryByTestId } = renderWithProvider(
+
+
+ ,
+ );
+ const describeButton = queryByTestId(
+ 'error-page-describe-what-happened-button',
+ );
+
+ expect(describeButton).toBeNull();
+ });
+
+ it('should reload the extension when the "Try Again" button is clicked', () => {
+ const { getByTestId } = renderWithProvider(
+
+
+ ,
+ );
+ const tryAgainButton = getByTestId('error-page-try-again-button');
+ fireEvent.click(tryAgainButton);
+ expect(browser.runtime.reload).toHaveBeenCalled();
+ });
+
+ it('should open the support link and track the MetaMetrics event when the "Contact Support" button is clicked', () => {
+ window.open = jest.fn();
+
+ const { getByTestId } = renderWithProvider(
+
+
+ ,
+ );
+
+ const contactSupportButton = getByTestId(
+ 'error-page-contact-support-button',
+ );
+ fireEvent.click(contactSupportButton);
+
+ expect(window.open).toHaveBeenCalledWith(SUPPORT_REQUEST_LINK, '_blank');
+
+ expect(mockTrackEvent).toHaveBeenCalledWith(
+ {
+ category: MetaMetricsEventCategory.Error,
+ event: MetaMetricsEventName.SupportLinkClicked,
+ properties: {
+ url: SUPPORT_REQUEST_LINK,
+ },
+ },
+ {
+ contextPropsIntoEventProperties: [MetaMetricsContextProp.PageTitle],
+ },
+ );
+ });
+});
diff --git a/ui/pages/error-page/error-page.component.tsx b/ui/pages/error-page/error-page.component.tsx
new file mode 100644
index 000000000000..9a094057dcc0
--- /dev/null
+++ b/ui/pages/error-page/error-page.component.tsx
@@ -0,0 +1,312 @@
+import React, { useEffect, useContext, useState } from 'react';
+import { useSelector } from 'react-redux';
+import * as Sentry from '@sentry/browser';
+import browser from 'webextension-polyfill';
+import {
+ MetaMetricsContextProp,
+ MetaMetricsEventCategory,
+ MetaMetricsEventName,
+} from '../../../shared/constants/metametrics';
+
+import { getParticipateInMetaMetrics } from '../../selectors';
+import { MetaMetricsContext } from '../../contexts/metametrics';
+import { useI18nContext } from '../../hooks/useI18nContext';
+import {
+ BannerAlert,
+ Box,
+ Icon,
+ IconName,
+ IconSize,
+ Text,
+ Button,
+ ButtonVariant,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+} from '../../components/component-library';
+import {
+ AlignItems,
+ BackgroundColor,
+ BlockSize,
+ BorderRadius,
+ Display,
+ FlexDirection,
+ IconColor,
+ JustifyContent,
+ TextColor,
+ TextVariant,
+} from '../../helpers/constants/design-system';
+
+import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common';
+
+import { Textarea } from '../../components/component-library/textarea/textarea';
+import { TextareaResize } from '../../components/component-library/textarea/textarea.types';
+import { ButtonSize } from '../../components/component-library/button/button.types';
+
+type ErrorPageProps = {
+ error: {
+ message?: string;
+ code?: string;
+ name?: string;
+ stack?: string;
+ };
+};
+
+const ErrorPage: React.FC = ({ error }) => {
+ const t = useI18nContext();
+ const trackEvent = useContext(MetaMetricsContext);
+ const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics);
+
+ const [feedbackMessage, setFeedbackMessage] = useState('');
+ const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false);
+ const [isSuccessModalShown, setIsSuccessModalShown] = useState(false);
+
+ const handleClickDescribeButton = (): void => {
+ setIsFeedbackModalOpen(true);
+ };
+
+ const handleCloseDescribeModal = (): void => {
+ setIsFeedbackModalOpen(false);
+ };
+
+ const handleSubmitFeedback = (e: React.MouseEvent) => {
+ e.preventDefault();
+ const eventId = Sentry.lastEventId();
+
+ Sentry.captureFeedback({
+ message: feedbackMessage,
+ associatedEventId: eventId,
+ });
+ handleCloseDescribeModal();
+ setIsSuccessModalShown(true);
+ };
+
+ useEffect(() => {
+ if (isSuccessModalShown) {
+ const timeoutId = setTimeout(() => {
+ setIsSuccessModalShown(false); // Close the modal after 5 seconds
+ }, 5000);
+
+ // Cleanup function to clear timeout if the component unmounts or state changes
+ return () => clearTimeout(timeoutId);
+ }
+ return undefined;
+ }, [isSuccessModalShown]);
+
+ return (
+
+
+
+
+
+ {t('errorPageTitle')}
+
+
+
+
+ {t('errorPageInfo')}
+
+
+ {t('errorPageMessageTitle')}
+
+
+ {error.message ? (
+
+ {t('errorMessage', [error.message])}
+
+ ) : null}
+ {error.code ? (
+
+ {t('errorCode', [error.code])}
+
+ ) : null}
+ {error.name ? (
+
+ {t('errorName', [error.name])}
+
+ ) : null}
+ {error.stack ? (
+ <>
+
+ {t('errorStack')}
+
+
+ {error.stack}
+
+ >
+ ) : null}
+
+
+ {isFeedbackModalOpen && (
+
+
+
+
+ {t('errorPageSentryFormTitle')}
+
+
+
+
+
+
+ {t('cancel')}
+
+
+ {t('submit')}
+
+
+
+
+
+ )}
+ {isSuccessModalShown && (
+ setIsSuccessModalShown(false)}
+ data-testid="error-page-sentry-feedback-success-modal"
+ >
+
+
+
+
+
+ {t('errorPageSentrySuccessMessageText')}
+
+
+
+
+ )}
+
+ {isMetaMetricsEnabled && (
+
+ {t('errorPageDescribeUsWhatHappened')}
+
+ )}
+ {
+ window.open(SUPPORT_REQUEST_LINK, '_blank');
+ trackEvent(
+ {
+ category: MetaMetricsEventCategory.Error,
+ event: MetaMetricsEventName.SupportLinkClicked,
+ properties: {
+ url: SUPPORT_REQUEST_LINK,
+ },
+ },
+ {
+ contextPropsIntoEventProperties: [
+ MetaMetricsContextProp.PageTitle,
+ ],
+ },
+ );
+ }}
+ >
+ {t('errorPageContactSupport')}
+
+ browser.runtime.reload()}
+ >
+ {t('errorPageTryAgain')}
+
+
+
+
+ );
+};
+
+export default ErrorPage;
diff --git a/ui/pages/error-page/index.scss b/ui/pages/error-page/index.scss
new file mode 100644
index 000000000000..b3d734899dd7
--- /dev/null
+++ b/ui/pages/error-page/index.scss
@@ -0,0 +1,41 @@
+@use "design-system";
+
+.error-page {
+ display: flex;
+ flex-flow: column;
+ padding: 16px;
+ height: 100%;
+
+ &__inner-wrapper {
+ @media screen and (min-width: design-system.$screen-md-min) {
+ width: 50%;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+
+ &__stack {
+ max-height: 120px;
+ overflow: auto;
+ font-family: var(--font-family-sans);
+ font-weight: var(--typography-l-body-xs-font-weight);
+ font-size: var(--typography-l-body-xs-font-size);
+ }
+
+ &__link-text {
+ color: var(--color-primary-default);
+ }
+}
+
+// override styles for sentry feedback form
+#sentry-feedback {
+ --font-family: var(--font-family-sans);
+ --inset: auto 20px 20px auto;
+ --input-font-size: 14px;
+ --button-color: var(--color-primary-default);
+ --button-primary-background: var(--color-primary-default);
+ --button-primary-border: var(--color-primary-default);
+ --button-primary-hover-background: var(--color-primary-default);
+ --success-color: var(--color-success-default);
+ --error-color: var(--color-error-default);
+}
diff --git a/ui/pages/error-page/index.ts b/ui/pages/error-page/index.ts
new file mode 100644
index 000000000000..7c868e73cad5
--- /dev/null
+++ b/ui/pages/error-page/index.ts
@@ -0,0 +1 @@
+export { default } from './error-page.component';
diff --git a/ui/pages/error/error.component.js b/ui/pages/error/error.component.js
deleted file mode 100644
index f7ab9c593d40..000000000000
--- a/ui/pages/error/error.component.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-// TODO: Remove restricted import
-// eslint-disable-next-line import/no-restricted-paths
-import { getEnvironmentType } from '../../../app/scripts/lib/util';
-import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app';
-import { getErrorMessage } from '../../../shared/modules/error';
-import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common';
-import {
- MetaMetricsContextProp,
- MetaMetricsEventCategory,
- MetaMetricsEventName,
-} from '../../../shared/constants/metametrics';
-
-class ErrorPage extends PureComponent {
- static contextTypes = {
- t: PropTypes.func.isRequired,
- trackEvent: PropTypes.func,
- };
-
- static propTypes = {
- error: PropTypes.object.isRequired,
- };
-
- renderErrorDetail(content) {
- return (
-
- {content}
-
- );
- }
-
- renderErrorStack(title, stack) {
- return (
-
- {title}
- {stack}
-
- );
- }
-
- render() {
- const { error } = this.props;
- const { t } = this.context;
-
- const isPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP;
- const supportLink = (
- {
- this.context.trackEvent(
- {
- category: MetaMetricsEventCategory.Error,
- event: MetaMetricsEventName.SupportLinkClicked,
- properties: {
- url: SUPPORT_REQUEST_LINK,
- },
- },
- {
- contextPropsIntoEventProperties: [
- MetaMetricsContextProp.PageTitle,
- ],
- },
- );
- }}
- >
- {this.context.t('here')}
-
- );
- const message = isPopup
- ? t('errorPagePopupMessage', [supportLink])
- : t('errorPageMessage', [supportLink]);
- const errorMessage = getErrorMessage(error);
-
- return (
-
- {t('errorPageTitle')}
- {message}
-
-
- {t('errorDetails')}
-
- {errorMessage
- ? this.renderErrorDetail(t('errorMessage', [errorMessage]))
- : null}
- {error.code
- ? this.renderErrorDetail(t('errorCode', [error.code]))
- : null}
- {error.name
- ? this.renderErrorDetail(t('errorName', [error.name]))
- : null}
- {error.stack
- ? this.renderErrorStack(t('errorStack'), error.stack)
- : null}
-
-
-
-
- );
- }
-}
-
-export default ErrorPage;
diff --git a/ui/pages/error/index.js b/ui/pages/error/index.js
deleted file mode 100644
index 386b79786307..000000000000
--- a/ui/pages/error/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './error.component';
diff --git a/ui/pages/error/index.scss b/ui/pages/error/index.scss
deleted file mode 100644
index 10e00597e8ed..000000000000
--- a/ui/pages/error/index.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-@use "design-system";
-
-.error-page {
- display: flex;
- flex-flow: column nowrap;
- align-items: center;
- font-style: normal;
- font-weight: normal;
- padding: 35px 10px 10px 10px;
- height: 100%;
-
- &__header {
- @include design-system.H1;
-
- display: flex;
- justify-content: center;
- padding: 10px 0;
- text-align: center;
- }
-
- &__subheader {
- @include design-system.H4;
-
- padding: 10px 0;
- width: 100%;
- max-width: 720px;
- text-align: center;
- }
-
- &__details {
- @include design-system.H4;
-
- overflow-y: auto;
- width: 100%;
- max-width: 720px;
- padding-top: 10px;
- }
-
- &__stack {
- overflow-x: auto;
- background-color: var(--color-background-alternative);
- }
-
- &__link-text {
- color: var(--color-primary-default);
- }
-}
diff --git a/ui/pages/index.js b/ui/pages/index.js
index c30846fff1e6..9b6147b6f421 100644
--- a/ui/pages/index.js
+++ b/ui/pages/index.js
@@ -11,7 +11,8 @@ import {
} from '../contexts/metametrics';
import { MetamaskNotificationsProvider } from '../contexts/metamask-notifications';
import { AssetPollingProvider } from '../contexts/assetPolling';
-import ErrorPage from './error';
+import ErrorPage from './error-page/error-page.component';
+
import Routes from './routes';
class Index extends PureComponent {
@@ -26,7 +27,7 @@ class Index extends PureComponent {
}
render() {
- const { error, errorId } = this.state;
+ const { error } = this.state;
const { store } = this.props;
if (error) {
@@ -34,7 +35,7 @@ class Index extends PureComponent {
-
+
diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss
index 378622a3994a..84276e4c7d99 100644
--- a/ui/pages/pages.scss
+++ b/ui/pages/pages.scss
@@ -10,7 +10,7 @@
@import 'connected-sites/index';
@import 'create-account/create-account';
@import 'create-account/connect-hardware/index';
-@import 'error/index';
+@import 'error-page/index';
@import 'home/index';
@import "institutional/account-list/index";
@import "institutional/institutional-entity-done-page/index";
diff --git a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap
index 4eea2d9cf7d1..17899d68724f 100644
--- a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap
+++ b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap
@@ -319,7 +319,7 @@ exports[`Develop options tab should match snapshot 1`] = `
class="settings-page__content-item-col"
>
Generate UI Error
@@ -362,7 +362,7 @@ exports[`Develop options tab should match snapshot 1`] = `
class="settings-page__content-item-col"
>
Generate Background Error
@@ -405,7 +405,7 @@ exports[`Develop options tab should match snapshot 1`] = `
class="settings-page__content-item-col"
>
Generate Trace
@@ -426,6 +426,46 @@ exports[`Develop options tab should match snapshot 1`] = `
+
+
+
+
+ Trigger the crash on extension to send user feedback to sentry. You can click "Try again" to reload extension
+
+
+
+
+
+ Generate A Page Crash
+
+
+
+
diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx
index a88d735a628f..a604082586fc 100644
--- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx
+++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx
@@ -38,7 +38,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm';
import ToggleRow from './developer-options-toggle-row-component';
-import { SentryTest } from './sentry-test';
+import SentryTest from './sentry-test';
import { ProfileSyncDevSettings } from './profile-sync';
/**
diff --git a/ui/pages/settings/developer-options-tab/sentry-test.tsx b/ui/pages/settings/developer-options-tab/sentry-test.tsx
index 7d04e3a4dd87..5c7132a7cfea 100644
--- a/ui/pages/settings/developer-options-tab/sentry-test.tsx
+++ b/ui/pages/settings/developer-options-tab/sentry-test.tsx
@@ -1,8 +1,9 @@
import React, { useState, useCallback, ReactElement } from 'react';
-import { ButtonVariant } from '@metamask/snaps-sdk';
+import { useDispatch, useSelector } from 'react-redux';
import {
Box,
Button,
+ ButtonVariant,
Icon,
IconName,
IconSize,
@@ -16,8 +17,23 @@ import {
JustifyContent,
} from '../../../helpers/constants/design-system';
import { trace, TraceName } from '../../../../shared/lib/trace';
+import { ButtonSize } from '../../../components/component-library/button/button.types';
+
+import {
+ forceUpdateMetamaskState,
+ setCurrentLocale,
+} from '../../../store/actions';
+import { FALLBACK_LOCALE, fetchLocale } from '../../../../shared/modules/i18n';
+import { getCurrentLocale } from '../../../ducks/locale/locale';
+
+function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+const SentryTest = () => {
+ const currentLocale: string =
+ useSelector(getCurrentLocale) || FALLBACK_LOCALE;
-export function SentryTest() {
return (
<>
@@ -27,10 +43,11 @@ export function SentryTest() {
+
>
);
-}
+};
function GenerateUIError() {
const handleClick = useCallback(async () => {
@@ -115,16 +132,48 @@ function GenerateTrace() {
);
}
+function GeneratePageCrash({ currentLocale }: { currentLocale: string }) {
+ const dispatch = useDispatch();
+ const handleClick = async () => {
+ const localeMessages = await fetchLocale(currentLocale);
+ await dispatch(
+ setCurrentLocale(currentLocale, {
+ ...localeMessages,
+ // @ts-expect-error - remove a language string in this page to trigger a page crash
+ developerOptions: undefined,
+ }),
+ );
+ await forceUpdateMetamaskState(dispatch);
+ };
+
+ return (
+
+ Trigger the crash on extension to send user feedback to sentry. You
+ can click "Try again" to reload extension
+
+ }
+ onClick={handleClick}
+ expectError
+ testId="developer-options-generate-page-crash-button"
+ />
+ );
+}
+
function TestButton({
name,
description,
onClick,
expectError,
+ testId,
}: {
name: string;
description: ReactElement;
onClick: () => Promise;
expectError?: boolean;
+ testId?: string;
}) {
const [isComplete, setIsComplete] = useState(false);
@@ -155,7 +204,12 @@ function TestButton({
{description}
-
+
{name}
@@ -180,6 +234,4 @@ function TestButton({
);
}
-function sleep(ms: number) {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
+export default SentryTest;
diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js
index 119b01257802..6cd630245819 100644
--- a/ui/pages/settings/settings.component.js
+++ b/ui/pages/settings/settings.component.js
@@ -336,7 +336,7 @@ class SettingsPage extends PureComponent {
});
}
- if (process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) {
+ if (process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS || process.env.IN_TEST) {
tabs.splice(-1, 0, {
content: t('developerOptions'),
icon: ,
@@ -410,7 +410,8 @@ class SettingsPage extends PureComponent {
/>
- {process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS && (
+ {(process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS ||
+ process.env.IN_TEST) && (
Date: Fri, 8 Nov 2024 15:17:27 +0100
Subject: [PATCH 17/18] test: [POM] Refactor import account e2e tests to use
Page Object Model (#28325)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
- Migrate e2e tests `test/e2e/tests/account/add-account.spec.js` to TS
and Page Object Model, to reduce flakiness.
- Migrate e2e tests `test/e2e/tests/account/import-flow.spec.js` to TS
and Page Object Model, to reduce flakiness.
- Deprecate/remove old functions
- Use onboarding related functions designed in page object model, which
are more stable, and removed unnecessary delays.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1)
## **Related issues**
Fixes: #28328
## **Manual testing steps**
Check code readability, make sure tests pass.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---------
Co-authored-by: Harika <153644847+hjetpoluru@users.noreply.github.com>
Co-authored-by: seaona <54408225+seaona@users.noreply.github.com>
---
test/e2e/helpers.js | 112 +----
.../e2e/page-objects/flows/onboarding.flow.ts | 23 +-
.../flows/send-transaction.flow.ts | 125 +++--
.../page-objects/pages/account-list-page.ts | 185 ++++++-
.../pages/onboarding/onboarding-srp-page.ts | 15 +
.../pages/send/send-token-page.ts | 119 ++---
test/e2e/tests/account/add-account.spec.js | 232 ---------
test/e2e/tests/account/add-account.spec.ts | 123 +++++
test/e2e/tests/account/import-flow.spec.js | 474 ------------------
test/e2e/tests/account/import-flow.spec.ts | 115 +++++
.../account/snap-account-transfers.spec.ts | 42 +-
.../erc20-token-send-redesign.spec.ts | 4 +-
.../hardware-wallets/lattice-connect.spec.ts | 44 ++
.../tests/privacy/basic-functionality.spec.js | 27 +-
.../e2e/tests/transaction/simple-send.spec.ts | 14 +-
15 files changed, 692 insertions(+), 962 deletions(-)
delete mode 100644 test/e2e/tests/account/add-account.spec.js
create mode 100644 test/e2e/tests/account/add-account.spec.ts
delete mode 100644 test/e2e/tests/account/import-flow.spec.js
create mode 100644 test/e2e/tests/account/import-flow.spec.ts
create mode 100644 test/e2e/tests/hardware-wallets/lattice-connect.spec.ts
diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js
index 1c3f53b30224..1e03da918ed7 100644
--- a/test/e2e/helpers.js
+++ b/test/e2e/helpers.js
@@ -385,107 +385,6 @@ const getWindowHandles = async (driver, handlesCount) => {
return { extension, dapp, popup };
};
-/**
- * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`.
- * @param driver
- * @param seedPhrase
- * @param password
- */
-const importSRPOnboardingFlow = async (driver, seedPhrase, password) => {
- // agree to terms of use
- await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
-
- // welcome
- await driver.clickElement('[data-testid="onboarding-import-wallet"]');
-
- // metrics
- await driver.clickElement('[data-testid="metametrics-no-thanks"]');
-
- await driver.waitForSelector('.import-srp__actions');
- // import with recovery phrase
- await driver.pasteIntoField(
- '[data-testid="import-srp__srp-word-0"]',
- seedPhrase,
- );
- await driver.clickElement('[data-testid="import-srp-confirm"]');
-
- // create password
- await driver.fill('[data-testid="create-password-new"]', password);
- await driver.fill('[data-testid="create-password-confirm"]', password);
- await driver.clickElement('[data-testid="create-password-terms"]');
- await driver.clickElement('[data-testid="create-password-import"]');
- await driver.assertElementNotPresent('.loading-overlay');
-};
-
-/**
- * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`.
- * @param driver
- * @param seedPhrase
- * @param password
- */
-const completeImportSRPOnboardingFlow = async (
- driver,
- seedPhrase,
- password,
-) => {
- await importSRPOnboardingFlow(driver, seedPhrase, password);
-
- // complete
- await driver.clickElement('[data-testid="onboarding-complete-done"]');
-
- // pin extension
- await driver.clickElement('[data-testid="pin-extension-next"]');
- await driver.clickElement('[data-testid="pin-extension-done"]');
-};
-
-/**
- * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`.
- * @param driver
- * @param seedPhrase
- * @param password
- */
-const completeImportSRPOnboardingFlowWordByWord = async (
- driver,
- seedPhrase,
- password,
-) => {
- // agree to terms of use
- await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
-
- // welcome
- await driver.clickElement('[data-testid="onboarding-import-wallet"]');
-
- // metrics
-
- await driver.clickElement('[data-testid="metametrics-no-thanks"]');
-
- // import with recovery phrase, word by word
- const words = seedPhrase.split(' ');
- for (const word of words) {
- await driver.pasteIntoField(
- `[data-testid="import-srp__srp-word-${words.indexOf(word)}"]`,
- word,
- );
- }
- await driver.clickElement('[data-testid="import-srp-confirm"]');
-
- // create password
- await driver.fill('[data-testid="create-password-new"]', password);
- await driver.fill('[data-testid="create-password-confirm"]', password);
- await driver.clickElement('[data-testid="create-password-terms"]');
- await driver.clickElement('[data-testid="create-password-import"]');
-
- // wait for loading to complete
- await driver.assertElementNotPresent('.loading-overlay');
-
- // complete
- await driver.clickElement('[data-testid="onboarding-complete-done"]');
-
- // pin extension
- await driver.clickElement('[data-testid="pin-extension-next"]');
- await driver.clickElement('[data-testid="pin-extension-done"]');
-};
-
/**
* @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`.
* Begin the create new wallet flow on onboarding screen.
@@ -1237,6 +1136,10 @@ async function initBundler(bundlerServer, ganacheServer, usePaymaster) {
}
}
+/**
+ * @deprecated Please use page object functions in `pages/account-list-page`.
+ * @param driver
+ */
async function removeSelectedAccount(driver) {
await driver.clickElement('[data-testid="account-menu-icon"]');
await driver.clickElement(
@@ -1246,6 +1149,10 @@ async function removeSelectedAccount(driver) {
await driver.clickElement({ text: 'Remove', tag: 'button' });
}
+/**
+ * @deprecated Please use page object functions in `pages/account-list-page`.
+ * @param driver
+ */
async function getSelectedAccountAddress(driver) {
await driver.clickElement('[data-testid="account-options-menu-button"]');
await driver.clickElement('[data-testid="account-list-menu-details"]');
@@ -1328,9 +1235,6 @@ module.exports = {
largeDelayMs,
veryLargeDelayMs,
withFixtures,
- importSRPOnboardingFlow,
- completeImportSRPOnboardingFlow,
- completeImportSRPOnboardingFlowWordByWord,
completeCreateNewWalletOnboardingFlow,
completeCreateNewWalletOnboardingFlowWithOptOut,
openSRPRevealQuiz,
diff --git a/test/e2e/page-objects/flows/onboarding.flow.ts b/test/e2e/page-objects/flows/onboarding.flow.ts
index adf591330798..e235048b0a01 100644
--- a/test/e2e/page-objects/flows/onboarding.flow.ts
+++ b/test/e2e/page-objects/flows/onboarding.flow.ts
@@ -43,17 +43,20 @@ export const createNewWalletOnboardingFlow = async (
*
* @param options - The options object.
* @param options.driver - The WebDriver instance.
- * @param [options.seedPhrase] - The seed phrase to import.
- * @param [options.password] - The password to use.
+ * @param [options.seedPhrase] - The seed phrase to import. Defaults to E2E_SRP.
+ * @param [options.password] - The password to use. Defaults to WALLET_PASSWORD.
+ * @param [options.fillSrpWordByWord] - Whether to fill the SRP word by word. Defaults to false.
*/
export const importSRPOnboardingFlow = async ({
driver,
seedPhrase = E2E_SRP,
password = WALLET_PASSWORD,
+ fillSrpWordByWord = false,
}: {
driver: Driver;
seedPhrase?: string;
password?: string;
+ fillSrpWordByWord?: boolean;
}): Promise => {
console.log('Starting the import of SRP onboarding flow');
await driver.navigate();
@@ -69,7 +72,11 @@ export const importSRPOnboardingFlow = async ({
const onboardingSrpPage = new OnboardingSrpPage(driver);
await onboardingSrpPage.check_pageIsLoaded();
- await onboardingSrpPage.fillSrp(seedPhrase);
+ if (fillSrpWordByWord) {
+ await onboardingSrpPage.fillSrpWordByWord(seedPhrase);
+ } else {
+ await onboardingSrpPage.fillSrp(seedPhrase);
+ }
await onboardingSrpPage.clickConfirmButton();
const onboardingPasswordPage = new OnboardingPasswordPage(driver);
@@ -102,19 +109,27 @@ export const completeCreateNewWalletOnboardingFlow = async (
* @param options.driver - The WebDriver instance.
* @param [options.seedPhrase] - The seed phrase to import. Defaults to E2E_SRP.
* @param [options.password] - The password to use. Defaults to WALLET_PASSWORD.
+ * @param [options.fillSrpWordByWord] - Whether to fill the SRP word by word. Defaults to false.
* @returns A promise that resolves when the onboarding flow is complete.
*/
export const completeImportSRPOnboardingFlow = async ({
driver,
seedPhrase = E2E_SRP,
password = WALLET_PASSWORD,
+ fillSrpWordByWord = false,
}: {
driver: Driver;
seedPhrase?: string;
password?: string;
+ fillSrpWordByWord?: boolean;
}): Promise => {
console.log('Starting to complete import SRP onboarding flow');
- await importSRPOnboardingFlow({ driver, seedPhrase, password });
+ await importSRPOnboardingFlow({
+ driver,
+ seedPhrase,
+ password,
+ fillSrpWordByWord,
+ });
const onboardingCompletePage = new OnboardingCompletePage(driver);
await onboardingCompletePage.check_pageIsLoaded();
diff --git a/test/e2e/page-objects/flows/send-transaction.flow.ts b/test/e2e/page-objects/flows/send-transaction.flow.ts
index 1cffe2428873..3f56076e3934 100644
--- a/test/e2e/page-objects/flows/send-transaction.flow.ts
+++ b/test/e2e/page-objects/flows/send-transaction.flow.ts
@@ -7,19 +7,26 @@ import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page';
/**
* This function initiates the steps required to send a transaction from the homepage to final confirmation.
*
- * @param driver - The webdriver instance.
- * @param recipientAddress - The recipient address.
- * @param amount - The amount of the asset to be sent in the transaction.
- * @param gasfee - The expected transaction gas fee.
- * @param totalfee - The expected total transaction fee.
+ * @param params - An object containing the parameters.
+ * @param params.driver - The webdriver instance.
+ * @param params.recipientAddress - The recipient address.
+ * @param params.amount - The amount of the asset to be sent in the transaction.
+ * @param params.gasFee - The expected transaction gas fee.
+ * @param params.totalFee - The expected total transaction fee.
*/
-export const sendTransaction = async (
- driver: Driver,
- recipientAddress: string,
- amount: string,
- gasfee: string,
- totalfee: string,
-): Promise => {
+export const sendTransactionToAddress = async ({
+ driver,
+ recipientAddress,
+ amount,
+ gasFee,
+ totalFee,
+}: {
+ driver: Driver;
+ recipientAddress: string;
+ amount: string;
+ gasFee: string;
+ totalFee: string;
+}): Promise => {
console.log(
`Start flow to send amount ${amount} to recipient ${recipientAddress} on home screen`,
);
@@ -36,31 +43,89 @@ export const sendTransaction = async (
// confirm transaction when user lands on confirm transaction screen
const confirmTxPage = new ConfirmTxPage(driver);
- await confirmTxPage.check_pageIsLoaded(gasfee, totalfee);
+ await confirmTxPage.check_pageIsLoaded(gasFee, totalFee);
+ await confirmTxPage.confirmTx();
+};
+
+/**
+ * This function initiates the steps required to send a transaction from the homepage to final confirmation.
+ *
+ * @param params - An object containing the parameters.
+ * @param params.driver - The webdriver instance.
+ * @param params.recipientAccount - The recipient account.
+ * @param params.amount - The amount of the asset to be sent in the transaction.
+ * @param params.gasFee - The expected transaction gas fee.
+ * @param params.totalFee - The expected total transaction fee.
+ */
+export const sendTransactionToAccount = async ({
+ driver,
+ recipientAccount,
+ amount,
+ gasFee,
+ totalFee,
+}: {
+ driver: Driver;
+ recipientAccount: string;
+ amount: string;
+ gasFee: string;
+ totalFee: string;
+}): Promise => {
+ console.log(
+ `Start flow to send amount ${amount} to recipient account ${recipientAccount} on home screen`,
+ );
+ // click send button on homepage to start flow
+ const homePage = new HomePage(driver);
+ await homePage.startSendFlow();
+
+ // user should land on send token screen to fill recipient and amount
+ const sendToPage = new SendTokenPage(driver);
+ await sendToPage.check_pageIsLoaded();
+ await sendToPage.selectRecipientAccount(recipientAccount);
+ await sendToPage.fillAmount(amount);
+ await sendToPage.goToNextScreen();
+
+ // confirm transaction when user lands on confirm transaction screen
+ const confirmTxPage = new ConfirmTxPage(driver);
+ await confirmTxPage.check_pageIsLoaded(gasFee, totalFee);
await confirmTxPage.confirmTx();
};
/**
* This function initiates the steps required to send a transaction from snap account on homepage to final confirmation.
*
- * @param driver - The webdriver instance.
- * @param recipientAddress - The recipient address.
- * @param amount - The amount of the asset to be sent in the transaction.
- * @param gasfee - The expected transaction gas fee.
- * @param totalfee - The expected total transaction fee.
- * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true.
- * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true.
+ * @param params - An object containing the parameters.
+ * @param params.driver - The webdriver instance.
+ * @param params.recipientAddress - The recipient address.
+ * @param params.amount - The amount of the asset to be sent in the transaction.
+ * @param params.gasFee - The expected transaction gas fee.
+ * @param params.totalFee - The expected total transaction fee.
+ * @param params.isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true.
+ * @param params.approveTransaction - Indicates whether the transaction should be approved. Defaults to true.
*/
-export const sendTransactionWithSnapAccount = async (
- driver: Driver,
- recipientAddress: string,
- amount: string,
- gasfee: string,
- totalfee: string,
- isSyncFlow: boolean = true,
- approveTransaction: boolean = true,
-): Promise => {
- await sendTransaction(driver, recipientAddress, amount, gasfee, totalfee);
+export const sendTransactionWithSnapAccount = async ({
+ driver,
+ recipientAddress,
+ amount,
+ gasFee,
+ totalFee,
+ isSyncFlow = true,
+ approveTransaction = true,
+}: {
+ driver: Driver;
+ recipientAddress: string;
+ amount: string;
+ gasFee: string;
+ totalFee: string;
+ isSyncFlow?: boolean;
+ approveTransaction?: boolean;
+}): Promise => {
+ await sendTransactionToAddress({
+ driver,
+ recipientAddress,
+ amount,
+ gasFee,
+ totalFee,
+ });
if (!isSyncFlow) {
await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction(
approveTransaction,
diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts
index ae02973b6ae5..fb7c1232c08c 100644
--- a/test/e2e/page-objects/pages/account-list-page.ts
+++ b/test/e2e/page-objects/pages/account-list-page.ts
@@ -1,4 +1,5 @@
import { Driver } from '../../webdriver/driver';
+import { largeDelayMs } from '../../helpers';
class AccountListPage {
private readonly driver: Driver;
@@ -17,12 +18,13 @@ class AccountListPage {
private readonly accountOptionsMenuButton =
'[data-testid="account-list-item-menu-button"]';
+ private readonly accountQrCodeImage = '.qr-code__wrapper';
+
+ private readonly accountQrCodeAddress = '.qr-code__address-segments';
+
private readonly addAccountConfirmButton =
'[data-testid="submit-add-account-with-name"]';
- private readonly importAccountConfirmButton =
- '[data-testid="import-account-confirm-button"]';
-
private readonly addEthereumAccountButton =
'[data-testid="multichain-account-menu-popover-add-account"]';
@@ -39,29 +41,61 @@ class AccountListPage {
private readonly createAccountButton =
'[data-testid="multichain-account-menu-popover-action-button"]';
+ private readonly currentSelectedAccount =
+ '.multichain-account-list-item--selected';
+
private readonly editableLabelButton =
'[data-testid="editable-label-button"]';
private readonly editableLabelInput = '[data-testid="editable-input"] input';
- private readonly hideUnhideAccountButton =
- '[data-testid="account-list-menu-hide"]';
-
private readonly hiddenAccountOptionsMenuButton =
'.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]';
private readonly hiddenAccountsList = '[data-testid="hidden-accounts-list"]';
+ private readonly hideUnhideAccountButton =
+ '[data-testid="account-list-menu-hide"]';
+
+ private readonly importAccountConfirmButton =
+ '[data-testid="import-account-confirm-button"]';
+
+ private readonly importAccountPrivateKeyInput = '#private-key-box';
+
+ private readonly importAccountDropdownOption = '.dropdown__select';
+
+ private readonly importAccountJsonFileOption = {
+ text: 'JSON File',
+ tag: 'option',
+ };
+
+ private readonly importAccountJsonFileInput =
+ 'input[data-testid="file-input"]';
+
+ private readonly importAccountJsonPasswordInput =
+ 'input[id="json-password-box"]';
+
private readonly pinUnpinAccountButton =
'[data-testid="account-list-menu-pin"]';
private readonly pinnedIcon = '[data-testid="account-pinned-icon"]';
+ private readonly removeAccountButton =
+ '[data-testid="account-list-menu-remove"]';
+
+ private readonly removeAccountConfirmButton = {
+ text: 'Remove',
+ tag: 'button',
+ };
+
+ private readonly removeAccountMessage = {
+ text: 'Remove account?',
+ tag: 'div',
+ };
+
private readonly saveAccountLabelButton =
'[data-testid="save-account-label-input"]';
- private readonly importAccountPrivateKeyInput = '#private-key-box';
-
constructor(driver: Driver) {
this.driver = driver;
}
@@ -89,6 +123,9 @@ class AccountListPage {
await this.driver.clickElement(this.createAccountButton);
await this.driver.clickElement(this.addEthereumAccountButton);
await this.driver.fill(this.accountNameInput, customLabel);
+ // needed to mitigate a race condition with the state update
+ // there is no condition we can wait for in the UI
+ await this.driver.delay(largeDelayMs);
await this.driver.clickElementAndWaitToDisappear(
this.addAccountConfirmButton,
);
@@ -102,24 +139,39 @@ class AccountListPage {
console.log(`Adding new account with next available name`);
await this.driver.clickElement(this.createAccountButton);
await this.driver.clickElement(this.addEthereumAccountButton);
+ // needed to mitigate a race condition with the state update
+ // there is no condition we can wait for in the UI
+ await this.driver.delay(largeDelayMs);
await this.driver.clickElementAndWaitToDisappear(
this.addAccountConfirmButton,
);
}
/**
- * Adds a new account with a custom label.
+ * Import a new account with a private key.
*
* @param privateKey - Private key of the account
+ * @param expectedErrorMessage - Expected error message if the import should fail
*/
- async addNewImportedAccount(privateKey: string): Promise {
+ async addNewImportedAccount(
+ privateKey: string,
+ expectedErrorMessage?: string,
+ ): Promise {
console.log(`Adding new imported account`);
await this.driver.clickElement(this.createAccountButton);
await this.driver.clickElement(this.addImportedAccountButton);
await this.driver.fill(this.importAccountPrivateKeyInput, privateKey);
- await this.driver.clickElementAndWaitToDisappear(
- this.importAccountConfirmButton,
- );
+ if (expectedErrorMessage) {
+ await this.driver.clickElement(this.importAccountConfirmButton);
+ await this.driver.waitForSelector({
+ css: '.mm-help-text',
+ text: expectedErrorMessage,
+ });
+ } else {
+ await this.driver.clickElementAndWaitToDisappear(
+ this.importAccountConfirmButton,
+ );
+ }
}
/**
@@ -148,6 +200,59 @@ class AccountListPage {
await this.driver.clickElement(this.hideUnhideAccountButton);
}
+ /**
+ * Import an account with a JSON file.
+ *
+ * @param jsonFilePath - Path to the JSON file to import
+ * @param password - Password for the imported account
+ */
+ async importAccountWithJsonFile(
+ jsonFilePath: string,
+ password: string,
+ ): Promise {
+ console.log(`Adding new imported account`);
+ await this.driver.clickElement(this.createAccountButton);
+ await this.driver.clickElement(this.addImportedAccountButton);
+ await this.driver.clickElement(this.importAccountDropdownOption);
+ await this.driver.clickElement(this.importAccountJsonFileOption);
+
+ const fileInput = await this.driver.findElement(
+ this.importAccountJsonFileInput,
+ );
+ await fileInput.sendKeys(jsonFilePath);
+ await this.driver.fill(this.importAccountJsonPasswordInput, password);
+ await this.driver.clickElementAndWaitToDisappear(
+ this.importAccountConfirmButton,
+ );
+ }
+
+ /**
+ * Open the account details modal for the specified account in account list.
+ *
+ * @param accountLabel - The label of the account to open the details modal for.
+ */
+ async openAccountDetailsModal(accountLabel: string): Promise {
+ console.log(
+ `Open account details modal in account list for account ${accountLabel}`,
+ );
+ await this.openAccountOptionsInAccountList(accountLabel);
+ await this.driver.clickElement(this.accountMenuButton);
+ }
+
+ /**
+ * Open the account options menu for the specified account.
+ *
+ * @param accountLabel - The label of the account to open the options menu for.
+ */
+ async openAccountOptionsInAccountList(accountLabel: string): Promise {
+ console.log(
+ `Open account options in account list for account ${accountLabel}`,
+ );
+ await this.driver.clickElement(
+ `button[data-testid="account-list-item-menu-button"][aria-label="${accountLabel} Options"]`,
+ );
+ }
+
async openAccountOptionsMenu(): Promise {
console.log(`Open account option menu`);
await this.driver.waitForSelector(this.accountListItem);
@@ -175,6 +280,19 @@ class AccountListPage {
await this.driver.clickElement(this.pinUnpinAccountButton);
}
+ /**
+ * Remove the specified account from the account list.
+ *
+ * @param accountLabel - The label of the account to remove.
+ */
+ async removeAccount(accountLabel: string): Promise {
+ console.log(`Remove account in account list`);
+ await this.openAccountOptionsInAccountList(accountLabel);
+ await this.driver.clickElement(this.removeAccountButton);
+ await this.driver.waitForSelector(this.removeAccountMessage);
+ await this.driver.clickElement(this.removeAccountConfirmButton);
+ }
+
async switchToAccount(expectedLabel: string): Promise {
console.log(
`Switch to account with label ${expectedLabel} in account list`,
@@ -259,6 +377,32 @@ class AccountListPage {
await this.driver.assertElementNotPresent(this.addSnapAccountButton);
}
+ /**
+ * Check that the correct address is displayed in the account details modal.
+ *
+ * @param expectedAddress - The expected address to check.
+ */
+ async check_addressInAccountDetailsModal(
+ expectedAddress: string,
+ ): Promise {
+ console.log(
+ `Check that address ${expectedAddress} is displayed in account details modal`,
+ );
+ await this.driver.waitForSelector(this.accountQrCodeImage);
+ await this.driver.waitForSelector({
+ css: this.accountQrCodeAddress,
+ text: expectedAddress,
+ });
+ }
+
+ async check_currentAccountIsImported(): Promise {
+ console.log(`Check that current account is an imported account`);
+ await this.driver.waitForSelector({
+ css: this.currentSelectedAccount,
+ text: 'Imported',
+ });
+ }
+
async check_hiddenAccountsListExists(): Promise {
console.log(`Check that hidden accounts list is displayed in account list`);
await this.driver.waitForSelector(this.hiddenAccountsList);
@@ -282,6 +426,21 @@ class AccountListPage {
return internalAccounts.length === expectedNumberOfAccounts;
}, 20000);
}
+
+ /**
+ * Check that the remove account button is not displayed in the account options menu for the specified account.
+ *
+ * @param accountLabel - The label of the account to check.
+ */
+ async check_removeAccountButtonIsNotDisplayed(
+ accountLabel: string,
+ ): Promise {
+ console.log(
+ `Check that remove account button is not displayed in account options menu for account ${accountLabel} in account list`,
+ );
+ await this.openAccountOptionsInAccountList(accountLabel);
+ await this.driver.assertElementNotPresent(this.removeAccountButton);
+ }
}
export default AccountListPage;
diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts
index b61279c8fe8c..37d1d640670f 100644
--- a/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts
+++ b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts
@@ -59,6 +59,21 @@ class OnboardingSrpPage {
await this.driver.pasteIntoField(this.srpWord0, seedPhrase);
}
+ /**
+ * Fill the SRP words with the provided seed phrase word by word
+ *
+ * @param seedPhrase - The seed phrase to fill. Defaults to E2E_SRP.
+ */
+ async fillSrpWordByWord(seedPhrase: string = E2E_SRP): Promise {
+ const words = seedPhrase.split(' ');
+ for (const word of words) {
+ await this.driver.pasteIntoField(
+ `[data-testid="import-srp__srp-word-${words.indexOf(word)}"]`,
+ word,
+ );
+ }
+ }
+
async check_confirmSrpButtonIsDisabled(): Promise {
console.log('Check that confirm SRP button is disabled');
const confirmSeedPhrase = await this.driver.findElement(
diff --git a/test/e2e/page-objects/pages/send/send-token-page.ts b/test/e2e/page-objects/pages/send/send-token-page.ts
index 60ffd86c6cdd..29222ed3d48c 100644
--- a/test/e2e/page-objects/pages/send/send-token-page.ts
+++ b/test/e2e/page-objects/pages/send/send-token-page.ts
@@ -1,47 +1,37 @@
import { strict as assert } from 'assert';
import { Driver } from '../../../webdriver/driver';
-import { RawLocator } from '../../common';
class SendTokenPage {
private driver: Driver;
- private inputRecipient: string;
+ private readonly assetPickerButton = '[data-testid="asset-picker-button"]';
- private inputAmount: string;
+ private readonly continueButton = {
+ text: 'Continue',
+ tag: 'button',
+ };
- private inputNFTAmount: string;
+ private readonly ensAddressAsRecipient = '[data-testid="ens-input-selected"]';
- private scanButton: string;
+ private readonly ensResolvedName =
+ '[data-testid="multichain-send-page__recipient__item__title"]';
- private continueButton: object;
+ private readonly inputAmount = '[data-testid="currency-input"]';
- private ensResolvedName: string;
+ private readonly inputNFTAmount = '[data-testid="nft-input"]';
- private ensAddressAsRecipient: string;
+ private readonly inputRecipient = '[data-testid="ens-input"]';
- private ensResolvedAddress: string;
+ private readonly recipientAccount =
+ '.multichain-account-list-item__account-name__button';
- private assetPickerButton: RawLocator;
+ private readonly scanButton = '[data-testid="ens-qr-scan-button"]';
- private tokenListButton: RawLocator;
+ private readonly tokenListButton =
+ '[data-testid="multichain-token-list-button"]';
constructor(driver: Driver) {
this.driver = driver;
- this.inputAmount = '[data-testid="currency-input"]';
- this.inputNFTAmount = '[data-testid="nft-input"]';
- this.inputRecipient = '[data-testid="ens-input"]';
- this.scanButton = '[data-testid="ens-qr-scan-button"]';
- this.ensResolvedName =
- '[data-testid="multichain-send-page__recipient__item__title"]';
- this.ensResolvedAddress =
- '[data-testid="multichain-send-page__recipient__item__subtitle"]';
- this.ensAddressAsRecipient = '[data-testid="ens-input-selected"]';
- this.continueButton = {
- text: 'Continue',
- tag: 'button',
- };
- this.assetPickerButton = '[data-testid="asset-picker-button"]';
- this.tokenListButton = '[data-testid="multichain-token-list-button"]';
}
async check_pageIsLoaded(): Promise {
@@ -60,11 +50,13 @@ class SendTokenPage {
console.log('Send token screen is loaded');
}
- async fillRecipient(recipientAddress: string): Promise {
- console.log(
- `Fill recipient input with ${recipientAddress} on send token screen`,
- );
- await this.driver.pasteIntoField(this.inputRecipient, recipientAddress);
+ async clickAssetPickerButton() {
+ await this.driver.clickElement(this.assetPickerButton);
+ }
+
+ async clickSecondTokenListButton() {
+ const elements = await this.driver.findElements(this.tokenListButton);
+ await elements[1].click();
}
async fillAmount(amount: string): Promise {
@@ -86,31 +78,31 @@ class SendTokenPage {
await this.driver.pasteIntoField(this.inputNFTAmount, amount);
}
+ /**
+ * Fill recipient address input on send token screen.
+ *
+ * @param recipientAddress - The recipient address to fill in the input field.
+ */
+ async fillRecipient(recipientAddress: string): Promise {
+ console.log(
+ `Fill recipient input with ${recipientAddress} on send token screen`,
+ );
+ await this.driver.pasteIntoField(this.inputRecipient, recipientAddress);
+ }
+
async goToNextScreen(): Promise {
await this.driver.clickElement(this.continueButton);
}
/**
- * Verifies that an ENS domain correctly resolves to the specified Ethereum address on the send token screen.
+ * Select recipient account on send token screen.
*
- * @param ensDomain - The ENS domain name expected to resolve (e.g., "test.eth").
- * @param address - The Ethereum address to which the ENS domain is expected to resolve.
- * @returns A promise that resolves if the ENS domain successfully resolves to the specified address on send token screen.
+ * @param recipientAccount - The recipient account to select.
*/
- async check_ensAddressResolution(
- ensDomain: string,
- address: string,
- ): Promise {
- console.log(
- `Check ENS domain resolution: '${ensDomain}' should resolve to address '${address}' on the send token screen.`,
- );
- // check if ens domain is resolved as expected address
- await this.driver.waitForSelector({
- text: ensDomain,
- css: this.ensResolvedName,
- });
- await this.driver.waitForSelector({
- text: address,
+ async selectRecipientAccount(recipientAccount: string): Promise {
+ await this.driver.clickElement({
+ text: recipientAccount,
+ css: this.recipientAccount,
});
}
@@ -140,13 +132,28 @@ class SendTokenPage {
);
}
- async click_assetPickerButton() {
- await this.driver.clickElement(this.assetPickerButton);
- }
-
- async click_secondTokenListButton() {
- const elements = await this.driver.findElements(this.tokenListButton);
- await elements[1].click();
+ /**
+ * Verifies that an ENS domain correctly resolves to the specified Ethereum address on the send token screen.
+ *
+ * @param ensDomain - The ENS domain name expected to resolve (e.g., "test.eth").
+ * @param address - The Ethereum address to which the ENS domain is expected to resolve.
+ * @returns A promise that resolves if the ENS domain successfully resolves to the specified address on send token screen.
+ */
+ async check_ensAddressResolution(
+ ensDomain: string,
+ address: string,
+ ): Promise {
+ console.log(
+ `Check ENS domain resolution: '${ensDomain}' should resolve to address '${address}' on the send token screen.`,
+ );
+ // check if ens domain is resolved as expected address
+ await this.driver.waitForSelector({
+ text: ensDomain,
+ css: this.ensResolvedName,
+ });
+ await this.driver.waitForSelector({
+ text: address,
+ });
}
}
diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js
deleted file mode 100644
index 04980cf20c3e..000000000000
--- a/test/e2e/tests/account/add-account.spec.js
+++ /dev/null
@@ -1,232 +0,0 @@
-const { strict: assert } = require('assert');
-const {
- TEST_SEED_PHRASE,
- withFixtures,
- completeImportSRPOnboardingFlow,
- sendTransaction,
- findAnotherAccountFromAccountList,
- locateAccountBalanceDOM,
- logInWithBalanceValidation,
- regularDelayMs,
- unlockWallet,
- WALLET_PASSWORD,
- generateGanacheOptions,
-} = require('../../helpers');
-
-const FixtureBuilder = require('../../fixture-builder');
-
-describe('Add account', function () {
- const secondAccount = '0x3ED0eE22E0685Ebbf07b2360A8331693c413CC59';
-
- const ganacheOptions = generateGanacheOptions({
- secretKey:
- '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9',
- });
-
- it('should display correct new account name after create', async function () {
- await withFixtures(
- {
- fixtures: new FixtureBuilder().build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver }) => {
- await unlockWallet(driver);
-
- await driver.clickElement('[data-testid="account-menu-icon"]');
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-add-account"]',
- );
-
- await driver.fill('[placeholder="Account 2"]', '2nd account');
- // needed to mitigate a race condition with the state update
- // there is no condition we can wait for in the UI
- await driver.delay(regularDelayMs);
- await driver.clickElement({ text: 'Add account', tag: 'button' });
- await driver.findElement({
- css: '[data-testid="account-menu-icon"]',
- text: '2nd account',
- });
- },
- );
- });
-
- it('should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi', async function () {
- await withFixtures(
- {
- fixtures: new FixtureBuilder({ onboarding: true }).build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver, ganacheServer }) => {
- await driver.navigate();
-
- // On boarding with 1st account
- await completeImportSRPOnboardingFlow(
- driver,
- TEST_SEED_PHRASE,
- WALLET_PASSWORD,
- );
-
- // Check address of 1st accoun
- await locateAccountBalanceDOM(driver, ganacheServer);
- await driver.findElement('[data-testid="app-header-copy-button"]');
-
- // Create 2nd account
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-add-account"]',
- );
- await driver.fill('[placeholder="Account 2"]', '2nd account');
- // needed to mitigate a race condition with the state update
- // there is no condition we can wait for in the UI
- await driver.delay(regularDelayMs);
- await driver.clickElement({ text: 'Add account', tag: 'button' });
-
- // Check address of 2nd account
- await locateAccountBalanceDOM(driver);
- await driver.findElement('[data-testid="app-header-copy-button"]');
-
- // Log into the account with balance(account 1)
- // and transfer some balance to 2nd account
- // so they will not be removed after recovering SRP
- const accountOneSelector = await findAnotherAccountFromAccountList(
- driver,
- 1,
- 'Account 1',
- );
- await locateAccountBalanceDOM(driver);
- await driver.clickElement(accountOneSelector);
- await sendTransaction(driver, secondAccount, '2.8');
-
- // Lock the account
- await driver.clickElement(
- '[data-testid="account-options-menu-button"]',
- );
-
- await driver.clickElement('[data-testid="global-menu-lock"]');
- await driver.waitForSelector('[data-testid="unlock-page"]');
-
- // Recover via SRP in "forget password" option
- await driver.clickElement('.unlock-page__link');
- await driver.pasteIntoField(
- '[data-testid="import-srp__srp-word-0"]',
- TEST_SEED_PHRASE,
- );
- await driver.fill('#password', 'correct horse battery staple');
- await driver.fill('#confirm-password', 'correct horse battery staple');
-
- await driver.clickElement(
- '[data-testid="create-new-vault-submit-button"]',
- );
-
- // Land in 1st account home page
- await driver.findElement('.home__main-view');
- await locateAccountBalanceDOM(driver);
-
- // Check address of 1st account
- await driver.findElement('[data-testid="app-header-copy-button"]');
-
- // Check address of 2nd account
- await driver.clickElement('[data-testid="account-menu-icon"]');
- await driver.clickElement({
- css: `.multichain-account-list-item__account-name__button`,
- text: 'Account 2',
- });
-
- await driver.findElement('[data-testid="app-header-copy-button"]');
- },
- );
- });
-
- it('should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding @no-mmi', async function () {
- const testPrivateKey =
- '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6';
-
- await withFixtures(
- {
- fixtures: new FixtureBuilder().build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver }) => {
- await logInWithBalanceValidation(driver);
-
- await driver.clickElement('[data-testid="account-menu-icon"]');
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-add-account"]',
- );
- await driver.fill('[placeholder="Account 2"]', '2nd account');
- // needed to mitigate a race condition with the state update
- // there is no condition we can wait for in the UI
- await driver.delay(regularDelayMs);
- await driver.clickElement({ text: 'Add account', tag: 'button' });
-
- // Wait for 2nd account to be created
- await locateAccountBalanceDOM(driver);
- await driver.findElement({
- css: '[data-testid="account-menu-icon"]',
- text: '2nd account',
- });
-
- await driver.clickElement('[data-testid="account-menu-icon"]');
- const menuItems = await driver.findElements(
- '.multichain-account-list-item',
- );
- assert.equal(menuItems.length, 2);
-
- // User cannot delete 2nd account generated from the SRP imported in onboarding
- await driver.clickElement(
- '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]',
- );
- await driver.assertElementNotPresent(
- '[data-testid="account-list-menu-remove"]',
- );
-
- // Create 3rd account with private key
- await driver.clickElement('.mm-text-field');
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({ text: 'Import account', tag: 'button' });
- await driver.fill('#private-key-box', testPrivateKey);
-
- await driver.clickElement(
- '[data-testid="import-account-confirm-button"]',
- );
-
- // Wait for 3rd account to be created
- await locateAccountBalanceDOM(driver);
- await driver.findElement({
- css: '[data-testid="account-menu-icon"]',
- text: 'Account 3',
- });
-
- // User can delete 3rd account imported with a private key
- await driver.clickElement('[data-testid="account-menu-icon"]');
- const importedMenuItems = await driver.findElements(
- '.multichain-account-list-item',
- );
- assert.equal(importedMenuItems.length, 3);
- await driver.clickElement(
- '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]',
- );
- await driver.findElement('[data-testid="account-list-menu-remove"]');
- },
- );
- });
-});
diff --git a/test/e2e/tests/account/add-account.spec.ts b/test/e2e/tests/account/add-account.spec.ts
new file mode 100644
index 000000000000..3fa65b14e87d
--- /dev/null
+++ b/test/e2e/tests/account/add-account.spec.ts
@@ -0,0 +1,123 @@
+import {
+ withFixtures,
+ WALLET_PASSWORD,
+ defaultGanacheOptions,
+} from '../../helpers';
+import { E2E_SRP } from '../../default-fixture';
+import FixtureBuilder from '../../fixture-builder';
+import AccountListPage from '../../page-objects/pages/account-list-page';
+import HeaderNavbar from '../../page-objects/pages/header-navbar';
+import HomePage from '../../page-objects/pages/homepage';
+import LoginPage from '../../page-objects/pages/login-page';
+import ResetPasswordPage from '../../page-objects/pages/reset-password-page';
+import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
+import { completeImportSRPOnboardingFlow } from '../../page-objects/flows/onboarding.flow';
+import { sendTransactionToAccount } from '../../page-objects/flows/send-transaction.flow';
+
+describe('Add account', function () {
+ it('should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi', async function () {
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder({ onboarding: true }).build(),
+ ganacheOptions: defaultGanacheOptions,
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver, ganacheServer }) => {
+ await completeImportSRPOnboardingFlow({ driver });
+ const homePage = new HomePage(driver);
+ await homePage.check_pageIsLoaded();
+ await homePage.check_ganacheBalanceIsDisplayed(ganacheServer);
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.openAccountMenu();
+
+ // Create new account with default name Account 2
+ const accountListPage = new AccountListPage(driver);
+ await accountListPage.check_pageIsLoaded();
+ await accountListPage.addNewAccountWithDefaultName();
+ await headerNavbar.check_accountLabel('Account 2');
+ await homePage.check_expectedBalanceIsDisplayed();
+
+ // Switch back to the first account and transfer some balance to 2nd account so they will not be removed after recovering SRP
+ await headerNavbar.openAccountMenu();
+ await accountListPage.check_pageIsLoaded();
+ await accountListPage.check_accountDisplayedInAccountList('Account 1');
+ await accountListPage.switchToAccount('Account 1');
+ await headerNavbar.check_accountLabel('Account 1');
+ await homePage.check_ganacheBalanceIsDisplayed(ganacheServer);
+ await sendTransactionToAccount({
+ driver,
+ recipientAccount: 'Account 2',
+ amount: '2.8',
+ gasFee: '0.000042',
+ totalFee: '2.800042',
+ });
+ await homePage.check_pageIsLoaded();
+ await homePage.check_confirmedTxNumberDisplayedInActivity();
+ await homePage.check_txAmountInActivity('-2.8 ETH');
+
+ // Lock wallet and recover via SRP in "forget password" option
+ await headerNavbar.lockMetaMask();
+ await new LoginPage(driver).gotoResetPasswordPage();
+ const resetPasswordPage = new ResetPasswordPage(driver);
+ await resetPasswordPage.check_pageIsLoaded();
+ await resetPasswordPage.resetPassword(E2E_SRP, WALLET_PASSWORD);
+
+ // Check wallet balance for both accounts
+ await homePage.check_pageIsLoaded();
+ await homePage.check_ganacheBalanceIsDisplayed(ganacheServer);
+ await headerNavbar.openAccountMenu();
+ await accountListPage.check_pageIsLoaded();
+ await accountListPage.check_accountDisplayedInAccountList('Account 2');
+ await accountListPage.switchToAccount('Account 2');
+ await headerNavbar.check_accountLabel('Account 2');
+ await homePage.check_expectedBalanceIsDisplayed('2.8');
+ },
+ );
+ });
+
+ it('should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding @no-mmi', async function () {
+ const testPrivateKey: string =
+ '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6';
+
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder().build(),
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }) => {
+ await loginWithBalanceValidation(driver);
+ const headerNavbar = new HeaderNavbar(driver);
+ const homePage = new HomePage(driver);
+ await headerNavbar.openAccountMenu();
+
+ // Create new account with default name Account 2
+ const accountListPage = new AccountListPage(driver);
+ await accountListPage.check_pageIsLoaded();
+ await accountListPage.addNewAccountWithDefaultName();
+ await headerNavbar.check_accountLabel('Account 2');
+ await homePage.check_expectedBalanceIsDisplayed();
+
+ // Check user cannot delete 2nd account generated from the SRP imported in onboarding
+ await headerNavbar.openAccountMenu();
+ await accountListPage.check_removeAccountButtonIsNotDisplayed(
+ 'Account 1',
+ );
+
+ // Create 3rd account with private key
+ await accountListPage.addNewImportedAccount(testPrivateKey);
+ await headerNavbar.check_accountLabel('Account 3');
+ await homePage.check_expectedBalanceIsDisplayed();
+
+ // Remove the 3rd account imported with a private key
+ await headerNavbar.openAccountMenu();
+ await accountListPage.removeAccount('Account 3');
+ await homePage.check_pageIsLoaded();
+ await homePage.check_expectedBalanceIsDisplayed();
+ await headerNavbar.openAccountMenu();
+ await accountListPage.check_accountIsNotDisplayedInAccountList(
+ 'Account 3',
+ );
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/account/import-flow.spec.js b/test/e2e/tests/account/import-flow.spec.js
deleted file mode 100644
index d2c84bfdc2b3..000000000000
--- a/test/e2e/tests/account/import-flow.spec.js
+++ /dev/null
@@ -1,474 +0,0 @@
-const { strict: assert } = require('assert');
-const path = require('path');
-const {
- TEST_SEED_PHRASE,
- convertToHexValue,
- withFixtures,
- regularDelayMs,
- largeDelayMs,
- completeImportSRPOnboardingFlow,
- completeImportSRPOnboardingFlowWordByWord,
- openActionMenuAndStartSendFlow,
- unlockWallet,
- logInWithBalanceValidation,
- locateAccountBalanceDOM,
- WALLET_PASSWORD,
-} = require('../../helpers');
-const FixtureBuilder = require('../../fixture-builder');
-const { emptyHtmlPage } = require('../../mock-e2e');
-const { isManifestV3 } = require('../../../../shared/modules/mv3.utils');
-
-const ganacheOptions = {
- accounts: [
- {
- secretKey:
- '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9',
- balance: convertToHexValue(25000000000000000000),
- },
- ],
-};
-
-async function mockTrezor(mockServer) {
- return await mockServer
- .forGet('https://connect.trezor.io/9/popup.html')
- .thenCallback(() => {
- return {
- statusCode: 200,
- body: emptyHtmlPage(),
- };
- });
-}
-
-describe('Import flow @no-mmi', function () {
- it('Import wallet using Secret Recovery Phrase', async function () {
- await withFixtures(
- {
- fixtures: new FixtureBuilder({ onboarding: true }).build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver, ganacheServer }) => {
- await driver.navigate();
-
- await completeImportSRPOnboardingFlow(
- driver,
- TEST_SEED_PHRASE,
- WALLET_PASSWORD,
- );
-
- // Show account information
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="account-list-item-menu-button"]',
- );
- await driver.clickElement('[data-testid="account-list-menu-details"]');
- await driver.findVisibleElement('.qr-code__wrapper');
-
- // shows a QR code for the account
- await driver.findVisibleElement(
- '[data-testid="account-details-modal"]',
- );
- // shows the correct account address
- await driver.findElement('[data-testid="app-header-copy-button"]');
-
- await driver.clickElement('button[aria-label="Close"]');
- await driver.assertElementNotPresent(
- '[data-testid="account-details-modal"]',
- );
- // logs out of the account
- await driver.clickElement(
- '[data-testid="account-options-menu-button"]',
- );
- await driver.clickElement({
- css: '[data-testid="global-menu-lock"]',
- text: 'Lock MetaMask',
- });
-
- // accepts the account password after lock
- await unlockWallet(driver, {
- navigate: false,
- waitLoginSuccess: false,
- });
-
- // Create a new account
- // switches to localhost
- await driver.delay(largeDelayMs);
- await driver.clickElement('[data-testid="network-display"]');
- await driver.clickElement('.toggle-button');
- await driver.clickElement({ text: 'Localhost', tag: 'p' });
-
- // choose Create account from the account menu
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({
- text: 'Add a new Ethereum account',
- tag: 'button',
- });
-
- // set account name
- await driver.fill('[placeholder="Account 2"]', '2nd account');
- await driver.delay(regularDelayMs);
- await driver.clickElement({ text: 'Add account', tag: 'button' });
-
- // should show the correct account name
- const accountName = await driver.isElementPresent({
- tag: 'span',
- text: '2nd account',
- });
-
- assert.equal(accountName, true, 'Account name is not correct');
-
- // Switch back to original account
- // chooses the original account from the account menu
- await driver.clickElement('[data-testid="account-menu-icon"]');
- await driver.clickElement(
- '.multichain-account-list-item__account-name__button',
- );
-
- // Send ETH from inside MetaMask
- // starts a send transaction
- await locateAccountBalanceDOM(driver, ganacheServer);
- await openActionMenuAndStartSendFlow(driver);
- await driver.fill(
- 'input[placeholder="Enter public address (0x) or domain name"]',
- '0x2f318C334780961FB129D2a6c30D0763d9a5C970',
- );
- await driver.fill('input[placeholder="0"]', '1');
- // Continue to next screen
- await driver.clickElement({ text: 'Continue', tag: 'button' });
-
- // confirms the transaction
- await driver.clickElement({ text: 'Confirm', tag: 'button' });
-
- // finds the transaction in the transactions list
- await driver.clickElement(
- '[data-testid="account-overview__activity-tab"]',
- );
- await driver.wait(async () => {
- const confirmedTxes = await driver.findElements(
- '.transaction-list__completed-transactions .activity-list-item',
- );
- return confirmedTxes.length === 1;
- }, 10000);
-
- const txValues = await driver.findElements(
- '[data-testid="transaction-list-item-primary-currency"]',
- );
- assert.equal(txValues.length, 1);
- assert.ok(/-1\s*ETH/u.test(await txValues[0].getText()));
- },
- );
- });
-
- it('Import wallet using Secret Recovery Phrase with pasting word by word', async function () {
- const testAddress = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3';
-
- await withFixtures(
- {
- fixtures: new FixtureBuilder({ onboarding: true }).build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver }) => {
- await driver.navigate();
-
- await completeImportSRPOnboardingFlowWordByWord(
- driver,
- TEST_SEED_PHRASE,
- WALLET_PASSWORD,
- );
-
- // Show account information
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="account-list-item-menu-button"]',
- );
- await driver.clickElement('[data-testid="account-list-menu-details"');
- await driver.findVisibleElement('.qr-code__wrapper');
-
- // Extract address segments from the DOM
- const outerSegment = await driver.findElement(
- '.qr-code__address-segments',
- );
-
- // Get the text content of each segment
- const displayedAddress = await outerSegment.getText();
-
- // Assert that the displayed address matches the testAddress
- assert.strictEqual(
- displayedAddress.toLowerCase(),
- testAddress.toLowerCase(),
- 'The displayed address does not match the test address',
- );
- },
- );
- });
-
- it('Import Account using private key and remove imported account', async function () {
- const testPrivateKey1 =
- '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6';
- const testPrivateKey2 =
- 'F4EC2590A0C10DE95FBF4547845178910E40F5035320C516A18C117DE02B5669';
-
- await withFixtures(
- {
- fixtures: new FixtureBuilder()
- .withKeyringControllerImportedAccountVault()
- .withPreferencesControllerImportedAccountIdentities()
- .withAccountsControllerImportedAccount()
- .build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver }) => {
- await unlockWallet(driver);
-
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({ text: 'Import account', tag: 'button' });
-
- // Imports Account 4 with private key
- await driver.findClickableElement('#private-key-box');
- await driver.fill('#private-key-box', testPrivateKey1);
- await driver.clickElement(
- '[data-testid="import-account-confirm-button"]',
- );
-
- // New imported account has correct name and label
- await driver.findClickableElement({
- css: '[data-testid="account-menu-icon"]',
- text: 'Account 4',
- });
- await driver.clickElement('[data-testid="account-menu-icon"]');
- await driver.findElement({
- css: `.multichain-account-list-item--selected .multichain-account-list-item__content .mm-tag`,
- text: 'Imported',
- });
-
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 4',
- tag: 'span',
- });
- // Imports Account 5 with private key
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({ text: 'Import account', tag: 'button' });
- await driver.findClickableElement('#private-key-box');
- await driver.fill('#private-key-box', testPrivateKey2);
- await driver.clickElement(
- '[data-testid="import-account-confirm-button"]',
- );
-
- // New imported account has correct name and label
- await driver.findClickableElement({
- css: '[data-testid="account-menu-icon"]',
- text: 'Account 5',
- });
- await driver.clickElement('[data-testid="account-menu-icon"]');
- const accountListItems = await driver.findElements(
- '.multichain-account-list-item',
- );
- assert.equal(accountListItems.length, 5);
-
- await driver.clickElement(
- '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]',
- );
-
- // Account 5 can be removed
- await driver.clickElement('[data-testid="account-list-menu-remove"]');
- await driver.clickElement({ text: 'Remove', tag: 'button' });
-
- await driver.delay(1000);
- await driver.clickElementUsingMouseMove({
- css: '[data-testid="account-menu-icon"]',
- text: 'Account 4',
- });
- const accountListItemsAfterRemoval = await driver.findElements(
- '.multichain-account-list-item',
- );
- assert.equal(accountListItemsAfterRemoval.length, 4);
- },
- );
- });
-
- it('Import Account using json file', async function () {
- await withFixtures(
- {
- fixtures: new FixtureBuilder()
- .withKeyringControllerImportedAccountVault()
- .withPreferencesControllerImportedAccountIdentities()
- .withAccountsControllerImportedAccount()
- .build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver, ganacheServer }) => {
- await logInWithBalanceValidation(driver, ganacheServer);
- // Imports an account with JSON file
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
-
- await driver.clickElement({ text: 'Import account', tag: 'button' });
-
- await driver.clickElement('.dropdown__select');
- await driver.clickElement({ text: 'JSON File', tag: 'option' });
-
- const fileInput = await driver.findElement('input[type="file"]');
- const importJsonFile = path.join(
- __dirname,
- '../..',
- 'import-utc-json',
- 'test-json-import-account-file.json',
- );
-
- fileInput.sendKeys(importJsonFile);
-
- await driver.fill('#json-password-box', 'foobarbazqux');
- await driver.clickElement(
- '[data-testid="import-account-confirm-button"]',
- );
-
- const importedAccount = '0x0961Ca10D49B9B8e371aA0Bcf77fE5730b18f2E4';
- await locateAccountBalanceDOM(driver, ganacheServer, importedAccount);
- // New imported account has correct name and label
- await driver.findClickableElement({
- css: '[data-testid="account-menu-icon"]',
- text: 'Account 4',
- });
-
- await driver.clickElement('[data-testid="account-menu-icon"]');
-
- await driver.findElement({
- css: `.multichain-account-list-item--selected .multichain-account-list-item__content .mm-tag`,
- text: 'Imported',
- });
-
- const accountListItems = await driver.findElements(
- '.multichain-account-list-item',
- );
- assert.equal(accountListItems.length, 4);
- },
- );
- });
-
- it('Import Account using private key of an already active account should result in an error', async function () {
- const testPrivateKey =
- '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9';
- await withFixtures(
- {
- fixtures: new FixtureBuilder()
- .withKeyringControllerImportedAccountVault()
- .withPreferencesControllerImportedAccountIdentities()
- .withAccountsControllerImportedAccount()
- .build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- },
- async ({ driver }) => {
- await unlockWallet(driver);
-
- // choose Import Account from the account menu
- await driver.clickElement('[data-testid="account-menu-icon"]');
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({ text: 'Import account', tag: 'button' });
-
- // enter private key
- await driver.findClickableElement('#private-key-box');
- await driver.fill('#private-key-box', testPrivateKey);
- await driver.clickElement(
- '[data-testid="import-account-confirm-button"]',
- );
-
- // error should occur
- await driver.waitForSelector({
- css: '.mm-help-text',
- text: 'The account you are trying to import is a duplicate',
- });
- },
- );
- });
-
- it('Connects to a Hardware wallet for lattice', async function () {
- await withFixtures(
- {
- fixtures: new FixtureBuilder().build(),
- ganacheOptions,
- title: this.test.fullTitle(),
- testSpecificMock: mockTrezor,
- },
- async ({ driver }) => {
- await unlockWallet(driver);
-
- // choose Connect hardware wallet from the account menu
- await driver.clickElement('[data-testid="account-menu-icon"]');
-
- // Wait until account list is loaded to mitigate race condition
- await driver.waitForSelector({
- text: 'Account 1',
- tag: 'span',
- });
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({
- text: 'Add hardware wallet',
- tag: 'button',
- });
- await driver.findClickableElement(
- '[data-testid="hardware-connect-close-btn"]',
- );
- await driver.clickElement('[data-testid="connect-lattice-btn"]');
- await driver.findClickableElement({
- text: 'Continue',
- tag: 'button',
- });
-
- await driver.clickElement({ text: 'Continue', tag: 'button' });
-
- const allWindows = await driver.waitUntilXWindowHandles(2);
-
- assert.equal(allWindows.length, isManifestV3 ? 3 : 2);
- },
- );
- });
-});
diff --git a/test/e2e/tests/account/import-flow.spec.ts b/test/e2e/tests/account/import-flow.spec.ts
new file mode 100644
index 000000000000..8dc989db4d41
--- /dev/null
+++ b/test/e2e/tests/account/import-flow.spec.ts
@@ -0,0 +1,115 @@
+import path from 'path';
+import { withFixtures } from '../../helpers';
+import FixtureBuilder from '../../fixture-builder';
+import AccountListPage from '../../page-objects/pages/account-list-page';
+import HeaderNavbar from '../../page-objects/pages/header-navbar';
+import HomePage from '../../page-objects/pages/homepage';
+import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
+import { completeImportSRPOnboardingFlow } from '../../page-objects/flows/onboarding.flow';
+
+describe('Import flow @no-mmi', function () {
+ it('Import wallet using Secret Recovery Phrase with pasting word by word', async function () {
+ const testAddress = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1';
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder({ onboarding: true }).build(),
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }) => {
+ await completeImportSRPOnboardingFlow({
+ driver,
+ fillSrpWordByWord: true,
+ });
+ const homePage = new HomePage(driver);
+ await homePage.check_pageIsLoaded();
+ await homePage.check_expectedBalanceIsDisplayed();
+
+ // Open account details modal and check displayed account address
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.check_accountLabel('Account 1');
+ await headerNavbar.openAccountMenu();
+
+ const accountListPage = new AccountListPage(driver);
+ await accountListPage.check_pageIsLoaded();
+ await accountListPage.openAccountDetailsModal('Account 1');
+ await accountListPage.check_addressInAccountDetailsModal(
+ testAddress.toLowerCase(),
+ );
+ },
+ );
+ });
+
+ it('Import Account using json file', async function () {
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder()
+ .withKeyringControllerImportedAccountVault()
+ .withPreferencesControllerImportedAccountIdentities()
+ .withAccountsControllerImportedAccount()
+ .build(),
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }) => {
+ await loginWithBalanceValidation(driver);
+
+ // Wait until account list is loaded to mitigate race condition
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.check_accountLabel('Account 1');
+ await headerNavbar.openAccountMenu();
+ const accountListPage = new AccountListPage(driver);
+ await accountListPage.check_pageIsLoaded();
+
+ // Imports an account with JSON file
+ const jsonFile = path.join(
+ __dirname,
+ '../..',
+ 'import-utc-json',
+ 'test-json-import-account-file.json',
+ );
+ await accountListPage.importAccountWithJsonFile(
+ jsonFile,
+ 'foobarbazqux',
+ );
+
+ // Check new imported account has correct name and label
+ const homePage = new HomePage(driver);
+ await homePage.check_pageIsLoaded();
+ await homePage.check_expectedBalanceIsDisplayed();
+ await headerNavbar.check_accountLabel('Account 4');
+
+ await headerNavbar.openAccountMenu();
+ await accountListPage.check_pageIsLoaded();
+ await accountListPage.check_numberOfAvailableAccounts(4);
+ await accountListPage.check_currentAccountIsImported();
+ },
+ );
+ });
+
+ it('Import Account using private key of an already active account should result in an error', async function () {
+ const testPrivateKey =
+ '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9';
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder()
+ .withKeyringControllerImportedAccountVault()
+ .build(),
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }) => {
+ await loginWithBalanceValidation(driver);
+
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.check_accountLabel('Account 1');
+ await headerNavbar.openAccountMenu();
+ const accountListPage = new AccountListPage(driver);
+ await accountListPage.check_pageIsLoaded();
+
+ // import active account with private key from the account menu and check error message
+ await accountListPage.addNewImportedAccount(
+ testPrivateKey,
+ 'The account you are trying to import is a duplicate',
+ );
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/account/snap-account-transfers.spec.ts b/test/e2e/tests/account/snap-account-transfers.spec.ts
index 23cc5d510eb2..ee2466cf28e1 100644
--- a/test/e2e/tests/account/snap-account-transfers.spec.ts
+++ b/test/e2e/tests/account/snap-account-transfers.spec.ts
@@ -47,13 +47,13 @@ describe('Snap Account Transfers @no-mmi', function (this: Suite) {
await headerNavbar.check_accountLabel('SSK Account');
// send 1 ETH from snap account to account 1
- await sendTransactionWithSnapAccount(
+ await sendTransactionWithSnapAccount({
driver,
- DEFAULT_FIXTURE_ACCOUNT,
- '1',
- '0.000042',
- '1.000042',
- );
+ recipientAddress: DEFAULT_FIXTURE_ACCOUNT,
+ amount: '1',
+ gasFee: '0.000042',
+ totalFee: '1.000042',
+ });
await headerNavbar.check_pageIsLoaded();
await headerNavbar.openAccountMenu();
const accountList = new AccountListPage(driver);
@@ -95,14 +95,14 @@ describe('Snap Account Transfers @no-mmi', function (this: Suite) {
await headerNavbar.check_accountLabel('SSK Account');
// send 1 ETH from snap account to account 1 and approve the transaction
- await sendTransactionWithSnapAccount(
+ await sendTransactionWithSnapAccount({
driver,
- DEFAULT_FIXTURE_ACCOUNT,
- '1',
- '0.000042',
- '1.000042',
- false,
- );
+ recipientAddress: DEFAULT_FIXTURE_ACCOUNT,
+ amount: '1',
+ gasFee: '0.000042',
+ totalFee: '1.000042',
+ isSyncFlow: false,
+ });
await headerNavbar.check_pageIsLoaded();
await headerNavbar.openAccountMenu();
const accountList = new AccountListPage(driver);
@@ -145,15 +145,15 @@ describe('Snap Account Transfers @no-mmi', function (this: Suite) {
await headerNavbar.check_accountLabel('SSK Account');
// send 1 ETH from snap account to account 1 and reject the transaction
- await sendTransactionWithSnapAccount(
+ await sendTransactionWithSnapAccount({
driver,
- DEFAULT_FIXTURE_ACCOUNT,
- '1',
- '0.000042',
- '1.000042',
- false,
- false,
- );
+ recipientAddress: DEFAULT_FIXTURE_ACCOUNT,
+ amount: '1',
+ gasFee: '0.000042',
+ totalFee: '1.000042',
+ isSyncFlow: false,
+ approveTransaction: false,
+ });
// check the transaction is failed in MetaMask activity list
const homepage = new HomePage(driver);
diff --git a/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts
index 7bacf156b71a..9cd1d3837729 100644
--- a/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts
+++ b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts
@@ -144,8 +144,8 @@ async function createWalletInitiatedTransactionAndAssertDetails(
await sendToPage.fillRecipient('0x2f318C334780961FB129D2a6c30D0763d9a5C970');
await sendToPage.fillAmount('1');
- await sendToPage.click_assetPickerButton();
- await sendToPage.click_secondTokenListButton();
+ await sendToPage.clickAssetPickerButton();
+ await sendToPage.clickSecondTokenListButton();
await sendToPage.goToNextScreen();
const tokenTransferTransactionConfirmation =
diff --git a/test/e2e/tests/hardware-wallets/lattice-connect.spec.ts b/test/e2e/tests/hardware-wallets/lattice-connect.spec.ts
new file mode 100644
index 000000000000..b8497a9df692
--- /dev/null
+++ b/test/e2e/tests/hardware-wallets/lattice-connect.spec.ts
@@ -0,0 +1,44 @@
+import { strict as assert } from 'assert';
+import { Suite } from 'mocha';
+import { Driver } from '../../webdriver/driver';
+import FixtureBuilder from '../../fixture-builder';
+import { withFixtures, unlockWallet } from '../../helpers';
+import { isManifestV3 } from '../../../../shared/modules/mv3.utils';
+
+describe('Lattice hardware wallet @no-mmi', function (this: Suite) {
+ it('connects to lattice hardware wallet', async function () {
+ await withFixtures(
+ {
+ fixtures: new FixtureBuilder().build(),
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }: { driver: Driver }) => {
+ await unlockWallet(driver);
+
+ // choose Connect hardware wallet from the account menu
+ await driver.clickElement('[data-testid="account-menu-icon"]');
+
+ // Wait until account list is loaded to mitigate race condition
+ await driver.waitForSelector({
+ text: 'Account 1',
+ tag: 'span',
+ });
+ await driver.clickElement(
+ '[data-testid="multichain-account-menu-popover-action-button"]',
+ );
+ await driver.clickElement({
+ text: 'Add hardware wallet',
+ tag: 'button',
+ });
+ await driver.findClickableElement(
+ '[data-testid="hardware-connect-close-btn"]',
+ );
+ await driver.clickElement('[data-testid="connect-lattice-btn"]');
+ await driver.clickElement({ text: 'Continue', tag: 'button' });
+
+ const allWindows = await driver.waitUntilXWindowHandles(2);
+ assert.equal(allWindows.length, isManifestV3 ? 3 : 2);
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js
index a945154f4bd3..e6439c569339 100644
--- a/test/e2e/tests/privacy/basic-functionality.spec.js
+++ b/test/e2e/tests/privacy/basic-functionality.spec.js
@@ -1,13 +1,10 @@
const { strict: assert } = require('assert');
-const {
- TEST_SEED_PHRASE,
- withFixtures,
- importSRPOnboardingFlow,
- WALLET_PASSWORD,
- defaultGanacheOptions,
-} = require('../../helpers');
+const { defaultGanacheOptions, withFixtures } = require('../../helpers');
const { METAMASK_STALELIST_URL } = require('../phishing-controller/helpers');
const FixtureBuilder = require('../../fixture-builder');
+const {
+ importSRPOnboardingFlow,
+} = require('../../page-objects/flows/onboarding.flow');
async function mockApis(mockServer) {
return [
@@ -49,12 +46,7 @@ describe('MetaMask onboarding @no-mmi', function () {
testSpecificMock: mockApis,
},
async ({ driver, mockedEndpoint: mockedEndpoints }) => {
- await driver.navigate();
- await importSRPOnboardingFlow(
- driver,
- TEST_SEED_PHRASE,
- WALLET_PASSWORD,
- );
+ await importSRPOnboardingFlow({ driver });
await driver.clickElement({
text: 'Manage default privacy settings',
@@ -134,12 +126,7 @@ describe('MetaMask onboarding @no-mmi', function () {
testSpecificMock: mockApis,
},
async ({ driver, mockedEndpoint: mockedEndpoints }) => {
- await driver.navigate();
- await importSRPOnboardingFlow(
- driver,
- TEST_SEED_PHRASE,
- WALLET_PASSWORD,
- );
+ await importSRPOnboardingFlow({ driver });
await driver.clickElement({
text: 'Manage default privacy settings',
@@ -171,6 +158,8 @@ describe('MetaMask onboarding @no-mmi', function () {
// Wait until network is fully switched and refresh tokens before asserting to mitigate flakiness
await driver.assertElementNotPresent('.loading-overlay');
await driver.clickElement('[data-testid="refresh-list-button"]');
+ // intended delay to allow for network requests to complete
+ await driver.delay(1000);
for (let i = 0; i < mockedEndpoints.length; i += 1) {
const requests = await mockedEndpoints[i].getSeenRequests();
assert.equal(
diff --git a/test/e2e/tests/transaction/simple-send.spec.ts b/test/e2e/tests/transaction/simple-send.spec.ts
index 0615a0e21d74..25f2368a9cfc 100644
--- a/test/e2e/tests/transaction/simple-send.spec.ts
+++ b/test/e2e/tests/transaction/simple-send.spec.ts
@@ -4,7 +4,7 @@ import { Ganache } from '../../seeder/ganache';
import { withFixtures, defaultGanacheOptions } from '../../helpers';
import FixtureBuilder from '../../fixture-builder';
import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
-import { sendTransaction } from '../../page-objects/flows/send-transaction.flow';
+import { sendTransactionToAddress } from '../../page-objects/flows/send-transaction.flow';
import HomePage from '../../page-objects/pages/homepage';
describe('Simple send eth', function (this: Suite) {
@@ -23,13 +23,13 @@ describe('Simple send eth', function (this: Suite) {
ganacheServer?: Ganache;
}) => {
await loginWithBalanceValidation(driver, ganacheServer);
- await sendTransaction(
+ await sendTransactionToAddress({
driver,
- '0x985c30949c92df7a0bd42e0f3e3d539ece98db24',
- '1',
- '0.000042',
- '1.000042',
- );
+ recipientAddress: '0x985c30949c92df7a0bd42e0f3e3d539ece98db24',
+ amount: '1',
+ gasFee: '0.000042',
+ totalFee: '1.000042',
+ });
const homePage = new HomePage(driver);
await homePage.check_pageIsLoaded();
await homePage.check_confirmedTxNumberDisplayedInActivity();
From 4ba7d897635e9f0f1c3472dc42cf1c0e8fd58a2a Mon Sep 17 00:00:00 2001
From: Pedro Figueiredo
Date: Fri, 8 Nov 2024 16:21:20 +0000
Subject: [PATCH 18/18] fix: Return to send page with different asset types
(#28382)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This bug was reproducible by opening a new wallet initiated send
confirmation with a Native token ("simple send") from the extension full
screen view, and then triggering a dApp initiated confirmation, and
trying to return back to the send flow stepper.
The bug was provoked due to having the `editExistingTransaction` action
dispatched on back button click hardcoded for asset of type token. The
fix involves dynamically determining the asset type.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28382?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/28316
## **Manual testing steps**
See above or check video on the bug report ticket.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../header/wallet-initiated-header.tsx | 26 +++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx
index ffc8e7549faf..523e0206167d 100644
--- a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx
+++ b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx
@@ -1,4 +1,7 @@
-import { TransactionMeta } from '@metamask/transaction-controller';
+import {
+ TransactionMeta,
+ TransactionType,
+} from '@metamask/transaction-controller';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
@@ -38,7 +41,26 @@ export const WalletInitiatedHeader = () => {
const handleBackButtonClick = useCallback(async () => {
const { id } = currentConfirmation;
- await dispatch(editExistingTransaction(AssetType.token, id.toString()));
+ const isNativeSend =
+ currentConfirmation.type === TransactionType.simpleSend;
+ const isERC20TokenSend =
+ currentConfirmation.type === TransactionType.tokenMethodTransfer;
+ const isNFTTokenSend =
+ currentConfirmation.type === TransactionType.tokenMethodTransferFrom ||
+ currentConfirmation.type === TransactionType.tokenMethodSafeTransferFrom;
+
+ let assetType: AssetType;
+ if (isNativeSend) {
+ assetType = AssetType.native;
+ } else if (isERC20TokenSend) {
+ assetType = AssetType.token;
+ } else if (isNFTTokenSend) {
+ assetType = AssetType.NFT;
+ } else {
+ assetType = AssetType.unknown;
+ }
+
+ await dispatch(editExistingTransaction(assetType, id.toString()));
dispatch(clearConfirmTransaction());
dispatch(showSendTokenPage());