From 13c758a831ad1dbbdd4a3cacfd6dd999c45f9bba Mon Sep 17 00:00:00 2001
From: MetaMask Bot
Date: Wed, 9 Oct 2024 17:09:23 +0000
Subject: [PATCH 01/51] Version v12.4.1
---
CHANGELOG.md | 5 ++++-
package.json | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5fbcff7a94f..7cae12b166c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [12.4.1]
+
## [12.4.0]
### Added
- Added a receive button to the home screen, allowing users to easily get their address or QR-code for receiving cryptocurrency ([#26148](https://github.com/MetaMask/metamask-extension/pull/26148))
@@ -5139,7 +5141,8 @@ Update styles and spacing on the critical error page ([#20350](https://github.c
- Added the ability to restore accounts from seed words.
-[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...HEAD
+[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...HEAD
+[12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1
[12.4.0]: https://github.com/MetaMask/metamask-extension/compare/v12.3.1...v12.4.0
[12.3.1]: https://github.com/MetaMask/metamask-extension/compare/v12.3.0...v12.3.1
[12.3.0]: https://github.com/MetaMask/metamask-extension/compare/v12.2.4...v12.3.0
diff --git a/package.json b/package.json
index d8fece95d0c0..19c9a6c0400d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metamask-crx",
- "version": "12.4.0",
+ "version": "12.4.1",
"private": true,
"repository": {
"type": "git",
From 3d096c2d673aa8756a27d14887f2ea748a2ea780 Mon Sep 17 00:00:00 2001
From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com>
Date: Thu, 10 Oct 2024 06:56:47 -0400
Subject: [PATCH 02/51] fix: remove old phishfort list from clients (#27743)
(#27746)
Cherry-pick #27743 for v12.4.1
Co-authored-by: Mark Stacey
---
app/scripts/migrations/126.1.test.ts | 142 +++++++++++++++++++++++++++
app/scripts/migrations/126.1.ts | 54 ++++++++++
app/scripts/migrations/index.js | 1 +
3 files changed, 197 insertions(+)
create mode 100644 app/scripts/migrations/126.1.test.ts
create mode 100644 app/scripts/migrations/126.1.ts
diff --git a/app/scripts/migrations/126.1.test.ts b/app/scripts/migrations/126.1.test.ts
new file mode 100644
index 000000000000..0d21a675ebcc
--- /dev/null
+++ b/app/scripts/migrations/126.1.test.ts
@@ -0,0 +1,142 @@
+import { migrate, version } from './126.1';
+
+const oldVersion = 126.1;
+
+const mockPhishingListMetaMask = {
+ allowlist: [],
+ blocklist: ['malicious1.com'],
+ c2DomainBlocklist: ['malicious2.com'],
+ fuzzylist: [],
+ tolerance: 0,
+ version: 1,
+ lastUpdated: Date.now(),
+ name: 'MetaMask',
+};
+
+const mockPhishingListPhishfort = {
+ allowlist: [],
+ blocklist: ['phishfort1.com'],
+ c2DomainBlocklist: ['phishfort2.com'],
+ fuzzylist: [],
+ tolerance: 0,
+ version: 1,
+ lastUpdated: Date.now(),
+ name: 'Phishfort',
+};
+
+describe(`migration #${version}`, () => {
+ it('updates the version metadata', async () => {
+ const oldStorage = {
+ meta: { version: oldVersion },
+ data: {},
+ };
+
+ const newStorage = await migrate(oldStorage);
+
+ expect(newStorage.meta).toStrictEqual({ version });
+ });
+
+ it('keeps only the MetaMask phishing list in PhishingControllerState', async () => {
+ const oldState = {
+ PhishingController: {
+ phishingLists: [mockPhishingListMetaMask, mockPhishingListPhishfort],
+ whitelist: [],
+ hotlistLastFetched: 0,
+ stalelistLastFetched: 0,
+ c2DomainBlocklistLastFetched: 0,
+ },
+ };
+
+ const transformedState = await migrate({
+ meta: { version: oldVersion },
+ data: oldState,
+ });
+
+ const updatedPhishingController = transformedState.data
+ .PhishingController as Record;
+
+ expect(updatedPhishingController.phishingLists).toStrictEqual([
+ mockPhishingListMetaMask,
+ ]);
+ });
+
+ it('removes all phishing lists if MetaMask is not present', async () => {
+ const oldState = {
+ PhishingController: {
+ phishingLists: [mockPhishingListPhishfort],
+ whitelist: [],
+ hotlistLastFetched: 0,
+ stalelistLastFetched: 0,
+ c2DomainBlocklistLastFetched: 0,
+ },
+ };
+
+ const transformedState = await migrate({
+ meta: { version: oldVersion },
+ data: oldState,
+ });
+
+ const updatedPhishingController = transformedState.data
+ .PhishingController as Record;
+
+ expect(updatedPhishingController.phishingLists).toStrictEqual([]);
+ });
+
+ it('does nothing if PhishingControllerState is empty', async () => {
+ const oldState = {
+ PhishingController: {
+ phishingLists: [],
+ whitelist: [],
+ hotlistLastFetched: 0,
+ stalelistLastFetched: 0,
+ c2DomainBlocklistLastFetched: 0,
+ },
+ };
+
+ const transformedState = await migrate({
+ meta: { version: oldVersion },
+ data: oldState,
+ });
+
+ const updatedPhishingController = transformedState.data
+ .PhishingController as Record;
+
+ expect(updatedPhishingController.phishingLists).toStrictEqual([]);
+ });
+
+ it('does nothing if PhishingController is not in the state', async () => {
+ const oldState = {
+ NetworkController: {
+ providerConfig: {
+ chainId: '0x1',
+ },
+ },
+ };
+
+ const transformedState = await migrate({
+ meta: { version: oldVersion },
+ data: oldState,
+ });
+
+ expect(transformedState.data).toStrictEqual(oldState);
+ });
+
+ it('does nothing if phishingLists is not an array (null)', async () => {
+ const oldState: Record = {
+ PhishingController: {
+ phishingLists: null,
+ whitelist: [],
+ hotlistLastFetched: 0,
+ stalelistLastFetched: 0,
+ c2DomainBlocklistLastFetched: 0,
+ },
+ };
+
+ const transformedState = await migrate({
+ meta: { version: oldVersion },
+ data: oldState,
+ });
+
+ expect(transformedState.data).toStrictEqual(oldState);
+ });
+});
diff --git a/app/scripts/migrations/126.1.ts b/app/scripts/migrations/126.1.ts
new file mode 100644
index 000000000000..81e609e672f1
--- /dev/null
+++ b/app/scripts/migrations/126.1.ts
@@ -0,0 +1,54 @@
+import { hasProperty, isObject } from '@metamask/utils';
+import { cloneDeep } from 'lodash';
+
+type VersionedData = {
+ meta: { version: number };
+ data: Record;
+};
+
+export const version = 126.1;
+
+/**
+ * This migration removes `providerConfig` from the network controller state.
+ *
+ * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist.
+ * @param originalVersionedData.meta - State metadata.
+ * @param originalVersionedData.meta.version - The current state version.
+ * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller.
+ * @returns Updated versioned MetaMask extension state.
+ */
+export async function migrate(
+ originalVersionedData: VersionedData,
+): Promise {
+ const versionedData = cloneDeep(originalVersionedData);
+ versionedData.meta.version = version;
+ transformState(versionedData.data);
+ return versionedData;
+}
+
+function transformState(
+ state: Record,
+): Record {
+ if (
+ hasProperty(state, 'PhishingController') &&
+ isObject(state.PhishingController) &&
+ hasProperty(state.PhishingController, 'phishingLists')
+ ) {
+ const phishingController = state.PhishingController;
+
+ if (!Array.isArray(phishingController.phishingLists)) {
+ console.error(
+ `Migration ${version}: Invalid PhishingController.phishingLists state`,
+ );
+ return state;
+ }
+
+ phishingController.phishingLists = phishingController.phishingLists.filter(
+ (list) => list.name === 'MetaMask',
+ );
+
+ state.PhishingController = phishingController;
+ }
+
+ return state;
+}
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index e5cfb6218019..119dfd79ede1 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -146,6 +146,7 @@ const migrations = [
require('./125'),
require('./125.1'),
require('./126'),
+ require('./126.1'),
];
export default migrations;
From 3ebc8a73f9aed0bef56f1230027b104a92a61bae Mon Sep 17 00:00:00 2001
From: Jack Clancy
Date: Thu, 10 Oct 2024 14:06:50 +0100
Subject: [PATCH 03/51] fix: cherry pick swaps undefined object access crash
hotfix into v12.4.1 RC (#27757)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Cherry picks swaps undefined object access crash hotfix intov12.4.1RC.
See [here](https://github.com/MetaMask/metamask-extension/pull/27708)
for more info
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27757?quickstart=1)
## **Related issues**
https://consensyssoftware.atlassian.net/jira/software/projects/MMS/boards/447/backlog?assignee=5ae37c7e42b8a62c4e15d92a&selectedIssue=MMS-1569
## **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: Mark Stacey
---
CHANGELOG.md | 2 ++
ui/pages/swaps/prepare-swap-page/prepare-swap-page.js | 4 +++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cae12b166c2..d69b3b7c6fb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [12.4.1]
+### Fixed
+- Fix crash on swaps review page ([27708](https://github.com/MetaMask/metamask-extension/pull/27708))
## [12.4.0]
### Added
diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
index 98bb6933d0c3..45120d9f6a6b 100644
--- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
+++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
@@ -52,6 +52,7 @@ import {
getTransactionSettingsOpened,
setTransactionSettingsOpened,
getLatestAddedTokenTo,
+ getUsedQuote,
} from '../../../ducks/swaps/swaps';
import {
getSwapsDefaultToken,
@@ -190,9 +191,10 @@ export default function PrepareSwapPage({
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual);
const tokenList = useSelector(getTokenList, isEqual);
const quotes = useSelector(getQuotes, isEqual);
+ const usedQuote = useSelector(getUsedQuote, isEqual);
const latestAddedTokenTo = useSelector(getLatestAddedTokenTo, isEqual);
const numberOfQuotes = Object.keys(quotes).length;
- const areQuotesPresent = numberOfQuotes > 0;
+ const areQuotesPresent = numberOfQuotes > 0 && usedQuote;
const swapsErrorKey = useSelector(getSwapsErrorKey);
const aggregatorMetadata = useSelector(getAggregatorMetadata, shallowEqual);
const transactionSettingsOpened = useSelector(
From ac46289e78a401af2dc2bcdd7a387cfdd1549e6b Mon Sep 17 00:00:00 2001
From: Dan J Miller
Date: Thu, 10 Oct 2024 16:03:52 -0230
Subject: [PATCH 04/51] Update CHANGELOG.md for v12.4.1 (#27775)
---
CHANGELOG.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d69b3b7c6fb1..f7b07834e387 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [12.4.1]
### Fixed
-- Fix crash on swaps review page ([27708](https://github.com/MetaMask/metamask-extension/pull/27708))
+- Fix crash on swaps review page ([#27708](https://github.com/MetaMask/metamask-extension/pull/27708))
+- Fix bug that could prevent the phishing detection feature from having the most up to date info on which web pages to block ([#27743](https://github.com/MetaMask/metamask-extension/pull/27743))
## [12.4.0]
### Added
From 71de55b8a3c97f17ea92653b1607e2e78f976967 Mon Sep 17 00:00:00 2001
From: seaona <54408225+seaona@users.noreply.github.com>
Date: Wed, 16 Oct 2024 14:32:07 +0200
Subject: [PATCH 05/51] fix: flaky test `ERC1155 NFTs testdapp interaction
should batch transfers ERC1155 token` (#27897)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Same problem as
https://github.com/MetaMask/metamask-extension/pull/27889.
We are looking for transactions by its text in the activity tab, but the
transaction element updates its state, from pending to confirm, meaning
that it can become stale when we do the assertion after.
```
await driver.waitForSelector({
css: '[data-testid="activity-list-item-action"]',
text: 'Deposit',
});
assert.equal(await transactionItem.isDisplayed(), true);
```
![Screenshot from 2024-10-16
12-05-08](https://github.com/user-attachments/assets/df2066a1-b692-4e5f-9961-6e2e4626aa00)
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27897?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27896
## **Manual testing steps**
1. Check ci
## **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.
---
.../tokens/nft/erc1155-interaction.spec.js | 150 ++++++++----------
1 file changed, 66 insertions(+), 84 deletions(-)
diff --git a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js
index 31425140c7f4..1fed3946dea9 100644
--- a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js
+++ b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js
@@ -38,33 +38,27 @@ describe('ERC1155 NFTs testdapp interaction', function () {
await driver.clickElement('#batchMintButton');
// Notification
- const windowHandles = await driver.waitUntilXWindowHandles(3);
- const [extension] = windowHandles;
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.Dialog,
- windowHandles,
- );
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
// Confirm Mint
await driver.waitForSelector({
css: '.confirm-page-container-summary__action__name',
text: 'Deposit',
});
- await driver.clickElement({ text: 'Confirm', tag: 'button' });
- await driver.waitUntilXWindowHandles(2);
- await driver.switchToWindow(extension);
+ await driver.clickElementAndWaitForWindowToClose({
+ text: 'Confirm',
+ tag: 'button',
+ });
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
await driver.clickElement(
'[data-testid="account-overview__activity-tab"]',
);
- const transactionItem = await driver.waitForSelector({
+ await driver.waitForSelector({
css: '[data-testid="activity-list-item-action"]',
text: 'Deposit',
});
- assert.equal(
- await transactionItem.isDisplayed(),
- true,
- `transaction item should be displayed in activity tab`,
- );
},
);
});
@@ -90,33 +84,27 @@ describe('ERC1155 NFTs testdapp interaction', function () {
await driver.fill('#batchTransferTokenAmounts', '1, 1, 1000000000000');
await driver.clickElement('#batchTransferFromButton');
- const windowHandles = await driver.waitUntilXWindowHandles(3);
- const [extension] = windowHandles;
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.Dialog,
- windowHandles,
- );
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
// Confirm Transfer
await driver.waitForSelector({
css: '.confirm-page-container-summary__action__name',
text: 'Deposit',
});
- await driver.clickElement({ text: 'Confirm', tag: 'button' });
- await driver.waitUntilXWindowHandles(2);
- await driver.switchToWindow(extension);
+ await driver.clickElementAndWaitForWindowToClose({
+ text: 'Confirm',
+ tag: 'button',
+ });
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
await driver.clickElement(
'[data-testid="account-overview__activity-tab"]',
);
- const transactionItem = await driver.waitForSelector({
+ await driver.waitForSelector({
css: '[data-testid="activity-list-item-action"]',
text: 'Deposit',
});
- assert.equal(
- await transactionItem.isDisplayed(),
- true,
- `transaction item should be displayed in activity tab`,
- );
},
);
});
@@ -147,26 +135,20 @@ describe('ERC1155 NFTs testdapp interaction', function () {
await driver.clickElement('#setApprovalForAllERC1155Button');
// Wait for notification popup and check the displayed message
- let windowHandles = await driver.waitUntilXWindowHandles(3);
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.Dialog,
- windowHandles,
- );
- const displayedMessageTitle = await driver.findElement(
- '[data-testid="confirm-approve-title"]',
- );
- assert.equal(
- await displayedMessageTitle.getText(),
- expectedMessageTitle,
- );
- const displayedUrl = await driver.findElement(
- '.confirm-approve-content h6',
- );
- assert.equal(await displayedUrl.getText(), DAPP_URL);
- const displayedDescription = await driver.findElement(
- '.confirm-approve-content__description',
- );
- assert.equal(await displayedDescription.getText(), expectedDescription);
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await driver.waitForSelector({
+ css: '[data-testid="confirm-approve-title"]',
+ text: expectedMessageTitle,
+ });
+ await driver.waitForSelector({
+ css: '.confirm-approve-content h6',
+ text: DAPP_URL,
+ });
+
+ await driver.waitForSelector({
+ css: '.confirm-approve-content__description',
+ text: expectedDescription,
+ });
// Check displayed transaction details
await driver.clickElement({
@@ -185,27 +167,29 @@ describe('ERC1155 NFTs testdapp interaction', function () {
'.set-approval-for-all-warning__content__header',
);
assert.equal(await displayedWarning.getText(), expectedWarningMessage);
- await driver.clickElement({ text: 'Approve', tag: 'button' });
- windowHandles = await driver.waitUntilXWindowHandles(2);
+ await driver.clickElementAndWaitForWindowToClose({
+ text: 'Approve',
+ tag: 'button',
+ });
// Switch to extension and check set approval for all transaction is displayed in activity tab
- await driver.switchToWindowWithTitle('MetaMask', windowHandles);
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
await driver.clickElement(
'[data-testid="account-overview__activity-tab"]',
);
- const setApprovalItem = await driver.findElement({
+ await driver.waitForSelector({
css: '.transaction-list__completed-transactions',
text: 'Approve Token with no spend limit',
});
- assert.equal(await setApprovalItem.isDisplayed(), true);
// Switch back to the dapp and verify that set approval for all action completed message is displayed
- await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
- const setApprovalStatus = await driver.findElement({
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await driver.waitForSelector({
css: '#erc1155Status',
text: 'Set Approval For All completed',
});
- assert.equal(await setApprovalStatus.isDisplayed(), true);
},
);
});
@@ -235,27 +219,22 @@ describe('ERC1155 NFTs testdapp interaction', function () {
await driver.clickElement('#revokeERC1155Button');
// Wait for notification popup and check the displayed message
- let windowHandles = await driver.waitUntilXWindowHandles(3);
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.Dialog,
- windowHandles,
- );
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- const displayedMessageTitle = await driver.findElement(
- '.confirm-approve-content__title',
- );
- assert.equal(
- await displayedMessageTitle.getText(),
- expectedMessageTitle,
- );
- const displayedUrl = await driver.findElement(
- '.confirm-approve-content h6',
- );
- assert.equal(await displayedUrl.getText(), DAPP_URL);
- const displayedDescription = await driver.findElement(
- '.confirm-approve-content__description',
- );
- assert.equal(await displayedDescription.getText(), expectedDescription);
+ await driver.waitForSelector({
+ css: '.confirm-approve-content__title',
+ text: expectedMessageTitle,
+ });
+
+ await driver.waitForSelector({
+ css: '.confirm-approve-content h6',
+ text: DAPP_URL,
+ });
+
+ await driver.waitForSelector({
+ css: '.confirm-approve-content__description',
+ text: expectedDescription,
+ });
// Check displayed transaction details
await driver.clickElement({
@@ -269,22 +248,25 @@ describe('ERC1155 NFTs testdapp interaction', function () {
assert.equal(await params.getText(), 'Parameters: false');
// Click on extension popup to confirm revoke approval for all
- await driver.clickElement('[data-testid="page-container-footer-next"]');
- windowHandles = await driver.waitUntilXWindowHandles(2);
+ await driver.clickElementAndWaitForWindowToClose(
+ '[data-testid="page-container-footer-next"]',
+ );
// Switch to extension and check revoke approval transaction is displayed in activity tab
- await driver.switchToWindowWithTitle('MetaMask', windowHandles);
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
+
await driver.clickElement(
'[data-testid="account-overview__activity-tab"]',
);
- const revokeApprovalItem = await driver.findElement({
+ await driver.waitForSelector({
css: '.transaction-list__completed-transactions',
text: 'Approve Token with no spend limit',
});
- assert.equal(await revokeApprovalItem.isDisplayed(), true);
// Switch back to the dapp and verify that revoke approval for all message is displayed
- await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
const revokeApprovalStatus = await driver.findElement({
css: '#erc1155Status',
text: 'Revoke completed',
From 130bdbf5d02702ef4227644aa7827715847e00fb Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 16 Oct 2024 08:43:59 -0400
Subject: [PATCH 06/51] test: [POM] Migrate signature with snap account e2e
tests to page object modal (#27829)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR migrates the snap account signature e2e tests to the Page Object
Model (POM) pattern, improving test stability and maintainability.
Changes include:
- Migrate test `snap-account-signatures.spec.ts` to POM
- Created all signature related functions in TestDapp class
- Avoid several delays in the original function implementation
- Reduced flakiness
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1)
## **Related issues**
Fixes: #27835
## **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: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Chloe Gao
Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com>
Co-authored-by: seaona <54408225+seaona@users.noreply.github.com>
---
.../accounts/snap-account-signatures.spec.ts | 51 ---
test/e2e/page-objects/flows/sign.flow.ts | 169 +++++++++
.../pages/experimental-settings.ts | 10 +-
.../pages/snap-simple-keyring-page.ts | 42 ++-
test/e2e/page-objects/pages/test-dapp.ts | 335 +++++++++++++++++-
.../account/snap-account-settings.spec.ts | 2 +-
.../account/snap-account-signatures.spec.ts | 100 ++++++
...55-revoke-set-approval-for-all-redesign.ts | 2 +-
...1155-set-approval-for-all-redesign.spec.ts | 2 +-
...21-revoke-set-approval-for-all-redesign.ts | 2 +-
...c721-set-approval-for-all-redesign.spec.ts | 2 +-
11 files changed, 624 insertions(+), 93 deletions(-)
delete mode 100644 test/e2e/accounts/snap-account-signatures.spec.ts
create mode 100644 test/e2e/page-objects/flows/sign.flow.ts
create mode 100644 test/e2e/tests/account/snap-account-signatures.spec.ts
diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts
deleted file mode 100644
index 536d8168b1a3..000000000000
--- a/test/e2e/accounts/snap-account-signatures.spec.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Suite } from 'mocha';
-import {
- tempToggleSettingRedesignedConfirmations,
- withFixtures,
-} from '../helpers';
-import { Driver } from '../webdriver/driver';
-import {
- accountSnapFixtures,
- installSnapSimpleKeyring,
- makeNewAccountAndSwitch,
- signData,
-} from './common';
-
-describe('Snap Account Signatures', function (this: Suite) {
- this.timeout(120000); // This test is very long, so we need an unusually high timeout
-
- // Run sync, async approve, and async reject flows
- // (in Jest we could do this with test.each, but that does not exist here)
- ['sync', 'approve', 'reject'].forEach((flowType) => {
- // generate title of the test from flowType
- const title = `can sign with ${flowType} flow`;
-
- it(title, async () => {
- await withFixtures(
- accountSnapFixtures(title),
- async ({ driver }: { driver: Driver }) => {
- const isAsyncFlow = flowType !== 'sync';
-
- await installSnapSimpleKeyring(driver, isAsyncFlow);
-
- const newPublicKey = await makeNewAccountAndSwitch(driver);
-
- await tempToggleSettingRedesignedConfirmations(driver);
-
- // Run all 5 signature types
- const locatorIDs = [
- '#personalSign',
- '#signTypedData',
- '#signTypedDataV3',
- '#signTypedDataV4',
- '#signPermit',
- ];
-
- for (const locatorID of locatorIDs) {
- await signData(driver, locatorID, newPublicKey, flowType);
- }
- },
- );
- });
- });
-});
diff --git a/test/e2e/page-objects/flows/sign.flow.ts b/test/e2e/page-objects/flows/sign.flow.ts
new file mode 100644
index 000000000000..c7d03bb4f96e
--- /dev/null
+++ b/test/e2e/page-objects/flows/sign.flow.ts
@@ -0,0 +1,169 @@
+import { Driver } from '../../webdriver/driver';
+import { WINDOW_TITLES } from '../../helpers';
+import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page';
+import TestDapp from '../pages/test-dapp';
+
+/**
+ * This function initiates the steps for a personal sign with snap account on test dapp.
+ *
+ * @param driver - The webdriver instance.
+ * @param publicAddress - The public address of the snap account.
+ * @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.
+ */
+export const personalSignWithSnapAccount = async (
+ driver: Driver,
+ publicAddress: string,
+ isSyncFlow: boolean = true,
+ approveTransaction: boolean = true,
+): Promise => {
+ const testDapp = new TestDapp(driver);
+ await testDapp.check_pageIsLoaded();
+ await testDapp.personalSign();
+ if (!isSyncFlow) {
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction(
+ approveTransaction,
+ true,
+ );
+ }
+ if ((!isSyncFlow && approveTransaction) || isSyncFlow) {
+ await testDapp.check_successPersonalSign(publicAddress);
+ } else {
+ await testDapp.check_failedPersonalSign(
+ 'Error: Request rejected by user or snap.',
+ );
+ }
+};
+
+/**
+ * This function initiates the steps for a signTypedData with snap account on test dapp.
+ *
+ * @param driver - The webdriver instance.
+ * @param publicAddress - The public address of the snap account.
+ * @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.
+ */
+export const signTypedDataWithSnapAccount = async (
+ driver: Driver,
+ publicAddress: string,
+ isSyncFlow: boolean = true,
+ approveTransaction: boolean = true,
+): Promise => {
+ const testDapp = new TestDapp(driver);
+ await testDapp.check_pageIsLoaded();
+ await testDapp.signTypedData();
+ if (!isSyncFlow) {
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction(
+ approveTransaction,
+ true,
+ );
+ }
+ if ((!isSyncFlow && approveTransaction) || isSyncFlow) {
+ await testDapp.check_successSignTypedData(publicAddress);
+ } else {
+ await testDapp.check_failedSignTypedData(
+ 'Error: Request rejected by user or snap.',
+ );
+ }
+};
+
+/**
+ * This function initiates the steps for a signTypedDataV3 with snap account on test dapp.
+ *
+ * @param driver - The webdriver instance.
+ * @param publicAddress - The public address of the snap account.
+ * @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.
+ */
+export const signTypedDataV3WithSnapAccount = async (
+ driver: Driver,
+ publicAddress: string,
+ isSyncFlow: boolean = true,
+ approveTransaction: boolean = true,
+): Promise => {
+ const testDapp = new TestDapp(driver);
+ await testDapp.check_pageIsLoaded();
+ await testDapp.signTypedDataV3();
+ if (!isSyncFlow) {
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction(
+ approveTransaction,
+ true,
+ );
+ }
+ if ((!isSyncFlow && approveTransaction) || isSyncFlow) {
+ await testDapp.check_successSignTypedDataV3(publicAddress);
+ } else {
+ await testDapp.check_failedSignTypedDataV3(
+ 'Error: Request rejected by user or snap.',
+ );
+ }
+};
+
+/**
+ * This function initiates the steps for a signTypedDataV4 with snap account on test dapp.
+ *
+ * @param driver - The webdriver instance.
+ * @param publicAddress - The public address of the snap account.
+ * @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.
+ */
+export const signTypedDataV4WithSnapAccount = async (
+ driver: Driver,
+ publicAddress: string,
+ isSyncFlow: boolean = true,
+ approveTransaction: boolean = true,
+): Promise => {
+ const testDapp = new TestDapp(driver);
+ await testDapp.check_pageIsLoaded();
+ await testDapp.signTypedDataV4();
+ if (!isSyncFlow) {
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction(
+ approveTransaction,
+ true,
+ );
+ }
+ if ((!isSyncFlow && approveTransaction) || isSyncFlow) {
+ await testDapp.check_successSignTypedDataV4(publicAddress);
+ } else {
+ await testDapp.check_failedSignTypedDataV4(
+ 'Error: Request rejected by user or snap.',
+ );
+ }
+};
+
+/**
+ * This function initiates the steps for a signPermit with snap account on test dapp.
+ *
+ * @param driver - The webdriver instance.
+ * @param publicAddress - The public address of the snap account.
+ * @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.
+ */
+export const signPermitWithSnapAccount = async (
+ driver: Driver,
+ publicAddress: string,
+ isSyncFlow: boolean = true,
+ approveTransaction: boolean = true,
+): Promise => {
+ const testDapp = new TestDapp(driver);
+ await testDapp.check_pageIsLoaded();
+ await testDapp.signPermit();
+ if (!isSyncFlow) {
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction(
+ approveTransaction,
+ true,
+ );
+ }
+ if ((!isSyncFlow && approveTransaction) || isSyncFlow) {
+ await testDapp.check_successSignPermit(publicAddress);
+ } else {
+ await testDapp.check_failedSignPermit(
+ 'Error: Request rejected by user or snap.',
+ );
+ }
+};
diff --git a/test/e2e/page-objects/pages/experimental-settings.ts b/test/e2e/page-objects/pages/experimental-settings.ts
index 8c7129b17555..7cd780229acd 100644
--- a/test/e2e/page-objects/pages/experimental-settings.ts
+++ b/test/e2e/page-objects/pages/experimental-settings.ts
@@ -9,9 +9,12 @@ class ExperimentalSettings {
private readonly experimentalPageTitle: object = {
text: 'Experimental',
- css: '.h4',
+ tag: 'h4',
};
+ private readonly redesignedSignatureToggle: string =
+ '[data-testid="toggle-redesigned-confirmations-container"]';
+
constructor(driver: Driver) {
this.driver = driver;
}
@@ -33,6 +36,11 @@ class ExperimentalSettings {
console.log('Toggle Add Account Snap on experimental setting page');
await this.driver.clickElement(this.addAccountSnapToggle);
}
+
+ async toggleRedesignedSignature(): Promise {
+ console.log('Toggle Redesigned Signature on experimental setting page');
+ await this.driver.clickElement(this.redesignedSignatureToggle);
+ }
}
export default ExperimentalSettings;
diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts
index 7f7a97d7d861..c75adb06da3a 100644
--- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts
+++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts
@@ -9,11 +9,6 @@ class SnapSimpleKeyringPage {
tag: 'h3',
};
- private readonly accountSupportedMethods = {
- text: 'Account Supported Methods',
- tag: 'p',
- };
-
private readonly addtoMetamaskMessage = {
text: 'Add to MetaMask',
tag: 'h3',
@@ -104,6 +99,11 @@ class SnapSimpleKeyringPage {
tag: 'div',
};
+ private readonly newAccountMessage = {
+ text: '"address":',
+ tag: 'div',
+ };
+
private readonly pageTitle = {
text: 'Snap Simple Keyring',
tag: 'p',
@@ -161,16 +161,25 @@ class SnapSimpleKeyringPage {
* Approves or rejects a transaction from a snap account on Snap Simple Keyring page.
*
* @param approveTransaction - Indicates if the transaction should be approved. Defaults to true.
+ * @param isSignatureRequest - Indicates if the request is a signature request. Defaults to false.
*/
async approveRejectSnapAccountTransaction(
approveTransaction: boolean = true,
+ isSignatureRequest: boolean = false,
): Promise {
console.log(
'Approve/Reject snap account transaction on Snap Simple Keyring page',
);
- await this.driver.clickElementAndWaitToDisappear(
- this.confirmationSubmitButton,
- );
+ if (isSignatureRequest) {
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmationSubmitButton,
+ );
+ } else {
+ // For send eth requests, the origin screen is not closed automatically, so we cannot call clickElementAndWaitForWindowToClose here.
+ await this.driver.clickElementAndWaitToDisappear(
+ this.confirmationSubmitButton,
+ );
+ }
await this.driver.switchToWindowWithTitle(
WINDOW_TITLES.SnapSimpleKeyringDapp,
);
@@ -242,7 +251,7 @@ class SnapSimpleKeyringPage {
await this.driver.switchToWindowWithTitle(
WINDOW_TITLES.SnapSimpleKeyringDapp,
);
- await this.check_accountSupportedMethodsDisplayed();
+ await this.driver.waitForSelector(this.newAccountMessage);
}
async confirmCreateSnapOnConfirmationScreen(): Promise {
@@ -255,15 +264,21 @@ class SnapSimpleKeyringPage {
*
* @param accountName - Optional: name for the snap account. Defaults to "SSK Account".
* @param isFirstAccount - Indicates if this is the first snap account being created. Defaults to true.
+ * @returns the public key of the new created account
*/
async createNewAccount(
accountName: string = 'SSK Account',
isFirstAccount: boolean = true,
- ): Promise {
+ ): Promise {
console.log('Create new account on Snap Simple Keyring page');
await this.openCreateSnapAccountConfirmationScreen(isFirstAccount);
await this.confirmCreateSnapOnConfirmationScreen();
await this.confirmAddAccountDialog(accountName);
+ const newAccountJSONMessage = await (
+ await this.driver.waitForSelector(this.newAccountMessage)
+ ).getText();
+ const newPublicKey = JSON.parse(newAccountJSONMessage).address;
+ return newPublicKey;
}
/**
@@ -331,13 +346,6 @@ class SnapSimpleKeyringPage {
await this.driver.clickElement(this.useSyncApprovalToggle);
}
- async check_accountSupportedMethodsDisplayed(): Promise {
- console.log(
- 'Check new created account supported methods are displayed on simple keyring snap page',
- );
- await this.driver.waitForSelector(this.accountSupportedMethods);
- }
-
async check_errorRequestMessageDisplayed(): Promise {
console.log(
'Check error request message is displayed on snap simple keyring page',
diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts
index 89ee6bc9cbd3..ffb1f9033bdb 100644
--- a/test/e2e/page-objects/pages/test-dapp.ts
+++ b/test/e2e/page-objects/pages/test-dapp.ts
@@ -1,5 +1,5 @@
import { Driver } from '../../webdriver/driver';
-import { RawLocator } from '../common';
+import { WINDOW_TITLES } from '../../helpers';
const DAPP_HOST_ADDRESS = '127.0.0.1:8080';
const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`;
@@ -7,40 +7,120 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`;
class TestDapp {
private driver: Driver;
- private erc721SetApprovalForAllButton: RawLocator;
+ private readonly confirmDialogScrollButton =
+ '[data-testid="signature-request-scroll-button"]';
- private erc1155SetApprovalForAllButton: RawLocator;
+ private readonly confirmSignatureButton =
+ '[data-testid="page-container-footer-next"]';
- private erc721RevokeSetApprovalForAllButton: RawLocator;
+ private readonly erc1155RevokeSetApprovalForAllButton =
+ '#revokeERC1155Button';
- private erc1155RevokeSetApprovalForAllButton: RawLocator;
+ private readonly erc1155SetApprovalForAllButton =
+ '#setApprovalForAllERC1155Button';
+
+ private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton';
+
+ private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton';
+
+ private readonly mmlogo = '#mm-logo';
+
+ private readonly personalSignButton = '#personalSign';
+
+ private readonly personalSignResult = '#personalSignVerifyECRecoverResult';
+
+ private readonly personalSignSignatureRequestMessage = {
+ text: 'personal_sign',
+ tag: 'div',
+ };
+
+ private readonly personalSignVerifyButton = '#personalSignVerify';
+
+ private readonly signPermitButton = '#signPermit';
+
+ private readonly signPermitResult = '#signPermitResult';
+
+ private readonly signPermitSignatureRequestMessage = {
+ text: 'Permit',
+ tag: 'p',
+ };
+
+ private readonly signPermitVerifyButton = '#signPermitVerify';
+
+ private readonly signPermitVerifyResult = '#signPermitVerifyResult';
+
+ private readonly signTypedDataButton = '#signTypedData';
+
+ private readonly signTypedDataResult = '#signTypedDataResult';
+
+ private readonly signTypedDataSignatureRequestMessage = {
+ text: 'Hi, Alice!',
+ tag: 'div',
+ };
+
+ private readonly signTypedDataV3Button = '#signTypedDataV3';
+
+ private readonly signTypedDataV3Result = '#signTypedDataV3Result';
+
+ private readonly signTypedDataV3V4SignatureRequestMessage = {
+ text: 'Hello, Bob!',
+ tag: 'div',
+ };
+
+ private readonly signTypedDataV3VerifyButton = '#signTypedDataV3Verify';
+
+ private readonly signTypedDataV3VerifyResult = '#signTypedDataV3VerifyResult';
+
+ private readonly signTypedDataV4Button = '#signTypedDataV4';
+
+ private readonly signTypedDataV4Result = '#signTypedDataV4Result';
+
+ private readonly signTypedDataV4VerifyButton = '#signTypedDataV4Verify';
+
+ private readonly signTypedDataV4VerifyResult = '#signTypedDataV4VerifyResult';
+
+ private readonly signTypedDataVerifyButton = '#signTypedDataVerify';
+
+ private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult';
constructor(driver: Driver) {
this.driver = driver;
+ }
- this.erc721SetApprovalForAllButton = '#setApprovalForAllButton';
- this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button';
- this.erc721RevokeSetApprovalForAllButton = '#revokeButton';
- this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button';
+ async check_pageIsLoaded(): Promise {
+ try {
+ await this.driver.waitForSelector(this.mmlogo);
+ } catch (e) {
+ console.log('Timeout while waiting for Test Dapp page to be loaded', e);
+ throw e;
+ }
+ console.log('Test Dapp page is loaded');
}
- async open({
- contractAddress,
+ /**
+ * Open the test dapp page.
+ *
+ * @param options - The options for opening the test dapp page.
+ * @param options.contractAddress - The contract address to open the dapp with. Defaults to null.
+ * @param options.url - The URL of the dapp. Defaults to DAPP_URL.
+ * @returns A promise that resolves when the new page is opened.
+ */
+ async openTestDappPage({
+ contractAddress = null,
url = DAPP_URL,
}: {
- contractAddress?: string;
+ contractAddress?: string | null;
url?: string;
- }) {
+ } = {}): Promise {
const dappUrl = contractAddress
? `${url}/?contract=${contractAddress}`
: url;
-
- return await this.driver.openNewPage(dappUrl);
+ await this.driver.openNewPage(dappUrl);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async request(method: string, params: any[]) {
- await this.open({
+ await this.openTestDappPage({
url: `${DAPP_URL}/request?method=${method}¶ms=${JSON.stringify(
params,
)}`,
@@ -55,13 +135,230 @@ class TestDapp {
await this.driver.clickElement(this.erc1155SetApprovalForAllButton);
}
- public async clickERC721RevokeSetApprovalForAllButton() {
+ async clickERC721RevokeSetApprovalForAllButton() {
await this.driver.clickElement(this.erc721RevokeSetApprovalForAllButton);
}
- public async clickERC1155RevokeSetApprovalForAllButton() {
+ async clickERC1155RevokeSetApprovalForAllButton() {
await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton);
}
-}
+ /**
+ * Verify the failed personal sign signature.
+ *
+ * @param expectedFailedMessage - The expected failed message.
+ */
+ async check_failedPersonalSign(expectedFailedMessage: string) {
+ console.log('Verify failed personal sign signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.waitForSelector({
+ css: this.personalSignButton,
+ text: expectedFailedMessage,
+ });
+ }
+
+ /**
+ * Verify the failed signPermit signature.
+ *
+ * @param expectedFailedMessage - The expected failed message.
+ */
+ async check_failedSignPermit(expectedFailedMessage: string) {
+ console.log('Verify failed signPermit signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.waitForSelector({
+ css: this.signPermitResult,
+ text: expectedFailedMessage,
+ });
+ }
+
+ /**
+ * Verify the failed signTypedData signature.
+ *
+ * @param expectedFailedMessage - The expected failed message.
+ */
+ async check_failedSignTypedData(expectedFailedMessage: string) {
+ console.log('Verify failed signTypedData signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.waitForSelector({
+ css: this.signTypedDataResult,
+ text: expectedFailedMessage,
+ });
+ }
+
+ /**
+ * Verify the failed signTypedDataV3 signature.
+ *
+ * @param expectedFailedMessage - The expected failed message.
+ */
+ async check_failedSignTypedDataV3(expectedFailedMessage: string) {
+ console.log('Verify failed signTypedDataV3 signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.waitForSelector({
+ css: this.signTypedDataV3Result,
+ text: expectedFailedMessage,
+ });
+ }
+
+ /**
+ * Verify the failed signTypedDataV4 signature.
+ *
+ * @param expectedFailedMessage - The expected failed message.
+ */
+ async check_failedSignTypedDataV4(expectedFailedMessage: string) {
+ console.log('Verify failed signTypedDataV4 signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.waitForSelector({
+ css: this.signTypedDataV4Result,
+ text: expectedFailedMessage,
+ });
+ }
+
+ /**
+ * Verify the successful personal sign signature.
+ *
+ * @param publicKey - The public key to verify the signature with.
+ */
+ async check_successPersonalSign(publicKey: string) {
+ console.log('Verify successful personal sign signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.clickElement(this.personalSignVerifyButton);
+ await this.driver.waitForSelector({
+ css: this.personalSignResult,
+ text: publicKey.toLowerCase(),
+ });
+ }
+
+ /**
+ * Verify the successful signPermit signature.
+ *
+ * @param publicKey - The public key to verify the signature with.
+ */
+ async check_successSignPermit(publicKey: string) {
+ console.log('Verify successful signPermit signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.clickElement(this.signPermitVerifyButton);
+ await this.driver.waitForSelector({
+ css: this.signPermitVerifyResult,
+ text: publicKey.toLowerCase(),
+ });
+ }
+
+ /**
+ * Verify the successful signTypedData signature.
+ *
+ * @param publicKey - The public key to verify the signature with.
+ */
+ async check_successSignTypedData(publicKey: string) {
+ console.log('Verify successful signTypedData signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.clickElement(this.signTypedDataVerifyButton);
+ await this.driver.waitForSelector({
+ css: this.signTypedDataVerifyResult,
+ text: publicKey.toLowerCase(),
+ });
+ }
+
+ /**
+ * Verify the successful signTypedDataV3 signature.
+ *
+ * @param publicKey - The public key to verify the signature with.
+ */
+ async check_successSignTypedDataV3(publicKey: string) {
+ console.log('Verify successful signTypedDataV3 signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.clickElement(this.signTypedDataV3VerifyButton);
+ await this.driver.waitForSelector({
+ css: this.signTypedDataV3VerifyResult,
+ text: publicKey.toLowerCase(),
+ });
+ }
+
+ /**
+ * Verify the successful signTypedDataV4 signature.
+ *
+ * @param publicKey - The public key to verify the signature with.
+ */
+ async check_successSignTypedDataV4(publicKey: string) {
+ console.log('Verify successful signTypedDataV4 signature');
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.clickElement(this.signTypedDataV4VerifyButton);
+ await this.driver.waitForSelector({
+ css: this.signTypedDataV4VerifyResult,
+ text: publicKey.toLowerCase(),
+ });
+ }
+
+ /**
+ * Sign a message with the personal sign method.
+ */
+ async personalSign() {
+ console.log('Sign message with personal sign');
+ await this.driver.clickElement(this.personalSignButton);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(this.personalSignSignatureRequestMessage);
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmSignatureButton,
+ );
+ }
+
+ /**
+ * Sign message with the signPermit method.
+ */
+ async signPermit() {
+ console.log('Sign message with signPermit');
+ await this.driver.clickElement(this.signPermitButton);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(this.signPermitSignatureRequestMessage);
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmSignatureButton,
+ );
+ }
+
+ /**
+ * Sign a message with the signTypedData method.
+ */
+ async signTypedData() {
+ console.log('Sign message with signTypedData');
+ await this.driver.clickElement(this.signTypedDataButton);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(
+ this.signTypedDataSignatureRequestMessage,
+ );
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmSignatureButton,
+ );
+ }
+
+ /**
+ * Sign a message with the signTypedDataV3 method.
+ */
+ async signTypedDataV3() {
+ console.log('Sign message with signTypedDataV3');
+ await this.driver.clickElement(this.signTypedDataV3Button);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(
+ this.signTypedDataV3V4SignatureRequestMessage,
+ );
+ await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200);
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmSignatureButton,
+ );
+ }
+
+ /**
+ * Sign a message with the signTypedDataV4 method.
+ */
+ async signTypedDataV4() {
+ console.log('Sign message with signTypedDataV4');
+ await this.driver.clickElement(this.signTypedDataV4Button);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(
+ this.signTypedDataV3V4SignatureRequestMessage,
+ );
+ await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200);
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmSignatureButton,
+ );
+ }
+}
export default TestDapp;
diff --git a/test/e2e/tests/account/snap-account-settings.spec.ts b/test/e2e/tests/account/snap-account-settings.spec.ts
index cbd5f8814b7b..1a0c761fb4df 100644
--- a/test/e2e/tests/account/snap-account-settings.spec.ts
+++ b/test/e2e/tests/account/snap-account-settings.spec.ts
@@ -33,7 +33,7 @@ describe('Add snap account experimental settings @no-mmi', function (this: Suite
await settingsPage.goToExperimentalSettings();
const experimentalSettings = new ExperimentalSettings(driver);
- await settingsPage.check_pageIsLoaded();
+ await experimentalSettings.check_pageIsLoaded();
await experimentalSettings.toggleAddAccountSnap();
// Make sure the "Add account Snap" button is visible.
diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts
new file mode 100644
index 000000000000..f5010fb61269
--- /dev/null
+++ b/test/e2e/tests/account/snap-account-signatures.spec.ts
@@ -0,0 +1,100 @@
+import { Suite } from 'mocha';
+import { Driver } from '../../webdriver/driver';
+import { WINDOW_TITLES, withFixtures } from '../../helpers';
+import ExperimentalSettings from '../../page-objects/pages/experimental-settings';
+import FixtureBuilder from '../../fixture-builder';
+import HeaderNavbar from '../../page-objects/pages/header-navbar';
+import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow';
+import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
+import {
+ personalSignWithSnapAccount,
+ signPermitWithSnapAccount,
+ signTypedDataV3WithSnapAccount,
+ signTypedDataV4WithSnapAccount,
+ signTypedDataWithSnapAccount,
+} from '../../page-objects/flows/sign.flow';
+import SettingsPage from '../../page-objects/pages/settings-page';
+import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page';
+import TestDapp from '../../page-objects/pages/test-dapp';
+
+describe('Snap Account Signatures @no-mmi', function (this: Suite) {
+ // Run sync, async approve, and async reject flows
+ // (in Jest we could do this with test.each, but that does not exist here)
+
+ ['sync', 'approve', 'reject'].forEach((flowType) => {
+ // generate title of the test from flowType
+ const title = `can sign with ${flowType} flow`;
+
+ it(title, async () => {
+ await withFixtures(
+ {
+ dapp: true,
+ fixtures: new FixtureBuilder()
+ .withPermissionControllerConnectedToTestDapp({
+ restrictReturnedAccounts: false,
+ })
+ .build(),
+ title,
+ },
+ async ({ driver }: { driver: Driver }) => {
+ const isSyncFlow = flowType === 'sync';
+ const approveTransaction = flowType === 'approve';
+ await loginWithBalanceValidation(driver);
+ await installSnapSimpleKeyring(driver, isSyncFlow);
+ const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver);
+ const newPublicKey = await snapSimpleKeyringPage.createNewAccount();
+
+ // Check snap account is displayed after adding the snap account.
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.check_accountLabel('SSK Account');
+
+ // Navigate to experimental settings and disable redesigned signature.
+ await headerNavbar.openSettingsPage();
+ const settingsPage = new SettingsPage(driver);
+ await settingsPage.check_pageIsLoaded();
+ await settingsPage.goToExperimentalSettings();
+
+ const experimentalSettings = new ExperimentalSettings(driver);
+ await experimentalSettings.check_pageIsLoaded();
+ await experimentalSettings.toggleRedesignedSignature();
+
+ // Run all 5 signature types
+ await new TestDapp(driver).openTestDappPage();
+ await personalSignWithSnapAccount(
+ driver,
+ newPublicKey,
+ isSyncFlow,
+ approveTransaction,
+ );
+ await signTypedDataWithSnapAccount(
+ driver,
+ newPublicKey,
+ isSyncFlow,
+ approveTransaction,
+ );
+ await signTypedDataV3WithSnapAccount(
+ driver,
+ newPublicKey,
+ isSyncFlow,
+ approveTransaction,
+ );
+ await signTypedDataV4WithSnapAccount(
+ driver,
+ newPublicKey,
+ isSyncFlow,
+ approveTransaction,
+ );
+ await signPermitWithSnapAccount(
+ driver,
+ newPublicKey,
+ isSyncFlow,
+ approveTransaction,
+ );
+ },
+ );
+ });
+ });
+});
diff --git a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts
index 7f26e02a572c..3e75adb34db8 100644
--- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts
+++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts
@@ -57,7 +57,7 @@ async function createTransactionAndAssertDetails(
const testDapp = new TestDapp(driver);
- await testDapp.open({ contractAddress, url: DAPP_URL });
+ await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL });
await testDapp.clickERC1155RevokeSetApprovalForAllButton();
diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts
index 438b3e979d0a..0e1134737c87 100644
--- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts
+++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts
@@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm(
const testDapp = new TestDapp(driver);
- await testDapp.open({ contractAddress, url: DAPP_URL });
+ await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL });
await testDapp.clickERC1155SetApprovalForAllButton();
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts
index 5a8dcd3768f7..138695904e55 100644
--- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts
+++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts
@@ -80,7 +80,7 @@ async function createTransactionAndAssertDetails(
const testDapp = new TestDapp(driver);
- await testDapp.open({ contractAddress, url: DAPP_URL });
+ await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL });
await testDapp.clickERC721RevokeSetApprovalForAllButton();
diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts
index 7ca9518cabc2..589670212be1 100644
--- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts
+++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts
@@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm(
const testDapp = new TestDapp(driver);
- await testDapp.open({ contractAddress, url: DAPP_URL });
+ await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL });
await testDapp.clickERC721SetApprovalForAllButton();
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
From fd1fad8c1f3056b2364ef75b1c22a1305cc29203 Mon Sep 17 00:00:00 2001
From: jiexi
Date: Wed, 16 Oct 2024 06:10:47 -0700
Subject: [PATCH 07/51] feat: Use requested permissions as default selected
values for AmonHenV2 connection flow with case insensitive address comparison
(#27517)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Previously, the permission approval component for the AmonHenV2 Flow
(accounts + permittedChains in one view) did not consider the caveat
values of the requested permission as valid defaults. This PR makes the
`ConnectPage` component use any caveat values in the permission request
as the default selected before falling back to the previous default
logic (currently selected account + all non test networks).
Also adds case insensitive account address comparison to related flow
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27517?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
`CHAIN_PERMISSIONS=1 yarn start`
```
await window.ethereum.request({
"method": "wallet_requestPermissions",
"params": [
{
eth_accounts: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: ['0x5bA08AF1bc30f17272178bDcACA1C74e94955cF4']
}
]
}
}
],
});
```
```
await window.ethereum.request({
"method": "wallet_requestPermissions",
"params": [
{
'endowment:permitted-chains': {
caveats: [
{
type: 'restrictNetworkSwitching',
value: ['0x1']
}
]
}
}
],
});
```
OR some combination of the above.
You should see the accounts/chains in the request as the default is
provided.
## **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.
---
.../edit-accounts-modal.tsx | 9 +-
.../site-cell/site-cell.tsx | 5 +-
.../__snapshots__/connect-page.test.tsx.snap | 240 ++++++++++++++++++
.../connect-page/connect-page.test.tsx | 37 +++
.../connect-page/connect-page.tsx | 33 ++-
5 files changed, 319 insertions(+), 5 deletions(-)
diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx
index ba842efc6a11..084596f07afb 100644
--- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx
+++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx
@@ -35,6 +35,7 @@ import {
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { MetaMetricsContext } from '../../../contexts/metametrics';
+import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
type EditAccountsModalProps = {
activeTabOrigin: string;
@@ -141,8 +142,12 @@ export const EditAccountsModal: React.FC = ({
isPinned={Boolean(account.pinned)}
startAccessory={
+ isEqualCaseInsensitive(
+ selectedAccountAddress,
+ account.address,
+ ),
)}
/>
}
diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx
index bb3a14a8f5e8..562d3e8c7d2e 100644
--- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx
+++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx
@@ -19,6 +19,7 @@ import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../../../shared/constants/metametrics';
+import { isEqualCaseInsensitive } from '../../../../../../shared/modules/string-utils';
import { SiteCellTooltip } from './site-cell-tooltip';
import { SiteCellConnectionListItem } from './site-cell-connection-list-item';
@@ -59,7 +60,9 @@ export const SiteCell: React.FC = ({
const [showEditNetworksModal, setShowEditNetworksModal] = useState(false);
const selectedAccounts = accounts.filter(({ address }) =>
- selectedAccountAddresses.includes(address),
+ selectedAccountAddresses.some((selectedAccountAddress) =>
+ isEqualCaseInsensitive(selectedAccountAddress, address),
+ ),
);
const selectedNetworks = allNetworks.filter(({ chainId }) =>
selectedChainIds.includes(chainId),
diff --git a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap
index e416011c1b08..ad53f67a7127 100644
--- a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap
+++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap
@@ -249,3 +249,243 @@ exports[`ConnectPage should render correctly 1`] = `
`;
+
+exports[`ConnectPage should render with defaults from the requested permissions 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ See your accounts and suggest transactions
+
+
+
+ Requesting for Test Account
+
+
+
+
+
+
+
+
+
+
+
+
+ Use your enabled networks
+
+
+
+ Requesting for
+
+
Alerts""
+ data-tooltipped=""
+ style="display: inline;"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx
index d7c50c6aa501..ef705e474ad9 100644
--- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx
+++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx
@@ -2,6 +2,11 @@ import React from 'react';
import { renderWithProvider } from '../../../../test/jest/rendering';
import mockState from '../../../../test/data/mock-state.json';
import configureStore from '../../../store/store';
+import {
+ CaveatTypes,
+ EndowmentTypes,
+ RestrictedMethods,
+} from '../../../../shared/constants/permissions';
import { ConnectPage, ConnectPageRequest } from './connect-page';
const render = (
@@ -74,4 +79,36 @@ describe('ConnectPage', () => {
expect(confirmButton).toBeDefined();
expect(cancelButton).toBeDefined();
});
+
+ it('should render with defaults from the requested permissions', () => {
+ const { container } = render({
+ request: {
+ id: '1',
+ origin: 'https://test.dapp',
+ permissions: {
+ [RestrictedMethods.eth_accounts]: {
+ caveats: [
+ {
+ type: CaveatTypes.restrictReturnedAccounts,
+ value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
+ },
+ ],
+ },
+ [EndowmentTypes.permittedChains]: {
+ caveats: [
+ {
+ type: CaveatTypes.restrictNetworkSwitching,
+ value: ['0x1'],
+ },
+ ],
+ },
+ },
+ },
+ permissionsRequestId: '1',
+ rejectPermissionsRequest: jest.fn(),
+ approveConnection: jest.fn(),
+ activeTabOrigin: 'https://test.dapp',
+ });
+ expect(container).toMatchSnapshot();
+ });
});
diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx
index a30047fbd38a..45e6c5b1f48f 100644
--- a/ui/pages/permissions-connect/connect-page/connect-page.tsx
+++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx
@@ -34,10 +34,19 @@ import { MergedInternalAccount } from '../../../selectors/selectors.types';
import { mergeAccounts } from '../../../components/multichain/account-list-menu/account-list-menu';
import { TEST_CHAINS } from '../../../../shared/constants/network';
import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer';
+import {
+ CaveatTypes,
+ EndowmentTypes,
+ RestrictedMethods,
+} from '../../../../shared/constants/permissions';
export type ConnectPageRequest = {
id: string;
origin: string;
+ permissions?: Record<
+ string,
+ { caveats?: { type: string; value: string[] }[] }
+ >;
};
type ConnectPageProps = {
@@ -57,6 +66,20 @@ export const ConnectPage: React.FC = ({
}) => {
const t = useI18nContext();
+ const ethAccountsPermission =
+ request?.permissions?.[RestrictedMethods.eth_accounts];
+ const requestedAccounts =
+ ethAccountsPermission?.caveats?.find(
+ (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts,
+ )?.value || [];
+
+ const permittedChainsPermission =
+ request?.permissions?.[EndowmentTypes.permittedChains];
+ const requestedChainIds =
+ permittedChainsPermission?.caveats?.find(
+ (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching,
+ )?.value || [];
+
const networkConfigurations = useSelector(getNetworkConfigurationsByChainId);
const [nonTestNetworks, testNetworks] = useMemo(
() =>
@@ -70,7 +93,10 @@ export const ConnectPage: React.FC = ({
),
[networkConfigurations],
);
- const defaultSelectedChainIds = nonTestNetworks.map(({ chainId }) => chainId);
+ const defaultSelectedChainIds =
+ requestedChainIds.length > 0
+ ? requestedChainIds
+ : nonTestNetworks.map(({ chainId }) => chainId);
const [selectedChainIds, setSelectedChainIds] = useState(
defaultSelectedChainIds,
);
@@ -84,7 +110,10 @@ export const ConnectPage: React.FC = ({
}, [accounts, internalAccounts]);
const currentAccount = useSelector(getSelectedInternalAccount);
- const defaultAccountsAddresses = [currentAccount?.address];
+ const defaultAccountsAddresses =
+ requestedAccounts.length > 0
+ ? requestedAccounts
+ : [currentAccount?.address];
const [selectedAccountAddresses, setSelectedAccountAddresses] = useState(
defaultAccountsAddresses,
);
From 56ed6930c59322b5275a94be7f75ca76a89e351f Mon Sep 17 00:00:00 2001
From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com>
Date: Wed, 16 Oct 2024 15:00:47 +0100
Subject: [PATCH 08/51] fix: Contract Interaction - cannot read the property
`text_signature` (#27686)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR introduces a validation to handle cases where the 4byte response
results are either undefined or an empty array. Instead of throwing an
error, the code now safely handles these cases by returning undefined,
preventing the TypeError from occurring.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27686?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27527
## **Manual testing steps**
1. Go to Remix
2. Deploy the contract below
3. Trigger the triggerMe func
4. See console error
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Params {
uint256 public x;
uint256 public value;
address public addr;
bool public flag;
string public text;
function triggerMe(
uint256 _x,
uint256 _value,
address _addr,
bool _flag,
string memory _text
) public returns (bool) {
x = _x;
value = _value;
addr = _addr;
flag = _flag;
text = _text;
return true;
}
receive() external payable {
}
}
```
## **Screenshots/Recordings**
[4bytes
response.webm](https://github.com/user-attachments/assets/0d6d8ba9-5c43-4c65-ad34-7ea416039c6f)
### **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.
---
shared/lib/four-byte.test.ts | 27 ++++++++++++++++++++++++---
shared/lib/four-byte.ts | 4 ++++
2 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/shared/lib/four-byte.test.ts b/shared/lib/four-byte.test.ts
index 2867aa2e51b7..77271c4aeba3 100644
--- a/shared/lib/four-byte.test.ts
+++ b/shared/lib/four-byte.test.ts
@@ -10,12 +10,14 @@ import { getMethodDataAsync, getMethodFrom4Byte } from './four-byte';
const FOUR_BYTE_MOCK = TRANSACTION_DATA_FOUR_BYTE.slice(0, 10);
describe('Four Byte', () => {
- const fetchMock = jest.fn();
-
describe('getMethodFrom4Byte', () => {
- it('returns signature with earliest creation date', async () => {
+ const fetchMock = jest.fn();
+
+ beforeEach(() => {
jest.spyOn(global, 'fetch').mockImplementation(fetchMock);
+ });
+ it('returns signature with earliest creation date', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => FOUR_BYTE_RESPONSE,
@@ -44,6 +46,25 @@ describe('Four Byte', () => {
expect(await getMethodFrom4Byte(prefix)).toBeUndefined();
},
);
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ ['undefined', { results: undefined }],
+ ['object', { results: {} }],
+ ['empty', { results: [] }],
+ ])(
+ 'returns `undefined` if fourByteResponse.results is %s',
+ async (_: string, mockResponse: { results: unknown }) => {
+ fetchMock.mockResolvedValue({
+ ok: true,
+ json: async () => mockResponse,
+ });
+
+ const result = await getMethodFrom4Byte('0x913aa952');
+
+ expect(result).toBeUndefined();
+ },
+ );
});
describe('getMethodDataAsync', () => {
diff --git a/shared/lib/four-byte.ts b/shared/lib/four-byte.ts
index e28f4d4c0c5c..c6b9da22e617 100644
--- a/shared/lib/four-byte.ts
+++ b/shared/lib/four-byte.ts
@@ -34,6 +34,10 @@ export async function getMethodFrom4Byte(
functionName: 'getMethodFrom4Byte',
})) as FourByteResponse;
+ if (!fourByteResponse.results?.length) {
+ return undefined;
+ }
+
fourByteResponse.results.sort((a, b) => {
return new Date(a.created_at).getTime() < new Date(b.created_at).getTime()
? -1
From bf87d720cb6c2f98487368dd8df81da078a7d2e7 Mon Sep 17 00:00:00 2001
From: Jyoti Puri
Date: Wed, 16 Oct 2024 20:24:01 +0530
Subject: [PATCH 09/51] feat: Adding typed sign support for NFT permit (#27796)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Adding support for NFT permit signature request.
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27396
## **Manual testing steps**
1. Submit an NFT permit request
2. Check the confirmation page that appears
## **Screenshots/Recordings**
## **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
- [ ] 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.
---
test/data/confirmations/typed_sign.ts | 15 ++++++
.../components/confirm/title/title.test.tsx | 20 ++++++-
.../components/confirm/title/title.tsx | 47 ++++++++++++----
ui/pages/confirmations/constants/index.ts | 5 ++
.../hooks/useTypedSignSignatureInfo.test.js | 27 ++++++++++
.../hooks/useTypedSignSignatureInfo.ts | 53 +++++++++++++++++++
6 files changed, 156 insertions(+), 11 deletions(-)
create mode 100644 ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js
create mode 100644 ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts
diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts
index f02705a2540b..7be24a1389c6 100644
--- a/test/data/confirmations/typed_sign.ts
+++ b/test/data/confirmations/typed_sign.ts
@@ -183,6 +183,21 @@ export const permitSignatureMsg = {
},
} as SignatureRequestType;
+export const permitNFTSignatureMsg = {
+ id: 'c5067710-87cf-11ef-916c-71f266571322',
+ status: 'unapproved',
+ time: 1728651190529,
+ type: 'eth_signTypedData',
+ msgParams: {
+ data: '{"domain":{"name":"Uniswap V3 Positions NFT-V1","version":"1","chainId":1,"verifyingContract":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88"},"types":{"Permit":[{"name":"spender","type":"address"},{"name":"tokenId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","message":{"spender":"0x00000000Ede6d8D217c60f93191C060747324bca","tokenId":"3606393","nonce":"0","deadline":"1734995006"}}',
+ from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
+ version: 'V4',
+ signatureMethod: 'eth_signTypedData_v4',
+ requestId: 2874791875,
+ origin: 'https://metamask.github.io',
+ },
+} as SignatureRequestType;
+
export const permitSignatureMsgWithNoDeadline = {
id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659',
securityAlertResponse: {
diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx
index 3c03343c2afb..3d4d6672940d 100644
--- a/ui/pages/confirmations/components/confirm/title/title.test.tsx
+++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx
@@ -11,7 +11,10 @@ import {
getMockTypedSignConfirmStateForRequest,
} from '../../../../../../test/data/confirmations/helper';
import { unapprovedPersonalSignMsg } from '../../../../../../test/data/confirmations/personal_sign';
-import { permitSignatureMsg } from '../../../../../../test/data/confirmations/typed_sign';
+import {
+ permitNFTSignatureMsg,
+ permitSignatureMsg,
+} from '../../../../../../test/data/confirmations/typed_sign';
import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers';
import { tEn } from '../../../../../../test/lib/i18n-helpers';
import {
@@ -71,6 +74,21 @@ describe('ConfirmTitle', () => {
).toBeInTheDocument();
});
+ it('should render the title and description for a NFT permit signature', () => {
+ const mockStore = configureMockStore([])(
+ getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg),
+ );
+ const { getByText } = renderWithConfirmContextProvider(
+ ,
+ mockStore,
+ );
+
+ expect(getByText('Withdrawal request')).toBeInTheDocument();
+ expect(
+ getByText('This site wants permission to withdraw your NFTs'),
+ ).toBeInTheDocument();
+ });
+
it('should render the title and description for typed signature', () => {
const mockStore = configureMockStore([])(getMockTypedSignConfirmState());
const { getByText } = renderWithConfirmContextProvider(
diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx
index 2645feed8a41..969e9c05518d 100644
--- a/ui/pages/confirmations/components/confirm/title/title.tsx
+++ b/ui/pages/confirmations/components/confirm/title/title.tsx
@@ -3,6 +3,8 @@ import {
TransactionType,
} from '@metamask/transaction-controller';
import React, { memo, useMemo } from 'react';
+
+import { TokenStandard } from '../../../../../../shared/constants/transaction';
import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert';
import { Box, Text } from '../../../../../components/component-library';
import {
@@ -12,12 +14,11 @@ import {
} from '../../../../../helpers/constants/design-system';
import useAlerts from '../../../../../hooks/useAlerts';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
+import { TypedSignSignaturePrimaryTypes } from '../../../constants';
import { useConfirmContext } from '../../../context/confirm';
import { Confirmation, SignatureRequestType } from '../../../types/confirm';
-import {
- isPermitSignatureRequest,
- isSIWESignatureRequest,
-} from '../../../utils';
+import { isSIWESignatureRequest } from '../../../utils';
+import { useTypedSignSignatureInfo } from '../../../hooks/useTypedSignSignatureInfo';
import { useIsNFT } from '../info/approve/hooks/use-is-nft';
import { useDecodedTransactionData } from '../info/hooks/useDecodedTransactionData';
import { getIsRevokeSetApprovalForAll } from '../info/utils';
@@ -51,6 +52,8 @@ function ConfirmBannerAlert({ ownerId }: { ownerId: string }) {
type IntlFunction = (str: string) => string;
+// todo: getTitle and getDescription can be merged to remove code duplication.
+
const getTitle = (
t: IntlFunction,
confirmation?: Confirmation,
@@ -58,6 +61,8 @@ const getTitle = (
customSpendingCap?: string,
isRevokeSetApprovalForAll?: boolean,
pending?: boolean,
+ primaryType?: keyof typeof TypedSignSignaturePrimaryTypes,
+ tokenStandard?: string,
) => {
if (pending) {
return '';
@@ -74,9 +79,13 @@ const getTitle = (
}
return t('confirmTitleSignature');
case TransactionType.signTypedData:
- return isPermitSignatureRequest(confirmation as SignatureRequestType)
- ? t('confirmTitlePermitTokens')
- : t('confirmTitleSignature');
+ if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) {
+ if (tokenStandard === TokenStandard.ERC721) {
+ return t('setApprovalForAllRedesignedTitle');
+ }
+ return t('confirmTitlePermitTokens');
+ }
+ return t('confirmTitleSignature');
case TransactionType.tokenMethodApprove:
if (isNFT) {
return t('confirmTitleApproveTransaction');
@@ -104,6 +113,8 @@ const getDescription = (
customSpendingCap?: string,
isRevokeSetApprovalForAll?: boolean,
pending?: boolean,
+ primaryType?: keyof typeof TypedSignSignaturePrimaryTypes,
+ tokenStandard?: string,
) => {
if (pending) {
return '';
@@ -120,9 +131,13 @@ const getDescription = (
}
return t('confirmTitleDescSign');
case TransactionType.signTypedData:
- return isPermitSignatureRequest(confirmation as SignatureRequestType)
- ? t('confirmTitleDescPermitSignature')
- : t('confirmTitleDescSign');
+ if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) {
+ if (tokenStandard === TokenStandard.ERC721) {
+ return t('confirmTitleDescApproveTransaction');
+ }
+ return t('confirmTitleDescPermitSignature');
+ }
+ return t('confirmTitleDescSign');
case TransactionType.tokenMethodApprove:
if (isNFT) {
return t('confirmTitleDescApproveTransaction');
@@ -150,6 +165,10 @@ const ConfirmTitle: React.FC = memo(() => {
const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta);
+ const { primaryType, tokenStandard } = useTypedSignSignatureInfo(
+ currentConfirmation as SignatureRequestType,
+ );
+
const { customSpendingCap, pending: spendingCapPending } =
useCurrentSpendingCap(currentConfirmation);
@@ -175,6 +194,8 @@ const ConfirmTitle: React.FC = memo(() => {
customSpendingCap,
isRevokeSetApprovalForAll,
spendingCapPending || revokePending,
+ primaryType,
+ tokenStandard,
),
[
currentConfirmation,
@@ -183,6 +204,8 @@ const ConfirmTitle: React.FC = memo(() => {
isRevokeSetApprovalForAll,
spendingCapPending,
revokePending,
+ primaryType,
+ tokenStandard,
],
);
@@ -195,6 +218,8 @@ const ConfirmTitle: React.FC = memo(() => {
customSpendingCap,
isRevokeSetApprovalForAll,
spendingCapPending || revokePending,
+ primaryType,
+ tokenStandard,
),
[
currentConfirmation,
@@ -203,6 +228,8 @@ const ConfirmTitle: React.FC = memo(() => {
isRevokeSetApprovalForAll,
spendingCapPending,
revokePending,
+ primaryType,
+ tokenStandard,
],
);
diff --git a/ui/pages/confirmations/constants/index.ts b/ui/pages/confirmations/constants/index.ts
index 38fd05b714ba..7e26ce5c6d62 100644
--- a/ui/pages/confirmations/constants/index.ts
+++ b/ui/pages/confirmations/constants/index.ts
@@ -9,3 +9,8 @@ export const TYPED_SIGNATURE_VERSIONS = {
};
export const SPENDING_CAP_UNLIMITED_MSG = 'UNLIMITED MESSAGE';
+
+export const TypedSignSignaturePrimaryTypes = {
+ PERMIT: 'Permit',
+ ORDER: 'Order',
+};
diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js
new file mode 100644
index 000000000000..38468749782d
--- /dev/null
+++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js
@@ -0,0 +1,27 @@
+import { renderHook } from '@testing-library/react-hooks';
+
+import { TokenStandard } from '../../../../shared/constants/transaction';
+import { permitNFTSignatureMsg } from '../../../../test/data/confirmations/typed_sign';
+import { unapprovedPersonalSignMsg } from '../../../../test/data/confirmations/personal_sign';
+import { TypedSignSignaturePrimaryTypes } from '../constants';
+import { useTypedSignSignatureInfo } from './useTypedSignSignatureInfo';
+
+describe('useTypedSignSignatureInfo', () => {
+ it('should return details for primaty type and token standard', () => {
+ const { result } = renderHook(() =>
+ useTypedSignSignatureInfo(permitNFTSignatureMsg),
+ );
+ expect(result.current.primaryType).toStrictEqual(
+ TypedSignSignaturePrimaryTypes.PERMIT,
+ );
+ expect(result.current.tokenStandard).toStrictEqual(TokenStandard.ERC721);
+ });
+
+ it('should return empty object if confirmation is not typed sign', () => {
+ const { result } = renderHook(() =>
+ useTypedSignSignatureInfo(unapprovedPersonalSignMsg),
+ );
+ expect(result.current.primaryType).toBeUndefined();
+ expect(result.current.tokenStandard).toBeUndefined();
+ });
+});
diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts
new file mode 100644
index 000000000000..30d4e58f1525
--- /dev/null
+++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts
@@ -0,0 +1,53 @@
+import { useMemo } from 'react';
+
+import {
+ isOrderSignatureRequest,
+ isPermitSignatureRequest,
+ isSignatureTransactionType,
+} from '../utils';
+import { SignatureRequestType } from '../types/confirm';
+import { parseTypedDataMessage } from '../../../../shared/modules/transaction.utils';
+import { TokenStandard } from '../../../../shared/constants/transaction';
+import { MESSAGE_TYPE } from '../../../../shared/constants/app';
+import { TypedSignSignaturePrimaryTypes } from '../constants';
+
+export const useTypedSignSignatureInfo = (
+ confirmation: SignatureRequestType,
+) => {
+ const primaryType = useMemo(() => {
+ if (
+ !confirmation ||
+ !isSignatureTransactionType(confirmation) ||
+ confirmation?.type !== MESSAGE_TYPE.ETH_SIGN_TYPED_DATA
+ ) {
+ return undefined;
+ }
+ if (isPermitSignatureRequest(confirmation)) {
+ return TypedSignSignaturePrimaryTypes.PERMIT;
+ } else if (isOrderSignatureRequest(confirmation)) {
+ return TypedSignSignaturePrimaryTypes.ORDER;
+ }
+ return undefined;
+ }, [confirmation]);
+
+ // here we are using presence of tokenId in typed message data to know if its NFT permit
+ // we can get contract details for verifyingContract but that is async process taking longer
+ // and result in confirmation page content loading late
+ const tokenStandard = useMemo(() => {
+ if (primaryType !== TypedSignSignaturePrimaryTypes.PERMIT) {
+ return undefined;
+ }
+ const {
+ message: { tokenId },
+ } = parseTypedDataMessage(confirmation?.msgParams?.data as string);
+ if (tokenId !== undefined) {
+ return TokenStandard.ERC721;
+ }
+ return undefined;
+ }, [confirmation, primaryType]);
+
+ return {
+ primaryType: primaryType as keyof typeof TypedSignSignaturePrimaryTypes,
+ tokenStandard,
+ };
+};
From a1e0b71a15b8458cae05d61cbbecd0f5d22a4fa6 Mon Sep 17 00:00:00 2001
From: Howard Braham
Date: Wed, 16 Oct 2024 08:28:24 -0700
Subject: [PATCH 10/51] test: set ENABLE_MV3 automatically (#27748)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
The stated problem was "automatically set `ENABLE_MV3` based on the
browser, in `run-e2e-test`"
However, that would only handle the case of Firefox, and miss builds
with:
- `start:mv2`
- `dist:mv2`
- `build:test:flask:mv2`
- `build:test:mv2`
- Any `webpack` build
I had previously written code that reads `manifest.json` from Node, so I
thought "hey let's just read it!" When running a test, you now never
even have to **think** about `ENABLE_MV3`.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27748?quickstart=1)
## **Related issues**
Fixes: #27704
## **Manual testing steps**
## **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
- [ ] 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.
---
.circleci/config.yml | 4 ++--
README.md | 5 ++++-
package.json | 2 +-
shared/modules/mv3.utils.js | 9 ++++-----
test/e2e/manifest-flag-mocha-hooks.ts | 4 +++-
test/e2e/set-manifest-flags.ts | 21 ++++++++++++++++++---
6 files changed, 32 insertions(+), 13 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b2c5ab712973..2bf244b9bf8a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1250,7 +1250,7 @@ jobs:
command: mv ./builds-test-flask-mv2 ./builds
- run:
name: test:e2e:firefox:flask
- command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask
+ command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask
no_output_timeout: 5m
- store_artifacts:
path: test-artifacts
@@ -1393,7 +1393,7 @@ jobs:
command: mv ./builds-test-mv2 ./builds
- run:
name: test:e2e:firefox
- command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox
+ command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox
no_output_timeout: 5m
- store_artifacts:
path: test-artifacts
diff --git a/README.md b/README.md
index 4f15e138be56..f3e738a40abc 100644
--- a/README.md
+++ b/README.md
@@ -84,12 +84,15 @@ If you are using VS Code and are unable to make commits from the source control
To start a development build (e.g. with logging and file watching) run `yarn start`.
#### Development build with wallet state
+
You can start a development build with a preloaded wallet state, by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file. Then you have the following options:
+
1. Start the wallet with the default fixture flags, by running `yarn start:with-state`.
2. Check the list of available fixture flags, by running `yarn start:with-state --help`.
3. Start the wallet with custom fixture flags, by running `yarn start:with-state --FIXTURE_NAME=VALUE` for example `yarn start:with-state --withAccounts=100`. You can pass as many flags as you want. The rest of the fixtures will take the default values.
#### Development build with Webpack
+
You can also start a development build using the `yarn webpack` command, or `yarn webpack --watch`. This uses an alternative build system that is much faster, but not yet production ready. See the [Webpack README](./development/webpack/README.md) for more information.
#### React and Redux DevTools
@@ -191,7 +194,7 @@ Different build types have different e2e tests sets. In order to run them look i
```console
"test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi",
"test:e2e:chrome:snaps": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --snaps",
- "test:e2e:firefox": "ENABLE_MV3=false SELENIUM_BROWSER=firefox node test/e2e/run-all.js",
+ "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js",
```
#### Note: Running MMI e2e tests
diff --git a/package.json b/package.json
index 860bad431285..9a77bb273995 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
"test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js",
"test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi",
"test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask",
- "test:e2e:chrome:webpack": "ENABLE_MV3=false SELENIUM_BROWSER=chrome node test/e2e/run-all.js",
+ "test:e2e:chrome:webpack": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js",
"test:api-specs": "SELENIUM_BROWSER=chrome ts-node test/e2e/run-openrpc-api-test-coverage.ts",
"test:e2e:mmi:ci": "yarn playwright test --project=mmi --project=mmi.visual",
"test:e2e:mmi:all": "yarn playwright test --project=mmi && yarn test:e2e:mmi:visual",
diff --git a/shared/modules/mv3.utils.js b/shared/modules/mv3.utils.js
index 417484c46de6..24a4bc8880f4 100644
--- a/shared/modules/mv3.utils.js
+++ b/shared/modules/mv3.utils.js
@@ -6,14 +6,13 @@ const runtimeManifest =
/**
* A boolean indicating whether the manifest of the current extension is set to manifest version 3.
*
- * We have found that when this is run early in a service worker process, the runtime manifest is
- * unavailable. That's why we have a fallback using the ENABLE_MV3 constant. The fallback is also
- * used in unit tests.
+ * If this function is running in the Extension, it will use the runtime manifest.
+ * If this function is running in Node doing a build job, it will read process.env.ENABLE_MV3.
+ * If this function is running in Node doing an E2E test, it will `fs.readFileSync` the manifest.json file.
*/
const isManifestV3 = runtimeManifest
? runtimeManifest.manifest_version === 3
- : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it will
- // always be a string
+ : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it can be a string
process.env.ENABLE_MV3 === true ||
process.env.ENABLE_MV3 === 'true' ||
process.env.ENABLE_MV3 === undefined;
diff --git a/test/e2e/manifest-flag-mocha-hooks.ts b/test/e2e/manifest-flag-mocha-hooks.ts
index f319984eb067..f5c8d5e8099c 100644
--- a/test/e2e/manifest-flag-mocha-hooks.ts
+++ b/test/e2e/manifest-flag-mocha-hooks.ts
@@ -14,7 +14,9 @@
*/
import fs from 'fs';
import { hasProperty } from '@metamask/utils';
-import { folder } from './set-manifest-flags';
+import { folder, getManifestVersion } from './set-manifest-flags';
+
+process.env.ENABLE_MV3 = getManifestVersion() === 3 ? 'true' : 'false';
// Global beforeEach hook to backup the manifest.json file
if (typeof beforeEach === 'function' && process.env.SELENIUM_BROWSER) {
diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts
index 290e8b863a9e..75339250506f 100644
--- a/test/e2e/set-manifest-flags.ts
+++ b/test/e2e/set-manifest-flags.ts
@@ -5,6 +5,9 @@ import { ManifestFlags } from '../../app/scripts/lib/manifestFlags';
export const folder = `dist/${process.env.SELENIUM_BROWSER}`;
+type ManifestType = { _flags?: ManifestFlags; manifest_version: string };
+let manifest: ManifestType;
+
function parseIntOrUndefined(value: string | undefined): number | undefined {
return value ? parseInt(value, 10) : undefined;
}
@@ -113,11 +116,23 @@ export function setManifestFlags(flags: ManifestFlags = {}) {
}
}
- const manifest = JSON.parse(
- fs.readFileSync(`${folder}/manifest.json`).toString(),
- );
+ readManifest();
manifest._flags = flags;
fs.writeFileSync(`${folder}/manifest.json`, JSON.stringify(manifest));
}
+
+export function getManifestVersion(): number {
+ readManifest();
+
+ return parseInt(manifest.manifest_version, 10);
+}
+
+function readManifest() {
+ if (!manifest) {
+ manifest = JSON.parse(
+ fs.readFileSync(`${folder}/manifest.json`).toString(),
+ );
+ }
+}
From 6d9cc1f5b44223bdea3b362ab14c436207e9015e Mon Sep 17 00:00:00 2001
From: micaelae <100321200+micaelae@users.noreply.github.com>
Date: Wed, 16 Oct 2024 09:27:02 -0700
Subject: [PATCH 11/51] feat: use asset pickers with network dropdown in
cross-chain swaps page (#27522)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Changes include
* PrepareBridge - cross-chain swaps landing page, accepts user inputs
for quote params
* useTokensWithFiltering - new hook for sorting and filtering tokens
* useLatestBalance - new hook that returns a user's src chain balance on
token selection
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26430?quickstart=1)
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/METABRIDGE-866
## **Manual testing steps**
1. Set BRIDGE_USE_DEV_APIS=1 and load extension
2. Click Bridge button
3. Verify that PrepareBridgePage appears
4. Change src/dest chains and tokens; verify that token list is updated
as a result
5. Change input value
6. Navigate away from Bridge page using Back button
7. Navigate back to Bridge page and verify that input fields are reset
## **Screenshots/Recordings**
### **Before**
![Screenshot 2024-07-12 at 3 44
34 PM](https://github.com/user-attachments/assets/9bcb8552-c1e7-4a4e-b9f3-b70327341d68)
### **After**
https://github.com/user-attachments/assets/1779a548-92da-4e80-8974-038d984ac0a0
## **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/_locales/en/messages.json | 6 +
test/e2e/tests/bridge/bridge-test-utils.ts | 4 -
ui/ducks/bridge/actions.ts | 10 +-
ui/ducks/bridge/bridge.test.ts | 62 ++-
ui/ducks/bridge/bridge.ts | 9 +
ui/hooks/bridge/useBridging.test.ts | 2 +-
ui/hooks/bridge/useBridging.ts | 12 +-
ui/hooks/bridge/useLatestBalance.test.ts | 89 ++++
ui/hooks/bridge/useLatestBalance.ts | 66 +++
ui/hooks/useTokensWithFiltering.test.ts | 153 ++++++
ui/hooks/useTokensWithFiltering.ts | 178 +++++++
.../bridge/__snapshots__/index.test.tsx.snap | 56 ++-
ui/pages/bridge/index.scss | 28 ++
ui/pages/bridge/index.tsx | 93 ++--
.../bridge-cta-button.test.tsx.snap | 14 +
.../prepare-bridge-page.test.tsx.snap | 456 ++++++++++++++++++
.../bridge/prepare/bridge-cta-button.test.tsx | 50 ++
ui/pages/bridge/prepare/bridge-cta-button.tsx | 49 ++
.../bridge/prepare/bridge-input-group.tsx | 157 ++++++
ui/pages/bridge/prepare/index.scss | 170 +++++++
.../prepare/prepare-bridge-page.test.tsx | 118 +++++
.../bridge/prepare/prepare-bridge-page.tsx | 173 ++++++-
.../connect-hardware/index.test.tsx | 4 +
ui/pages/pages.scss | 1 +
ui/pages/routes/routes.component.js | 11 +
25 files changed, 1873 insertions(+), 98 deletions(-)
create mode 100644 ui/hooks/bridge/useLatestBalance.test.ts
create mode 100644 ui/hooks/bridge/useLatestBalance.ts
create mode 100644 ui/hooks/useTokensWithFiltering.test.ts
create mode 100644 ui/hooks/useTokensWithFiltering.ts
create mode 100644 ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
create mode 100644 ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
create mode 100644 ui/pages/bridge/prepare/bridge-cta-button.test.tsx
create mode 100644 ui/pages/bridge/prepare/bridge-cta-button.tsx
create mode 100644 ui/pages/bridge/prepare/bridge-input-group.tsx
create mode 100644 ui/pages/bridge/prepare/index.scss
create mode 100644 ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 49d48b9c71ac..6e907c35a088 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -854,9 +854,15 @@
"bridgeDontSend": {
"message": "Bridge, don't send"
},
+ "bridgeFrom": {
+ "message": "Bridge from"
+ },
"bridgeSelectNetwork": {
"message": "Select network"
},
+ "bridgeTo": {
+ "message": "Bridge to"
+ },
"browserNotSupported": {
"message": "Your browser is not supported..."
},
diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts
index 40bb8c6bd97f..1f4a3e5cda79 100644
--- a/test/e2e/tests/bridge/bridge-test-utils.ts
+++ b/test/e2e/tests/bridge/bridge-test-utils.ts
@@ -84,10 +84,6 @@ export class BridgePage {
verifySwapPage = async (expectedHandleCount: number) => {
await this.driver.delay(4000);
- await this.driver.waitForSelector({
- css: '.bridge__title',
- text: 'Bridge',
- });
assert.equal(
(await this.driver.getAllWindowHandles()).length,
IS_FIREFOX || !isManifestV3
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index 5bfbda1e23cf..5e50b004b774 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -18,9 +18,17 @@ const {
setFromToken,
setToToken,
setFromTokenInputValue,
+ resetInputFields,
+ switchToAndFromTokens,
} = bridgeSlice.actions;
-export { setFromToken, setToToken, setFromTokenInputValue };
+export {
+ setFromToken,
+ setToToken,
+ setFromTokenInputValue,
+ switchToAndFromTokens,
+ resetInputFields,
+};
const callBridgeControllerMethod = (
bridgeAction: BridgeUserAction | BridgeBackgroundAction,
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index b8d2e09eb0ea..f4a566c233b5 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -17,6 +17,8 @@ import {
setToChain,
setToToken,
setFromChain,
+ resetInputFields,
+ switchToAndFromTokens,
} from './actions';
const middleware = [thunk];
@@ -43,9 +45,9 @@ describe('Ducks - Bridge', () => {
// Check redux state
const actions = store.getActions();
- expect(actions[0].type).toBe('bridge/setToChainId');
+ expect(actions[0].type).toStrictEqual('bridge/setToChainId');
const newState = bridgeReducer(state, actions[0]);
- expect(newState.toChainId).toBe(actionPayload);
+ expect(newState.toChainId).toStrictEqual(actionPayload);
// Check background state
expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1);
expect(mockSelectDestNetwork).toHaveBeenCalledWith(
@@ -61,9 +63,9 @@ describe('Ducks - Bridge', () => {
const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' };
store.dispatch(setFromToken(actionPayload));
const actions = store.getActions();
- expect(actions[0].type).toBe('bridge/setFromToken');
+ expect(actions[0].type).toStrictEqual('bridge/setFromToken');
const newState = bridgeReducer(state, actions[0]);
- expect(newState.fromToken).toBe(actionPayload);
+ expect(newState.fromToken).toStrictEqual(actionPayload);
});
});
@@ -73,9 +75,9 @@ describe('Ducks - Bridge', () => {
const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' };
store.dispatch(setToToken(actionPayload));
const actions = store.getActions();
- expect(actions[0].type).toBe('bridge/setToToken');
+ expect(actions[0].type).toStrictEqual('bridge/setToToken');
const newState = bridgeReducer(state, actions[0]);
- expect(newState.toToken).toBe(actionPayload);
+ expect(newState.toToken).toStrictEqual(actionPayload);
});
});
@@ -85,9 +87,9 @@ describe('Ducks - Bridge', () => {
const actionPayload = '10';
store.dispatch(setFromTokenInputValue(actionPayload));
const actions = store.getActions();
- expect(actions[0].type).toBe('bridge/setFromTokenInputValue');
+ expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue');
const newState = bridgeReducer(state, actions[0]);
- expect(newState.fromTokenInputValue).toBe(actionPayload);
+ expect(newState.fromTokenInputValue).toStrictEqual(actionPayload);
});
});
@@ -118,4 +120,48 @@ describe('Ducks - Bridge', () => {
);
});
});
+
+ describe('resetInputFields', () => {
+ it('resets to initalState', async () => {
+ const state = store.getState().bridge;
+ store.dispatch(resetInputFields());
+ const actions = store.getActions();
+ expect(actions[0].type).toStrictEqual('bridge/resetInputFields');
+ const newState = bridgeReducer(state, actions[0]);
+ expect(newState).toStrictEqual({
+ toChainId: null,
+ fromToken: null,
+ toToken: null,
+ fromTokenInputValue: null,
+ });
+ });
+ });
+
+ describe('switchToAndFromTokens', () => {
+ it('switches to and from input values', async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const bridgeStore = configureMockStore(middleware)(
+ createBridgeMockStore(
+ {},
+ {
+ toChainId: CHAIN_IDS.MAINNET,
+ fromToken: { symbol: 'WETH', address: '0x13341432' },
+ toToken: { symbol: 'USDC', address: '0x13341431' },
+ fromTokenInputValue: '10',
+ },
+ ),
+ );
+ const state = bridgeStore.getState().bridge;
+ bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON));
+ const actions = bridgeStore.getActions();
+ expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens');
+ const newState = bridgeReducer(state, actions[0]);
+ expect(newState).toStrictEqual({
+ toChainId: CHAIN_IDS.POLYGON,
+ fromToken: { symbol: 'USDC', address: '0x13341431' },
+ toToken: { symbol: 'WETH', address: '0x13341432' },
+ fromTokenInputValue: null,
+ });
+ });
+ });
});
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index f2469d1025f3..9ec744d9e953 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -36,6 +36,15 @@ const bridgeSlice = createSlice({
setFromTokenInputValue: (state, action) => {
state.fromTokenInputValue = action.payload;
},
+ resetInputFields: () => ({
+ ...initialState,
+ }),
+ switchToAndFromTokens: (state, { payload }) => ({
+ toChainId: payload,
+ fromToken: state.toToken,
+ toToken: state.fromToken,
+ fromTokenInputValue: null,
+ }),
},
});
diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts
index df8bbb940f4e..6e3f3b534e35 100644
--- a/ui/hooks/bridge/useBridging.test.ts
+++ b/ui/hooks/bridge/useBridging.test.ts
@@ -174,7 +174,7 @@ describe('useBridging', () => {
result.current.openBridgeExperience(location, token, urlSuffix);
- expect(mockDispatch.mock.calls).toHaveLength(3);
+ expect(mockDispatch.mock.calls).toHaveLength(2);
expect(mockHistoryPush.mock.calls).toHaveLength(1);
expect(mockHistoryPush).toHaveBeenCalledWith(expectedUrl);
expect(openTabSpy).not.toHaveBeenCalled();
diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts
index ce4b8c48b89c..a68aeb361bdd 100644
--- a/ui/hooks/bridge/useBridging.ts
+++ b/ui/hooks/bridge/useBridging.ts
@@ -1,10 +1,7 @@
import { useCallback, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
-import {
- setBridgeFeatureFlags,
- setFromChain,
-} from '../../ducks/bridge/actions';
+import { setBridgeFeatureFlags } from '../../ducks/bridge/actions';
import {
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
getCurrentKeyring,
@@ -55,13 +52,6 @@ const useBridging = () => {
dispatch(setBridgeFeatureFlags());
}, [dispatch, setBridgeFeatureFlags]);
- useEffect(() => {
- isBridgeChain &&
- isBridgeSupported &&
- providerConfig &&
- dispatch(setFromChain(providerConfig.chainId));
- }, []);
-
const openBridgeExperience = useCallback(
(
location: string,
diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts
new file mode 100644
index 000000000000..d1186c3eeb91
--- /dev/null
+++ b/ui/hooks/bridge/useLatestBalance.test.ts
@@ -0,0 +1,89 @@
+import { BigNumber } from 'ethers';
+import { renderHookWithProvider } from '../../../test/lib/render-helpers';
+import { CHAIN_IDS } from '../../../shared/constants/network';
+import { createBridgeMockStore } from '../../../test/jest/mock-store';
+import { zeroAddress } from '../../__mocks__/ethereumjs-util';
+import { createTestProviderTools } from '../../../test/stub/provider';
+import * as tokenutil from '../../../shared/lib/token-util';
+import useLatestBalance from './useLatestBalance';
+
+const mockGetBalance = jest.fn();
+jest.mock('@ethersproject/providers', () => {
+ return {
+ Web3Provider: jest.fn().mockImplementation(() => {
+ return {
+ getBalance: mockGetBalance,
+ };
+ }),
+ };
+});
+
+const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance');
+jest.mock('../../../shared/lib/token-util', () => ({
+ ...jest.requireActual('../../../shared/lib/token-util'),
+ fetchTokenBalance: jest.fn(),
+}));
+
+const renderUseLatestBalance = (
+ token: { address: string; decimals?: number | string },
+ chainId: string,
+ mockStoreState: object,
+) =>
+ renderHookWithProvider(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ () => useLatestBalance(token as any, chainId as any),
+ mockStoreState,
+ );
+
+describe('useLatestBalance', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ const { provider } = createTestProviderTools({
+ networkId: 'Ethereum',
+ chainId: CHAIN_IDS.MAINNET,
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ global.ethereumProvider = provider as any;
+ });
+
+ it('returns formattedBalance for native asset in current chain', async () => {
+ mockGetBalance.mockResolvedValue(BigNumber.from('1000000000000000000'));
+
+ const { result, waitForNextUpdate } = renderUseLatestBalance(
+ { address: zeroAddress(), decimals: 18 },
+ CHAIN_IDS.MAINNET,
+ createBridgeMockStore(),
+ );
+
+ await waitForNextUpdate();
+ expect(result.current.formattedBalance).toStrictEqual('1');
+
+ expect(mockGetBalance).toHaveBeenCalledTimes(1);
+ expect(mockGetBalance).toHaveBeenCalledWith(
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ );
+ expect(mockFetchTokenBalance).toHaveBeenCalledTimes(0);
+ });
+
+ it('returns formattedBalance for ERC20 asset in current chain', async () => {
+ mockFetchTokenBalance.mockResolvedValueOnce(BigNumber.from('15390000'));
+
+ const { result, waitForNextUpdate } = renderUseLatestBalance(
+ { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: '6' },
+ CHAIN_IDS.MAINNET,
+ createBridgeMockStore(),
+ );
+
+ await waitForNextUpdate();
+ expect(result.current.formattedBalance).toStrictEqual('15.39');
+
+ expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1);
+ expect(mockFetchTokenBalance).toHaveBeenCalledWith(
+ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ global.ethereumProvider,
+ );
+ expect(mockGetBalance).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts
new file mode 100644
index 000000000000..dfe868a04090
--- /dev/null
+++ b/ui/hooks/bridge/useLatestBalance.ts
@@ -0,0 +1,66 @@
+import { useSelector } from 'react-redux';
+import { zeroAddress } from 'ethereumjs-util';
+import { Web3Provider } from '@ethersproject/providers';
+import { Hex } from '@metamask/utils';
+import { BigNumber } from 'ethers';
+import { Numeric } from '../../../shared/modules/Numeric';
+import { DEFAULT_PRECISION } from '../useCurrencyDisplay';
+import { fetchTokenBalance } from '../../../shared/lib/token-util';
+import {
+ getCurrentChainId,
+ getSelectedInternalAccount,
+ SwapsEthToken,
+} from '../../selectors';
+import { SwapsTokenObject } from '../../../shared/constants/swaps';
+import { useAsyncResult } from '../useAsyncResult';
+
+/**
+ * Custom hook to fetch and format the latest balance of a given token or native asset.
+ *
+ * @param token - The token object for which the balance is to be fetched. Can be null.
+ * @param chainId - The chain ID to be used for fetching the balance. Optional.
+ * @returns An object containing the formatted balance as a string.
+ */
+const useLatestBalance = (
+ token: SwapsTokenObject | SwapsEthToken | null,
+ chainId?: Hex,
+) => {
+ const { address: selectedAddress } = useSelector(getSelectedInternalAccount);
+ const currentChainId = useSelector(getCurrentChainId);
+
+ const { value: latestBalance } = useAsyncResult(async () => {
+ if (token && chainId && currentChainId === chainId) {
+ if (!token.address || token.address === zeroAddress()) {
+ const ethersProvider = new Web3Provider(global.ethereumProvider);
+ return await ethersProvider.getBalance(selectedAddress);
+ }
+ return await fetchTokenBalance(
+ token.address,
+ selectedAddress,
+ global.ethereumProvider,
+ );
+ }
+
+ return undefined;
+ }, [token, selectedAddress, global.ethereumProvider]);
+
+ if (token && !token.decimals) {
+ throw new Error(
+ `Failed to calculate latest balance - ${token.symbol} token is missing "decimals" value`,
+ );
+ }
+
+ const tokenDecimals = token?.decimals ? Number(token.decimals) : 1;
+
+ return {
+ formattedBalance:
+ token && latestBalance
+ ? Numeric.from(latestBalance.toString(), 10)
+ .shiftedBy(tokenDecimals)
+ .round(DEFAULT_PRECISION)
+ .toString()
+ : undefined,
+ };
+};
+
+export default useLatestBalance;
diff --git a/ui/hooks/useTokensWithFiltering.test.ts b/ui/hooks/useTokensWithFiltering.test.ts
new file mode 100644
index 000000000000..f5ea05e02b8d
--- /dev/null
+++ b/ui/hooks/useTokensWithFiltering.test.ts
@@ -0,0 +1,153 @@
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import { createBridgeMockStore } from '../../test/jest/mock-store';
+import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens';
+import {
+ SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
+ SwapsTokenObject,
+ TokenBucketPriority,
+} from '../../shared/constants/swaps';
+import { useTokensWithFiltering } from './useTokensWithFiltering';
+
+const mockUseTokenTracker = jest
+ .fn()
+ .mockReturnValue({ tokensWithBalances: [] });
+jest.mock('./useTokenTracker', () => ({
+ useTokenTracker: () => mockUseTokenTracker(),
+}));
+
+const TEST_CHAIN_ID = '0x1';
+const NATIVE_TOKEN = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[TEST_CHAIN_ID];
+
+const MOCK_TOP_ASSETS = [
+ { address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' }, // UNI
+ { address: NATIVE_TOKEN.address },
+ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, // USDC
+ { address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, // USDT
+];
+
+const MOCK_TOKEN_LIST_BY_ADDRESS: Record = {
+ [NATIVE_TOKEN.address]: NATIVE_TOKEN,
+ ...STATIC_MAINNET_TOKEN_LIST,
+};
+
+describe('useTokensWithFiltering should return token list generator', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('when chainId === activeChainId and sorted by topAssets', () => {
+ const mockStore = createBridgeMockStore();
+ const { result } = renderHookWithProvider(
+ () =>
+ useTokensWithFiltering(
+ MOCK_TOKEN_LIST_BY_ADDRESS,
+ MOCK_TOP_ASSETS,
+ TokenBucketPriority.top,
+ TEST_CHAIN_ID,
+ ),
+ mockStore,
+ );
+
+ expect(result.current).toHaveLength(1);
+ expect(typeof result.current).toStrictEqual('function');
+ const tokenGenerator = result.current(() => true);
+ expect(tokenGenerator.next().value).toStrictEqual({
+ address: '0x0000000000000000000000000000000000000000',
+ balance: undefined,
+ decimals: 18,
+ iconUrl: './images/eth_logo.svg',
+ identiconAddress: null,
+ image: './images/eth_logo.svg',
+ name: 'Ether',
+ primaryLabel: 'ETH',
+ rawFiat: '',
+ rightPrimaryLabel: undefined,
+ rightSecondaryLabel: '',
+ secondaryLabel: 'Ether',
+ symbol: 'ETH',
+ type: 'NATIVE',
+ });
+ expect(tokenGenerator.next().value).toStrictEqual({
+ address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
+ aggregators: [],
+ balance: undefined,
+ decimals: 18,
+ erc20: true,
+ erc721: false,
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b3595068778dd592e39a122f4f5a5cf09c90fe2.png',
+ identiconAddress: null,
+ image: 'images/contract/sushi.svg',
+ name: 'SushiSwap',
+ primaryLabel: 'SUSHI',
+ rawFiat: '',
+ rightPrimaryLabel: undefined,
+ rightSecondaryLabel: '',
+ secondaryLabel: 'SushiSwap',
+ symbol: 'SUSHI',
+ type: 'TOKEN',
+ });
+ });
+
+ it('when chainId === activeChainId and sorted by balance', () => {
+ const mockStore = createBridgeMockStore();
+ mockUseTokenTracker.mockReturnValue({
+ tokensWithBalances: [
+ {
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ balance: '0xa',
+ },
+ ],
+ });
+ const { result } = renderHookWithProvider(
+ () =>
+ useTokensWithFiltering(
+ MOCK_TOKEN_LIST_BY_ADDRESS,
+ MOCK_TOP_ASSETS,
+ TokenBucketPriority.owned,
+ TEST_CHAIN_ID,
+ ),
+ mockStore,
+ );
+
+ expect(result.current).toHaveLength(1);
+ expect(typeof result.current).toStrictEqual('function');
+ const tokenGenerator = result.current(() => true);
+ expect(tokenGenerator.next().value).toStrictEqual({
+ address: '0x0000000000000000000000000000000000000000',
+ balance: '0x0',
+ decimals: 18,
+ iconUrl: './images/eth_logo.svg',
+ identiconAddress: null,
+ image: './images/eth_logo.svg',
+ name: 'Ether',
+ primaryLabel: 'ETH',
+ rawFiat: '0',
+ rightPrimaryLabel: '0 ETH',
+ rightSecondaryLabel: '$0.00 USD',
+ secondaryLabel: 'Ether',
+ string: '0',
+ symbol: 'ETH',
+ type: 'NATIVE',
+ });
+ expect(tokenGenerator.next().value).toStrictEqual({
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ aggregators: [],
+ balance: '0xa',
+ decimals: 6,
+ erc20: true,
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png',
+ identiconAddress: null,
+ image: 'images/contract/usdt.svg',
+ name: 'Tether USD',
+ primaryLabel: 'USDT',
+ rawFiat: '',
+ rightPrimaryLabel: undefined,
+ rightSecondaryLabel: '',
+ secondaryLabel: 'Tether USD',
+ symbol: 'USDT',
+ type: 'TOKEN',
+ });
+ });
+});
diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts
new file mode 100644
index 000000000000..a7ff3f2513ac
--- /dev/null
+++ b/ui/hooks/useTokensWithFiltering.ts
@@ -0,0 +1,178 @@
+import { useCallback, useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { isEqual } from 'lodash';
+import { ChainId, hexToBN } from '@metamask/controller-utils';
+import { Hex } from '@metamask/utils';
+import {
+ getAllTokens,
+ getCurrentCurrency,
+ getSelectedInternalAccountWithBalance,
+ getShouldHideZeroBalanceTokens,
+ getTokenExchangeRates,
+} from '../selectors';
+import { getConversionRate } from '../ducks/metamask/metamask';
+import {
+ SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
+ SwapsTokenObject,
+ TokenBucketPriority,
+} from '../../shared/constants/swaps';
+import { getValueFromWeiHex } from '../../shared/modules/conversion.utils';
+import { EtherDenomination } from '../../shared/constants/common';
+import {
+ AssetWithDisplayData,
+ ERC20Asset,
+ NativeAsset,
+ TokenWithBalance,
+} from '../components/multichain/asset-picker-amount/asset-picker-modal/types';
+import { AssetType } from '../../shared/constants/transaction';
+import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils';
+import { useTokenTracker } from './useTokenTracker';
+import { getRenderableTokenData } from './useTokensToSearch';
+
+/*
+ * Returns a token list generator that filters and sorts tokens based on
+ * query match, balance/popularity, all other tokens
+ */
+export const useTokensWithFiltering = (
+ tokenList: Record,
+ topTokens: { address: string }[],
+ sortOrder: TokenBucketPriority = TokenBucketPriority.owned,
+ chainId?: ChainId | Hex,
+) => {
+ // Only includes non-native tokens
+ const allDetectedTokens = useSelector(getAllTokens);
+ const { address: selectedAddress, balance: balanceOnActiveChain } =
+ useSelector(getSelectedInternalAccountWithBalance);
+
+ const allDetectedTokensForChainAndAddress = chainId
+ ? allDetectedTokens?.[chainId]?.[selectedAddress] ?? []
+ : [];
+
+ const shouldHideZeroBalanceTokens = useSelector(
+ getShouldHideZeroBalanceTokens,
+ );
+ const {
+ tokensWithBalances: erc20TokensWithBalances,
+ }: { tokensWithBalances: TokenWithBalance[] } = useTokenTracker({
+ tokens: allDetectedTokensForChainAndAddress,
+ address: selectedAddress,
+ hideZeroBalanceTokens: Boolean(shouldHideZeroBalanceTokens),
+ });
+
+ const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
+ const conversionRate = useSelector(getConversionRate);
+ const currentCurrency = useSelector(getCurrentCurrency);
+
+ const sortedErc20TokensWithBalances = useMemo(
+ () =>
+ erc20TokensWithBalances.toSorted(
+ (a, b) => Number(b.string) - Number(a.string),
+ ),
+ [erc20TokensWithBalances],
+ );
+
+ const filteredTokenListGenerator = useCallback(
+ (shouldAddToken: (symbol: string, address?: string) => boolean) => {
+ const buildTokenData = (
+ token: SwapsTokenObject,
+ ):
+ | AssetWithDisplayData
+ | AssetWithDisplayData
+ | undefined => {
+ if (chainId && shouldAddToken(token.symbol, token.address)) {
+ return getRenderableTokenData(
+ {
+ ...token,
+ type: isSwapsDefaultTokenSymbol(token.symbol, chainId)
+ ? AssetType.native
+ : AssetType.token,
+ image: token.iconUrl,
+ },
+ tokenConversionRates,
+ conversionRate,
+ currentCurrency,
+ chainId,
+ tokenList,
+ );
+ }
+ return undefined;
+ };
+
+ return (function* (): Generator<
+ AssetWithDisplayData | AssetWithDisplayData
+ > {
+ const balance = hexToBN(balanceOnActiveChain);
+ const srcBalanceFields =
+ sortOrder === TokenBucketPriority.owned
+ ? {
+ balance: balanceOnActiveChain,
+ string: getValueFromWeiHex({
+ value: balance,
+ numberOfDecimals: 4,
+ toDenomination: EtherDenomination.ETH,
+ }),
+ }
+ : {};
+ const nativeToken = buildTokenData({
+ ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
+ chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
+ ],
+ ...srcBalanceFields,
+ });
+ if (nativeToken) {
+ yield nativeToken;
+ }
+
+ if (sortOrder === TokenBucketPriority.owned) {
+ for (const tokenWithBalance of sortedErc20TokensWithBalances) {
+ const cachedTokenData =
+ tokenWithBalance.address &&
+ tokenList &&
+ (tokenList[tokenWithBalance.address] ??
+ tokenList[tokenWithBalance.address.toLowerCase()]);
+ if (cachedTokenData) {
+ const combinedTokenData = buildTokenData({
+ ...tokenWithBalance,
+ ...(cachedTokenData ?? {}),
+ });
+ if (combinedTokenData) {
+ yield combinedTokenData;
+ }
+ }
+ }
+ }
+
+ for (const topToken of topTokens) {
+ const tokenListItem =
+ tokenList?.[topToken.address] ??
+ tokenList?.[topToken.address.toLowerCase()];
+ if (tokenListItem) {
+ const tokenWithTokenListData = buildTokenData(tokenListItem);
+ if (tokenWithTokenListData) {
+ yield tokenWithTokenListData;
+ }
+ }
+ }
+
+ for (const token of Object.values(tokenList)) {
+ const tokenWithTokenListData = buildTokenData(token);
+ if (tokenWithTokenListData) {
+ yield tokenWithTokenListData;
+ }
+ }
+ })();
+ },
+ [
+ balanceOnActiveChain,
+ sortedErc20TokensWithBalances,
+ topTokens,
+ tokenConversionRates,
+ conversionRate,
+ currentCurrency,
+ chainId,
+ tokenList,
+ ],
+ );
+
+ return filteredTokenListGenerator;
+};
diff --git a/ui/pages/bridge/__snapshots__/index.test.tsx.snap b/ui/pages/bridge/__snapshots__/index.test.tsx.snap
index 14514e59987d..cebca14e93bb 100644
--- a/ui/pages/bridge/__snapshots__/index.test.tsx.snap
+++ b/ui/pages/bridge/__snapshots__/index.test.tsx.snap
@@ -9,27 +9,61 @@ exports[`Bridge renders the component with initial props 1`] = `
class="bridge__container"
>
+
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss
index 99b8712cc63e..98a3a3ee5c34 100644
--- a/ui/pages/bridge/index.scss
+++ b/ui/pages/bridge/index.scss
@@ -1 +1,29 @@
@use "design-system";
+
+@import 'prepare/index';
+
+.bridge {
+ max-height: 100vh;
+ width: 360px;
+ position: relative;
+
+ &__container {
+ width: 100%;
+
+ .multichain-page-footer {
+ position: absolute;
+ width: 100%;
+ height: 80px;
+ bottom: 0;
+ padding: 16px;
+ display: flex;
+
+ button {
+ flex: 1;
+ height: 100%;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ }
+ }
+}
diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx
index e4b5c0b930d4..e81b20670011 100644
--- a/ui/pages/bridge/index.tsx
+++ b/ui/pages/bridge/index.tsx
@@ -1,9 +1,7 @@
-import React, { useContext } from 'react';
+import React, { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Switch, useHistory } from 'react-router-dom';
-import classnames from 'classnames';
import { I18nContext } from '../../contexts/i18n';
-
import { clearSwapsState } from '../../ducks/swaps/swaps';
import {
DEFAULT_ROUTE,
@@ -11,25 +9,24 @@ import {
PREPARE_SWAP_ROUTE,
CROSS_CHAIN_SWAP_ROUTE,
} from '../../helpers/constants/routes';
-
import { resetBackgroundSwapsState } from '../../store/actions';
-
import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route';
import {
- Box,
- Icon,
+ ButtonIcon,
+ ButtonIconSize,
IconName,
- IconSize,
} from '../../components/component-library';
-import {
- JustifyContent,
- IconColor,
- Display,
- BlockSize,
-} from '../../helpers/constants/design-system';
-import { getIsBridgeEnabled } from '../../selectors';
+import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors';
import useBridging from '../../hooks/bridge/useBridging';
-import { PrepareBridgePage } from './prepare/prepare-bridge-page';
+import {
+ Content,
+ Footer,
+ Header,
+} from '../../components/multichain/pages/page';
+import { getProviderConfig } from '../../ducks/metamask/metamask';
+import { resetInputFields, setFromChain } from '../../ducks/bridge/actions';
+import PrepareBridgePage from './prepare/prepare-bridge-page';
+import { BridgeCTAButton } from './prepare/bridge-cta-button';
const CrossChainSwap = () => {
const t = useContext(I18nContext);
@@ -40,6 +37,19 @@ const CrossChainSwap = () => {
const dispatch = useDispatch();
const isBridgeEnabled = useSelector(getIsBridgeEnabled);
+ const providerConfig = useSelector(getProviderConfig);
+ const isBridgeChain = useSelector(getIsBridgeChain);
+
+ useEffect(() => {
+ isBridgeChain &&
+ isBridgeEnabled &&
+ providerConfig &&
+ dispatch(setFromChain(providerConfig.chainId));
+
+ return () => {
+ dispatch(resetInputFields());
+ };
+ }, [isBridgeChain, isBridgeEnabled, providerConfig]);
const redirectToDefaultRoute = async () => {
history.push({
@@ -53,37 +63,27 @@ const CrossChainSwap = () => {
return (
-
-
) => {
- if (e.key === 'Enter') {
- redirectToDefaultRoute();
- }
- }}
- >
-
-
-
-
{t('bridge')}
-
-
+ }
>
+ {t('bridge')}
+
+
{
}}
/>
-
+
+
);
diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
new file mode 100644
index 000000000000..f225adec3b6d
--- /dev/null
+++ b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BridgeCTAButton should render the component's initial state 1`] = `
+
+
+
+`;
diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
new file mode 100644
index 000000000000..b406cafe0941
--- /dev/null
+++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
@@ -0,0 +1,456 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PrepareBridgePage should render the component, with initial state 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`PrepareBridgePage should render the component, with inputs set 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx
new file mode 100644
index 000000000000..5e42823c885b
--- /dev/null
+++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { renderWithProvider } from '../../../../test/jest';
+import configureStore from '../../../store/store';
+import { createBridgeMockStore } from '../../../../test/jest/mock-store';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import { BridgeCTAButton } from './bridge-cta-button';
+
+describe('BridgeCTAButton', () => {
+ it("should render the component's initial state", () => {
+ const mockStore = createBridgeMockStore(
+ {
+ srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
+ destNetworkAllowlist: [CHAIN_IDS.OPTIMISM],
+ },
+ { fromTokenInputValue: 1 },
+ );
+ const { container, getByText, getByRole } = renderWithProvider(
+ ,
+ configureStore(mockStore),
+ );
+
+ expect(container).toMatchSnapshot();
+
+ expect(getByText('Select token')).toBeInTheDocument();
+ expect(getByRole('button')).toBeDisabled();
+ });
+
+ it('should render the component when tx is submittable', () => {
+ const mockStore = createBridgeMockStore(
+ {
+ srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
+ destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
+ },
+ {
+ fromTokenInputValue: 1,
+ fromToken: 'ETH',
+ toToken: 'ETH',
+ toChainId: CHAIN_IDS.LINEA_MAINNET,
+ },
+ {},
+ );
+ const { getByText, getByRole } = renderWithProvider(
+ ,
+ configureStore(mockStore),
+ );
+
+ expect(getByText('Bridge')).toBeInTheDocument();
+ expect(getByRole('button')).not.toBeDisabled();
+ });
+});
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
new file mode 100644
index 000000000000..fedcf4d4606a
--- /dev/null
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -0,0 +1,49 @@
+import React, { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { Button } from '../../../components/component-library';
+import {
+ getFromAmount,
+ getFromChain,
+ getFromToken,
+ getToAmount,
+ getToChain,
+ getToToken,
+} from '../../../ducks/bridge/selectors';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+
+export const BridgeCTAButton = () => {
+ const t = useI18nContext();
+ const fromToken = useSelector(getFromToken);
+ const toToken = useSelector(getToToken);
+
+ const fromChain = useSelector(getFromChain);
+ const toChain = useSelector(getToChain);
+
+ const fromAmount = useSelector(getFromAmount);
+ const toAmount = useSelector(getToAmount);
+
+ const isTxSubmittable =
+ fromToken && toToken && fromChain && toChain && fromAmount && toAmount;
+
+ const label = useMemo(() => {
+ if (isTxSubmittable) {
+ return t('bridge');
+ }
+
+ return t('swapSelectToken');
+ }, [isTxSubmittable]);
+
+ return (
+
+ );
+};
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
new file mode 100644
index 000000000000..811310590c71
--- /dev/null
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import { Hex } from '@metamask/utils';
+import { SwapsTokenObject } from '../../../../shared/constants/swaps';
+import {
+ Box,
+ Text,
+ TextField,
+ TextFieldType,
+} from '../../../components/component-library';
+import { AssetPicker } from '../../../components/multichain/asset-picker-amount/asset-picker';
+import { TabName } from '../../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs';
+import CurrencyDisplay from '../../../components/ui/currency-display';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
+import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
+import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils';
+import Tooltip from '../../../components/ui/tooltip';
+import { SwapsEthToken } from '../../../selectors';
+import {
+ ERC20Asset,
+ NativeAsset,
+} from '../../../components/multichain/asset-picker-amount/asset-picker-modal/types';
+import { zeroAddress } from '../../../__mocks__/ethereumjs-util';
+import { AssetType } from '../../../../shared/constants/transaction';
+import {
+ CHAIN_ID_TO_CURRENCY_SYMBOL_MAP,
+ CHAIN_ID_TOKEN_IMAGE_MAP,
+} from '../../../../shared/constants/network';
+import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
+
+const generateAssetFromToken = (
+ chainId: Hex,
+ tokenDetails: SwapsTokenObject | SwapsEthToken,
+): ERC20Asset | NativeAsset => {
+ if ('iconUrl' in tokenDetails && tokenDetails.address !== zeroAddress()) {
+ return {
+ type: AssetType.token,
+ image: tokenDetails.iconUrl,
+ symbol: tokenDetails.symbol,
+ address: tokenDetails.address,
+ };
+ }
+
+ return {
+ type: AssetType.native,
+ image:
+ CHAIN_ID_TOKEN_IMAGE_MAP[
+ chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP
+ ],
+ symbol:
+ CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[
+ chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP
+ ],
+ };
+};
+
+export const BridgeInputGroup = ({
+ className,
+ header,
+ token,
+ onAssetChange,
+ onAmountChange,
+ networkProps,
+ customTokenListGenerator,
+ amountFieldProps = {},
+}: {
+ className: string;
+ onAmountChange?: (value: string) => void;
+ token: SwapsTokenObject | SwapsEthToken | null;
+ amountFieldProps?: Pick<
+ React.ComponentProps,
+ 'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled'
+ >;
+} & Pick<
+ React.ComponentProps,
+ 'networkProps' | 'header' | 'customTokenListGenerator' | 'onAssetChange'
+>) => {
+ const t = useI18nContext();
+
+ const tokenFiatValue = useTokenFiatAmount(
+ token?.address || undefined,
+ amountFieldProps?.value?.toString() || '0x0',
+ token?.symbol,
+ {
+ showFiat: true,
+ },
+ true,
+ );
+ const ethFiatValue = useEthFiatAmount(
+ amountFieldProps?.value?.toString() || '0x0',
+ { showFiat: true },
+ true,
+ );
+
+ const { formattedBalance } = useLatestBalance(
+ token,
+ networkProps?.network?.chainId,
+ );
+
+ return (
+
+
+
+
+ {
+ onAmountChange?.(e.target.value);
+ }}
+ {...amountFieldProps}
+ />
+
+
+
+
+ {formattedBalance ? `${t('balance')}: ${formattedBalance}` : ' '}
+
+
+
+
+ );
+};
diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss
new file mode 100644
index 000000000000..1fa416df727f
--- /dev/null
+++ b/ui/pages/bridge/prepare/index.scss
@@ -0,0 +1,170 @@
+@use "design-system";
+
+.tokens-main-view-modal {
+ .multichain-asset-picker-list-item .mm-badge-wrapper__badge-container {
+ display: none;
+ }
+}
+
+.prepare-bridge-page {
+ display: flex;
+ flex-flow: column;
+ flex: 1;
+ width: 100%;
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ padding: 16px 0 16px 0;
+ border-radius: 8px;
+ border: 1px solid var(--color-border-muted);
+ }
+
+ &__from,
+ &__to {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ justify-content: center;
+ padding: 8px 16px 8px 16px;
+ }
+
+ &__input-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 298px;
+ gap: 16px;
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ display: none;
+ }
+
+ .mm-text-field {
+ background-color: inherit;
+
+ &--focused {
+ outline: none;
+ }
+ }
+ }
+
+ &__amounts-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 298px;
+ height: 22px;
+
+ p,
+ span {
+ color: var(--color-text-alternative);
+ font-size: 12px;
+ }
+ }
+
+ .asset-picker {
+ border: 1px solid var(--color-border-muted);
+ height: 40px;
+ min-width: fit-content;
+ max-width: fit-content;
+ background-color: inherit;
+
+ p {
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .mm-avatar-token {
+ height: 24px;
+ width: 24px;
+ border: 1px solid var(--color-border-muted);
+ }
+
+ .mm-badge-wrapper__badge-container .mm-avatar-base {
+ height: 10px;
+ width: 10px;
+ border: none;
+ }
+ }
+
+ .amount-input {
+ border: none;
+
+ input {
+ text-align: right;
+ padding-right: 0;
+ font-size: 24px;
+ font-weight: 700;
+
+ &:focus,
+ &:focus-visible {
+ outline: none;
+ }
+ }
+
+ .mm-text-field--focused {
+ outline: none;
+ }
+ }
+
+ &__switch-tokens {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &::before,
+ &::after {
+ content: '';
+ border-top: 1px solid var(--color-border-muted);
+ flex-grow: 1;
+ }
+
+ button {
+ border-radius: 50%;
+ padding: 10px;
+ border: 1px solid var(--color-border-muted);
+ transition: all 0.3s ease-in-out;
+ cursor: pointer;
+ width: 40px;
+ height: 40px;
+
+ &:hover:enabled {
+ background: var(--color-background-default-hover);
+
+ .mm-icon {
+ color: var(--color-icon-default);
+ }
+ }
+
+ &:active {
+ background: var(--color-background-default-pressed);
+
+ .mm-icon {
+ color: var(--color-icon-default);
+ }
+ }
+
+ .rotate {
+ transform: rotate(360deg);
+ }
+ }
+
+ .mm-icon {
+ color: var(--color-icon-alternative);
+ transition: all 0.3s ease-in-out;
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+ }
+}
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
new file mode 100644
index 000000000000..82441aad218d
--- /dev/null
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { act } from '@testing-library/react';
+import { fireEvent, renderWithProvider } from '../../../../test/jest';
+import configureStore from '../../../store/store';
+import { createBridgeMockStore } from '../../../../test/jest/mock-store';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import { createTestProviderTools } from '../../../../test/stub/provider';
+import PrepareBridgePage from './prepare-bridge-page';
+
+describe('PrepareBridgePage', () => {
+ beforeAll(() => {
+ const { provider } = createTestProviderTools({
+ networkId: 'Ethereum',
+ chainId: CHAIN_IDS.MAINNET,
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ global.ethereumProvider = provider as any;
+ });
+
+ it('should render the component, with initial state', async () => {
+ const mockStore = createBridgeMockStore(
+ {
+ srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
+ destNetworkAllowlist: [CHAIN_IDS.OPTIMISM],
+ },
+ {},
+ );
+ const { container, getByRole, getByTestId } = renderWithProvider(
+ ,
+ configureStore(mockStore),
+ );
+
+ expect(container).toMatchSnapshot();
+
+ expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument();
+ expect(getByRole('button', { name: /Select token/u })).toBeInTheDocument();
+
+ expect(getByTestId('from-amount')).toBeInTheDocument();
+ expect(getByTestId('from-amount').closest('input')).not.toBeDisabled();
+ await act(() => {
+ fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } });
+ });
+ expect(getByTestId('from-amount').closest('input')).toHaveValue(2);
+
+ expect(getByTestId('to-amount')).toBeInTheDocument();
+ expect(getByTestId('to-amount').closest('input')).toBeDisabled();
+
+ expect(getByTestId('switch-tokens').closest('button')).toBeDisabled();
+ });
+
+ it('should render the component, with inputs set', async () => {
+ const mockStore = createBridgeMockStore(
+ {
+ srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
+ destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
+ },
+ {
+ fromTokenInputValue: 1,
+ fromToken: { address: '0x3103910', decimals: 6 },
+ toToken: {
+ iconUrl: 'http://url',
+ symbol: 'UNI',
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ decimals: 6,
+ },
+ toChainId: CHAIN_IDS.LINEA_MAINNET,
+ },
+ {},
+ );
+ const { container, getByRole, getByTestId } = renderWithProvider(
+ ,
+ configureStore(mockStore),
+ );
+
+ expect(container).toMatchSnapshot();
+
+ expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument();
+ expect(getByRole('button', { name: /UNI/u })).toBeInTheDocument();
+
+ expect(getByTestId('from-amount')).toBeInTheDocument();
+ expect(getByTestId('from-amount').closest('input')).not.toBeDisabled();
+ expect(getByTestId('from-amount').closest('input')).toHaveValue(1);
+ await act(() => {
+ fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } });
+ });
+ expect(getByTestId('from-amount').closest('input')).toHaveValue(2);
+
+ expect(getByTestId('to-amount')).toBeInTheDocument();
+ expect(getByTestId('to-amount').closest('input')).toBeDisabled();
+
+ expect(getByTestId('switch-tokens').closest('button')).not.toBeDisabled();
+ });
+
+ it('should throw an error if token decimals are not defined', async () => {
+ const mockStore = createBridgeMockStore(
+ {
+ srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
+ destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
+ },
+ {
+ fromTokenInputValue: 1,
+ fromToken: { address: '0x3103910' },
+ toToken: {
+ iconUrl: 'http://url',
+ symbol: 'UNI',
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ },
+ toChainId: CHAIN_IDS.LINEA_MAINNET,
+ },
+ {},
+ );
+
+ expect(() =>
+ renderWithProvider(, configureStore(mockStore)),
+ ).toThrow();
+ });
+});
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 6c66499ac9b9..2fdb11289c5b 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -1,24 +1,163 @@
-import React from 'react';
-import { useSelector, shallowEqual } from 'react-redux';
-import { isEqual, shuffle } from 'lodash';
-import PrepareSwapPage from '../../swaps/prepare-swap-page/prepare-swap-page';
-import { getSelectedAccount, getTokenList } from '../../../selectors';
+import React, { useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import classnames from 'classnames';
+import {
+ setFromChain,
+ setFromToken,
+ setFromTokenInputValue,
+ setToChain,
+ setToToken,
+ switchToAndFromTokens,
+} from '../../../ducks/bridge/actions';
+import {
+ getFromAmount,
+ getFromChain,
+ getFromChains,
+ getFromToken,
+ getFromTokens,
+ getFromTopAssets,
+ getToAmount,
+ getToChain,
+ getToChains,
+ getToToken,
+ getToTokens,
+ getToTopAssets,
+} from '../../../ducks/bridge/selectors';
+import {
+ Box,
+ ButtonIcon,
+ IconName,
+} from '../../../components/component-library';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { TokenBucketPriority } from '../../../../shared/constants/swaps';
+import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering';
+import { setActiveNetwork } from '../../../store/actions';
+import { BlockSize } from '../../../helpers/constants/design-system';
+import { BridgeInputGroup } from './bridge-input-group';
-export const PrepareBridgePage = () => {
- const selectedAccount = useSelector(getSelectedAccount, shallowEqual);
- const { balance: ethBalance, address: selectedAccountAddress } =
- selectedAccount;
+const PrepareBridgePage = () => {
+ const dispatch = useDispatch();
- const tokenList = useSelector(getTokenList, isEqual);
- const shuffledTokensList = shuffle(Object.values(tokenList));
+ const t = useI18nContext();
+
+ const fromToken = useSelector(getFromToken);
+ const fromTokens = useSelector(getFromTokens);
+ const fromTopAssets = useSelector(getFromTopAssets);
+
+ const toToken = useSelector(getToToken);
+ const toTokens = useSelector(getToTokens);
+ const toTopAssets = useSelector(getToTopAssets);
+
+ const fromChains = useSelector(getFromChains);
+ const toChains = useSelector(getToChains);
+ const fromChain = useSelector(getFromChain);
+ const toChain = useSelector(getToChain);
+
+ const fromAmount = useSelector(getFromAmount);
+ const toAmount = useSelector(getToAmount);
+
+ const fromTokenListGenerator = useTokensWithFiltering(
+ fromTokens,
+ fromTopAssets,
+ TokenBucketPriority.owned,
+ fromChain?.chainId,
+ );
+ const toTokenListGenerator = useTokensWithFiltering(
+ toTokens,
+ toTopAssets,
+ TokenBucketPriority.top,
+ toChain?.chainId,
+ );
+
+ const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false);
return (
-
-
+
+
+ {
+ dispatch(setFromTokenInputValue(e));
+ }}
+ onAssetChange={(token) => dispatch(setFromToken(token))}
+ networkProps={{
+ network: fromChain,
+ networks: fromChains,
+ onNetworkChange: (networkConfig) => {
+ dispatch(
+ setActiveNetwork(
+ networkConfig.rpcEndpoints[
+ networkConfig.defaultRpcEndpointIndex
+ ].networkClientId,
+ ),
+ );
+ dispatch(setFromChain(networkConfig.chainId));
+ },
+ }}
+ customTokenListGenerator={
+ fromTokens && fromTopAssets ? fromTokenListGenerator : undefined
+ }
+ amountFieldProps={{
+ testId: 'from-amount',
+ autoFocus: true,
+ value: fromAmount || undefined,
+ }}
+ />
+
+
+ {
+ setRotateSwitchTokens(!rotateSwitchTokens);
+ const toChainClientId =
+ toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints
+ ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex]
+ .networkClientId
+ : undefined;
+ toChainClientId && dispatch(setActiveNetwork(toChainClientId));
+ dispatch(switchToAndFromTokens({ fromChain }));
+ }}
+ />
+
+
+ dispatch(setToToken(token))}
+ networkProps={{
+ network: toChain,
+ networks: toChains,
+ onNetworkChange: (networkConfig) => {
+ dispatch(setToChain(networkConfig.chainId));
+ },
+ }}
+ customTokenListGenerator={
+ toChain && toTokens && toTopAssets
+ ? toTokenListGenerator
+ : fromTokenListGenerator
+ }
+ amountFieldProps={{
+ testId: 'to-amount',
+ readOnly: true,
+ disabled: true,
+ value: toAmount,
+ }}
+ />
+
);
};
+
+export default PrepareBridgePage;
diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx
index 6e0d2627d4aa..0b8585fd0b5c 100644
--- a/ui/pages/create-account/connect-hardware/index.test.tsx
+++ b/ui/pages/create-account/connect-hardware/index.test.tsx
@@ -30,6 +30,10 @@ jest.mock('../../../selectors', () => ({
},
}));
+jest.mock('../../../ducks/bridge/selectors', () => ({
+ getAllBridgeableNetworks: () => [],
+}));
+
const MOCK_RECENT_PAGE = '/home';
jest.mock('../../../ducks/history/history', () => ({
getMostRecentOverviewPage: jest
diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss
index 9f86ad6f6e5a..378622a3994a 100644
--- a/ui/pages/pages.scss
+++ b/ui/pages/pages.scss
@@ -28,4 +28,5 @@
@import 'create-snap-account/index';
@import 'remove-snap-account/index';
@import 'swaps/index';
+@import 'bridge/index';
@import 'unlock-page/index';
diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js
index a27d59e3b33b..d59c4b29e0c1 100644
--- a/ui/pages/routes/routes.component.js
+++ b/ui/pages/routes/routes.component.js
@@ -500,6 +500,17 @@ export default class Routes extends Component {
hideAppHeader() {
const { location } = this.props;
+ const isCrossChainSwapsPage = Boolean(
+ matchPath(location.pathname, {
+ path: `${CROSS_CHAIN_SWAP_ROUTE}`,
+ exact: false,
+ }),
+ );
+
+ if (isCrossChainSwapsPage) {
+ return true;
+ }
+
const isNotificationsPage = Boolean(
matchPath(location.pathname, {
path: `${NOTIFICATIONS_ROUTE}`,
From 526d3ecd02528fb3978c6b113170c64d2dd2f632 Mon Sep 17 00:00:00 2001
From: Nidhi Kumari
Date: Wed, 16 Oct 2024 17:40:18 +0100
Subject: [PATCH 12/51] fix: updated edit modals (#27623)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR is to update edit modals followed by design QA
## **Description**
Following changes have been made in this PR:
1. The update CTA is now fixed/pinned to the bottom
2. The warning message while clicking on disconnect has been updated:
- Simple the copy to This will disconnect you from this site
- Increase Icon size from 12px to 16px
- Center align warning message with the button
NOTE: Add new accounts will be handled in a separate PR
Including RPC URL is a new change proposed so I will update that in a
different PR as well
## **Related issues**
Fixes:
[https://github.com/MetaMask/MetaMask-planning/issues/3390](https://github.com/MetaMask/MetaMask-planning/issues/3390)
## **Manual testing steps**
1. Run extension with yarn start
2. Connect to dapp
3. Click on All Permissions and the go to individual permission page
4. Click on Edit Accounts Modal and observe the above UI changes
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/8d3c95e8-5f49-486d-9d70-69e50339fe43
### **After**
https://github.com/user-attachments/assets/9f48b909-8c3a-4435-9a38-99b71e05e5d2
## **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/en/messages.json | 3 +-
.../edit-accounts-modal.test.tsx | 1 -
.../edit-accounts-modal.tsx | 281 +++++++++---------
.../multichain/edit-accounts-modal/index.scss | 5 +
.../edit-networks-modal.js | 96 +++---
.../multichain/edit-networks-modal/index.scss | 5 +
.../multichain/multichain-components.scss | 2 +
.../review-permissions-page.tsx | 1 -
.../site-cell/site-cell.tsx | 4 -
.../connect-page/connect-page.tsx | 2 -
10 files changed, 198 insertions(+), 202 deletions(-)
create mode 100644 ui/components/multichain/edit-accounts-modal/index.scss
create mode 100644 ui/components/multichain/edit-networks-modal/index.scss
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 6e907c35a088..bb10d6f579a0 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -1646,8 +1646,7 @@
"message": "Snaps"
},
"disconnectMessage": {
- "message": "This will disconnect you from $1",
- "description": "$1 is the name of the dapp"
+ "message": "This will disconnect you from this site"
},
"disconnectPrompt": {
"message": "Disconnect $1"
diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx
index b00e3dc39c3e..65d29bfec036 100644
--- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx
+++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx
@@ -44,7 +44,6 @@ const render = (
,
store,
diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx
index 084596f07afb..ddc2749e2ee7 100644
--- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx
+++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx
@@ -27,8 +27,8 @@ import {
IconColor,
FlexDirection,
AlignItems,
+ BlockSize,
} from '../../../helpers/constants/design-system';
-import { getURLHost } from '../../../helpers/utils/util';
import { MergedInternalAccount } from '../../../selectors/selectors.types';
import {
MetaMetricsEventCategory,
@@ -38,7 +38,6 @@ import { MetaMetricsContext } from '../../../contexts/metametrics';
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
type EditAccountsModalProps = {
- activeTabOrigin: string;
accounts: MergedInternalAccount[];
defaultSelectedAccountAddresses: string[];
onClose: () => void;
@@ -46,7 +45,6 @@ type EditAccountsModalProps = {
};
export const EditAccountsModal: React.FC = ({
- activeTabOrigin,
accounts,
defaultSelectedAccountAddresses,
onClose,
@@ -56,7 +54,6 @@ export const EditAccountsModal: React.FC = ({
const trackEvent = useContext(MetaMetricsContext);
const [showAddNewAccounts, setShowAddNewAccounts] = useState(false);
-
const [selectedAccountAddresses, setSelectedAccountAddresses] = useState(
defaultSelectedAccountAddresses,
);
@@ -84,155 +81,151 @@ export const EditAccountsModal: React.FC = ({
}
};
- const allAreSelected = () => {
- return accounts.length === selectedAccountAddresses.length;
- };
-
+ const allAreSelected = () =>
+ accounts.length === selectedAccountAddresses.length;
const checked = allAreSelected();
const isIndeterminate = !checked && selectedAccountAddresses.length > 0;
- const hostName = getURLHost(activeTabOrigin);
-
const defaultSet = new Set(defaultSelectedAccountAddresses);
const selectedSet = new Set(selectedAccountAddresses);
return (
- <>
-
-
-
- {t('editAccounts')}
-
- {showAddNewAccounts ? (
-
- setShowAddNewAccounts(false)}
+
+
+
+ {t('editAccounts')}
+
+ {showAddNewAccounts ? (
+
+ setShowAddNewAccounts(false)}
+ />
+
+ ) : (
+ <>
+
+
+ allAreSelected() ? deselectAll() : selectAll()
+ }
+ isIndeterminate={isIndeterminate}
/>
+ setShowAddNewAccounts(true)}>
+ {t('newAccount')}
+
- ) : (
- <>
-
-
- allAreSelected() ? deselectAll() : selectAll()
- }
- isIndeterminate={isIndeterminate}
- />
- setShowAddNewAccounts(true)}>
- {t('newAccount')}
-
-
- {accounts.map((account) => (
- handleAccountClick(account.address)}
- account={account}
- key={account.address}
- isPinned={Boolean(account.pinned)}
- startAccessory={
-
- isEqualCaseInsensitive(
- selectedAccountAddress,
- account.address,
- ),
- )}
- />
- }
- selected={false}
- />
- ))}
-
-
- {selectedAccountAddresses.length === 0 ? (
-
-
-
-
- {t('disconnectMessage', [hostName])}
-
-
- {
- onSubmit([]);
- onClose();
- }}
- size={ButtonPrimarySize.Lg}
- block
- danger
- >
- {t('disconnect')}
-
-
- ) : (
- {
- // Get accounts that are in `selectedAccountAddresses` but not in `defaultSelectedAccountAddresses`
- const addedAccounts = selectedAccountAddresses.filter(
- (address) => !defaultSet.has(address),
- );
- // Get accounts that are in `defaultSelectedAccountAddresses` but not in `selectedAccountAddresses`
- const removedAccounts =
- defaultSelectedAccountAddresses.filter(
- (address) => !selectedSet.has(address),
- );
-
- onSubmit(selectedAccountAddresses);
- trackEvent({
- category: MetaMetricsEventCategory.Permissions,
- event:
- MetaMetricsEventName.UpdatePermissionedAccounts,
- properties: {
- addedAccounts: addedAccounts.length,
- removedAccounts: removedAccounts.length,
- location: 'Edit Accounts Modal',
- },
- });
-
- onClose();
- }}
- size={ButtonPrimarySize.Lg}
- block
- >
- {t('update')}
-
- )}
-
- >
- )}
-
-
-
- >
+ {accounts.map((account) => (
+ handleAccountClick(account.address)}
+ account={account}
+ key={account.address}
+ isPinned={Boolean(account.pinned)}
+ startAccessory={
+
+ isEqualCaseInsensitive(
+ selectedAccountAddress,
+ account.address,
+ ),
+ )}
+ />
+ }
+ selected={false}
+ />
+ ))}
+ >
+ )}
+
+
+
+ {selectedAccountAddresses.length === 0 ? (
+
+
+
+
+ {t('disconnectMessage')}
+
+
+ {
+ onSubmit([]);
+ onClose();
+ }}
+ size={ButtonPrimarySize.Lg}
+ block
+ danger
+ >
+ {t('disconnect')}
+
+
+ ) : (
+ {
+ const addedAccounts = selectedAccountAddresses.filter(
+ (address) => !defaultSet.has(address),
+ );
+ const removedAccounts = defaultSelectedAccountAddresses.filter(
+ (address) => !selectedSet.has(address),
+ );
+
+ onSubmit(selectedAccountAddresses);
+ trackEvent({
+ category: MetaMetricsEventCategory.Permissions,
+ event: MetaMetricsEventName.UpdatePermissionedAccounts,
+ properties: {
+ addedAccounts: addedAccounts.length,
+ removedAccounts: removedAccounts.length,
+ location: 'Edit Accounts Modal',
+ },
+ });
+
+ onClose();
+ }}
+ size={ButtonPrimarySize.Lg}
+ block
+ >
+ {t('update')}
+
+ )}
+
+
+
);
};
diff --git a/ui/components/multichain/edit-accounts-modal/index.scss b/ui/components/multichain/edit-accounts-modal/index.scss
new file mode 100644
index 000000000000..887b8afb8183
--- /dev/null
+++ b/ui/components/multichain/edit-accounts-modal/index.scss
@@ -0,0 +1,5 @@
+.edit-accounts-modal {
+ &__body {
+ overflow: auto;
+ }
+}
diff --git a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js
index e4a7c391b4df..0b86716af50a 100644
--- a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js
+++ b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js
@@ -2,9 +2,11 @@ import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
AlignItems,
+ BlockSize,
Display,
FlexDirection,
IconColor,
+ JustifyContent,
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
@@ -26,7 +28,6 @@ import {
IconSize,
} from '../../component-library';
import { NetworkListItem } from '..';
-import { getURLHost } from '../../../helpers/utils/util';
import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network';
import {
MetaMetricsEventCategory,
@@ -35,7 +36,6 @@ import {
import { MetaMetricsContext } from '../../../contexts/metametrics';
export const EditNetworksModal = ({
- activeTabOrigin,
nonTestNetworks,
testNetworks,
defaultSelectedChainIds,
@@ -80,8 +80,6 @@ export const EditNetworksModal = ({
const checked = allAreSelected();
const isIndeterminate = !checked && selectedChainIds.length > 0;
- const hostName = getURLHost(activeTabOrigin);
-
const defaultChainIdsSet = new Set(defaultSelectedChainIds);
const selectedChainIdsSet = new Set(selectedChainIds);
@@ -102,7 +100,11 @@ export const EditNetworksModal = ({
>
{t('editNetworksTitle')}
-
+
))}
-
- {selectedChainIds.length === 0 ? (
+
+
+ {selectedChainIds.length === 0 ? (
+
-
-
-
- {t('disconnectMessage', [hostName])}
-
-
- {
- onSubmit([]);
- onClose();
- }}
- size={ButtonPrimarySize.Lg}
- block
- danger
+
+
- {t('disconnect')}
-
+ {t('disconnectMessage')}
+
- ) : (
{
onSubmit(selectedChainIds);
// Get networks that are in `selectedChainIds` but not in `defaultSelectedChainIds`
@@ -211,23 +203,31 @@ export const EditNetworksModal = ({
}}
size={ButtonPrimarySize.Lg}
block
+ danger
>
- {t('update')}
+ {t('disconnect')}
- )}
-
-
+
+ ) : (
+ {
+ onSubmit(selectedChainIds);
+ onClose();
+ }}
+ size={ButtonPrimarySize.Lg}
+ block
+ >
+ {t('update')}
+
+ )}
+
);
};
EditNetworksModal.propTypes = {
- /**
- * Origin for the active tab.
- */
- activeTabOrigin: PropTypes.string,
-
/**
* Array of network objects representing available non-test networks to choose from.
*/
diff --git a/ui/components/multichain/edit-networks-modal/index.scss b/ui/components/multichain/edit-networks-modal/index.scss
new file mode 100644
index 000000000000..113351b8cd2e
--- /dev/null
+++ b/ui/components/multichain/edit-networks-modal/index.scss
@@ -0,0 +1,5 @@
+.edit-networks-modal {
+ &__body {
+ overflow: auto;
+ }
+}
diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss
index 2d2d6b3fdef0..bf3191c7e994 100644
--- a/ui/components/multichain/multichain-components.scss
+++ b/ui/components/multichain/multichain-components.scss
@@ -19,6 +19,8 @@
@import 'connected-site-menu';
@import 'create-named-snap-account';
@import 'dropdown-editor';
+@import "edit-accounts-modal";
+@import "edit-networks-modal";
@import 'token-list-item';
@import 'network-list-item';
@import 'network-list-item-menu';
diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx
index b12fea776c65..66e7cadd7546 100644
--- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx
+++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx
@@ -195,7 +195,6 @@ export const ReviewPermissions = () => {
onSelectChainIds={handleSelectChainIds}
selectedAccountAddresses={connectedAccountAddresses}
selectedChainIds={connectedChainIds}
- activeTabOrigin={activeTabOrigin}
/>
) : (
diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx
index 562d3e8c7d2e..ae7a93283ead 100644
--- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx
+++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx
@@ -37,7 +37,6 @@ type SiteCellProps = {
onSelectChainIds: (chainIds: Hex[]) => void;
selectedAccountAddresses: string[];
selectedChainIds: string[];
- activeTabOrigin: string;
isConnectFlow?: boolean;
};
@@ -49,7 +48,6 @@ export const SiteCell: React.FC = ({
onSelectChainIds,
selectedAccountAddresses,
selectedChainIds,
- activeTabOrigin,
isConnectFlow,
}) => {
const t = useI18nContext();
@@ -148,7 +146,6 @@ export const SiteCell: React.FC = ({
{showEditAccountsModal && (
setShowEditAccountsModal(false)}
@@ -158,7 +155,6 @@ export const SiteCell: React.FC = ({
{showEditNetworksModal && (
= ({
permissionsRequestId,
rejectPermissionsRequest,
approveConnection,
- activeTabOrigin,
}) => {
const t = useI18nContext();
@@ -146,7 +145,6 @@ export const ConnectPage: React.FC = ({
onSelectChainIds={setSelectedChainIds}
selectedAccountAddresses={selectedAccountAddresses}
selectedChainIds={selectedChainIds}
- activeTabOrigin={activeTabOrigin}
isConnectFlow
/>
From 601b5fabea723404b6043a19b0176be681d5ca05 Mon Sep 17 00:00:00 2001
From: David Walsh
Date: Wed, 16 Oct 2024 11:45:57 -0500
Subject: [PATCH 13/51] fix: Onboarding: Code style nits (#27767)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Just doing some code style nits for the onboarding flow
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27767?quickstart=1)
## **Related issues**
Fixes: N/A
## **Manual testing steps**
1. Get to the end of onboarding
2. Try out the advance configuration
3. Ensure all screens toggle as they should.
## **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.
---
.../privacy-settings/privacy-settings.js | 26 ++++++++++++-------
1 file changed, 16 insertions(+), 10 deletions(-)
diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js
index ee11f63caf2a..aed08f196957 100644
--- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js
+++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js
@@ -1,6 +1,7 @@
import React, { useContext, useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
+import classnames from 'classnames';
import { ButtonVariant } from '@metamask/snaps-sdk';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
@@ -78,6 +79,8 @@ import {
} from '../../../../shared/constants/network';
import { Setting } from './setting';
+const ANIMATION_TIME = 500;
+
/**
* Profile Syncing Setting props
*
@@ -232,14 +235,14 @@ export default function PrivacySettings() {
setTimeout(() => {
setHiddenClass(false);
- }, 500);
+ }, ANIMATION_TIME);
};
const handleBack = () => {
setShowDetail(false);
setTimeout(() => {
setHiddenClass(true);
- }, 500);
+ }, ANIMATION_TIME);
};
const items = [
@@ -252,7 +255,10 @@ export default function PrivacySettings() {
<>
- {selectedItem && selectedItem.title}
+ {selectedItem?.title}
@@ -397,7 +403,7 @@ export default function PrivacySettings() {
className="privacy-settings__settings"
data-testid="privacy-settings-settings"
>
- {selectedItem && selectedItem.id === 1 ? (
+ {selectedItem?.id === 1 ? (
<>
>
) : null}
- {selectedItem && selectedItem.id === 2 ? (
+ {selectedItem?.id === 2 ? (
<>
>
) : null}
- {selectedItem && selectedItem.id === 3 ? (
+ {selectedItem?.id === 3 ? (
<>
Date: Wed, 16 Oct 2024 19:16:34 +0200
Subject: [PATCH 14/51] chore: update @metamask/bitcoin-wallet-snap to 0.7.0
(#27730)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Bump the BTC Snap to version 0.7.0.
- This new release uses a new node provider (QuickNode)
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26701?quickstart=1)
## **Related issues**
N/A
## **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.
---
package.json | 2 +-
privacy-snapshot.json | 1 +
test/e2e/constants.ts | 3 ++
.../flask/btc/btc-account-overview.spec.ts | 21 ++++++++++
test/e2e/flask/btc/common-btc.ts | 38 ++++++++++---------
test/e2e/mock-e2e.js | 1 +
yarn.lock | 10 ++---
7 files changed, 52 insertions(+), 24 deletions(-)
diff --git a/package.json b/package.json
index 9a77bb273995..fe2f43f63c9a 100644
--- a/package.json
+++ b/package.json
@@ -303,7 +303,7 @@
"@metamask/approval-controller": "^7.0.0",
"@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch",
"@metamask/base-controller": "^7.0.0",
- "@metamask/bitcoin-wallet-snap": "^0.6.1",
+ "@metamask/bitcoin-wallet-snap": "^0.7.0",
"@metamask/browser-passworder": "^4.3.0",
"@metamask/contract-metadata": "^2.5.0",
"@metamask/controller-utils": "^11.2.0",
diff --git a/privacy-snapshot.json b/privacy-snapshot.json
index 37a05025382d..41b04a9b5210 100644
--- a/privacy-snapshot.json
+++ b/privacy-snapshot.json
@@ -1,4 +1,5 @@
[
+ "*.btc*.quiknode.pro",
"accounts.api.cx.metamask.io",
"acl.execution.metamask.io",
"api.blockchair.com",
diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts
index 7e92a28cf463..c3957cb6fbbf 100644
--- a/test/e2e/constants.ts
+++ b/test/e2e/constants.ts
@@ -46,3 +46,6 @@ export const DAPP_ONE_URL = 'http://127.0.0.1:8081';
/* Default BTC address created using test SRP */
export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252';
+
+/* Default (mocked) BTC balance used by the Bitcoin RPC provider */
+export const DEFAULT_BTC_BALANCE = 1; // BTC
diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts
index 5f0277c191de..24eedb60b6a2 100644
--- a/test/e2e/flask/btc/btc-account-overview.spec.ts
+++ b/test/e2e/flask/btc/btc-account-overview.spec.ts
@@ -1,4 +1,6 @@
+import { strict as assert } from 'assert';
import { Suite } from 'mocha';
+import { DEFAULT_BTC_BALANCE } from '../../constants';
import { withBtcAccountSnap } from './common-btc';
describe('BTC Account - Overview', function (this: Suite) {
@@ -39,4 +41,23 @@ describe('BTC Account - Overview', function (this: Suite) {
},
);
});
+
+ it('has balance', async function () {
+ await withBtcAccountSnap(
+ { title: this.test?.fullTitle() },
+ async (driver) => {
+ // Wait for the balance to load up
+ await driver.delay(2000);
+
+ const balanceElement = await driver.findElement(
+ '.coin-overview__balance',
+ );
+ const balanceText = await balanceElement.getText();
+
+ const [balance, unit] = balanceText.split('\n');
+ assert(Number(balance) === DEFAULT_BTC_BALANCE);
+ assert(unit === 'BTC');
+ },
+ );
+ });
});
diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts
index a33ab1241a1c..15bf7d49eb0b 100644
--- a/test/e2e/flask/btc/common-btc.ts
+++ b/test/e2e/flask/btc/common-btc.ts
@@ -1,34 +1,36 @@
import { Mockttp } from 'mockttp';
import FixtureBuilder from '../../fixture-builder';
import { withFixtures, unlockWallet } from '../../helpers';
-import { DEFAULT_BTC_ACCOUNT } from '../../constants';
+import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants';
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
import { Driver } from '../../webdriver/driver';
import { createBtcAccount } from '../../accounts/common';
-const GENERATE_MOCK_BTC_BALANCE_CALL = (
- address: string = DEFAULT_BTC_ACCOUNT,
-): { data: { [address: string]: number } } => {
- return {
- data: {
- [address]: 9999,
- },
- };
-};
-
export async function mockBtcBalanceQuote(
mockServer: Mockttp,
address: string = DEFAULT_BTC_ACCOUNT,
) {
return await mockServer
- .forGet(/https:\/\/api\.blockchair\.com\/bitcoin\/addresses\/balances/u)
- .withQuery({
- addresses: address,
+ .forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u)
+ .withJsonBodyIncluding({
+ method: 'bb_getaddress',
})
- .thenCallback(() => ({
- statusCode: 200,
- json: GENERATE_MOCK_BTC_BALANCE_CALL(address),
- }));
+ .thenCallback(() => {
+ return {
+ statusCode: 200,
+ json: {
+ result: {
+ address,
+ balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats
+ totalReceived: '0',
+ totalSent: '0',
+ unconfirmedBalance: '0',
+ unconfirmedTxs: 0,
+ txs: 0,
+ },
+ },
+ };
+ });
}
export async function mockRampsDynamicFeatureFlag(
diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js
index 1d0783b82624..12d0fb293e15 100644
--- a/test/e2e/mock-e2e.js
+++ b/test/e2e/mock-e2e.js
@@ -76,6 +76,7 @@ const browserAPIRequestDomains =
*/
const privateHostMatchers = [
// { pattern: RegExp, host: string }
+ { pattern: /^.*\.btc.*\.quiknode.pro$/iu, host: '*.btc*.quiknode.pro' },
];
/**
diff --git a/yarn.lock b/yarn.lock
index 94ecd006df3e..5b2708b2df0d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4981,10 +4981,10 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/bitcoin-wallet-snap@npm:^0.6.1":
- version: 0.6.1
- resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.1"
- checksum: 10/9c595e328cd63efe62cdda4194efe44ab3da4a54a89007f485280924aa9e8ee37042bda0a07751f3ce01c2c3e4740b16cd130f07558aa84cd57b20a8d5f1d3a7
+"@metamask/bitcoin-wallet-snap@npm:^0.7.0":
+ version: 0.7.0
+ resolution: "@metamask/bitcoin-wallet-snap@npm:0.7.0"
+ checksum: 10/be4eceef1715c5e6d33d095d5b4aaa974656d945ff0ed0304fdc1244eb8940eb8978f304378367642aa8fd60d6b375eecc2a4653c38ba62ec306c03955c96682
languageName: node
linkType: hard
@@ -26129,7 +26129,7 @@ __metadata:
"@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch"
"@metamask/auto-changelog": "npm:^2.1.0"
"@metamask/base-controller": "npm:^7.0.0"
- "@metamask/bitcoin-wallet-snap": "npm:^0.6.1"
+ "@metamask/bitcoin-wallet-snap": "npm:^0.7.0"
"@metamask/browser-passworder": "npm:^4.3.0"
"@metamask/build-utils": "npm:^3.0.0"
"@metamask/contract-metadata": "npm:^2.5.0"
From d1d469eef346e0901cd14835a013174b7b755d22 Mon Sep 17 00:00:00 2001
From: Maarten Zuidhoorn
Date: Wed, 16 Oct 2024 21:11:49 +0200
Subject: [PATCH 15/51] chore: Bump Snaps packages (#27376)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This bumps all Snaps packages to the latest versions.
Summary of changes in the snaps deps:
- Allow updating `context` in `snap_updateInterface`
- Add `snap_getCurrencyRate` RPC method
- Add `Avatar` component
- Add `min`, `max` and `step` props to `Input` component
- Add `size` prop to `Heading` component
- Add support for `metamask:` schemed URLs in Snap UI links
- Pass full URLs to PhishingController
- Ignore Snap insight response if transaction or signature has already
been signed
- Allow `Link` in `Row` and `Address` in `Link`
Closes https://github.com/MetaMask/snaps/issues/2776
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27376?quickstart=1)
## **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: Guillaume Roux
Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com>
Co-authored-by: Hassan Malik
Co-authored-by: Frederik Bolding
---
...ask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch | 120 ------------------
.../controllers/permissions/specifications.js | 1 +
app/scripts/metamask-controller.js | 14 ++
builds.yml | 8 +-
package.json | 17 ++-
test/e2e/snaps/enums.js | 2 +-
.../safe-component-list.js | 2 +
.../snaps/snap-ui-address/snap-ui-address.tsx | 37 ++----
.../app/snaps/snap-ui-avatar/index.ts | 1 +
.../snaps/snap-ui-avatar/snap-ui-avatar.tsx | 44 +++++++
.../app/snaps/snap-ui-link/snap-ui-link.js | 23 +++-
.../snap-ui-renderer/components/address.ts | 2 +-
.../snap-ui-renderer/components/avatar.ts | 9 ++
.../snap-ui-renderer/components/field.ts | 5 +-
.../snap-ui-renderer/components/heading.ts | 15 ++-
.../snap-ui-renderer/components/index.ts | 2 +
.../snap-ui-renderer/components/input.ts | 58 +++++++--
ui/hooks/snaps/useSnapNavigation.ts | 22 ++++
yarn.lock | 111 ++++++----------
19 files changed, 246 insertions(+), 247 deletions(-)
delete mode 100644 .yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch
create mode 100644 ui/components/app/snaps/snap-ui-avatar/index.ts
create mode 100644 ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx
create mode 100644 ui/components/app/snaps/snap-ui-renderer/components/avatar.ts
create mode 100644 ui/hooks/snaps/useSnapNavigation.ts
diff --git a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch
deleted file mode 100644
index 3361025d4860..000000000000
--- a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch
+++ /dev/null
@@ -1,120 +0,0 @@
-diff --git a/dist/ui.cjs b/dist/ui.cjs
-index 300fe9e97bba85945e3c2d200e736987453f8268..d6fa322e2b3629f41d653b91db52c3db85064276 100644
---- a/dist/ui.cjs
-+++ b/dist/ui.cjs
-@@ -200,13 +200,23 @@ function getMarkdownLinks(text) {
- * @param link - The link to validate.
- * @param isOnPhishingList - The function that checks the link against the
- * phishing list.
-+ * @throws If the link is invalid.
- */
- function validateLink(link, isOnPhishingList) {
- try {
- const url = new URL(link);
- (0, utils_1.assert)(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`);
-- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;
-- (0, utils_1.assert)(!isOnPhishingList(hostname), 'The specified URL is not allowed.');
-+ if (url.protocol === 'mailto:') {
-+ const emails = url.pathname.split(',');
-+ for (const email of emails) {
-+ const hostname = email.split('@')[1];
-+ (0, utils_1.assert)(!hostname.includes(':'));
-+ const href = `https://${hostname}`;
-+ (0, utils_1.assert)(!isOnPhishingList(href), 'The specified URL is not allowed.');
-+ }
-+ return;
-+ }
-+ (0, utils_1.assert)(!isOnPhishingList(url.href), 'The specified URL is not allowed.');
- }
- catch (error) {
- throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`);
-diff --git a/dist/ui.cjs.map b/dist/ui.cjs.map
-index 71b5ecb9eb8bc8bdf919daccf24b25737ee69819..6d6e56cd7fea85e4d477c0399506a03d465ca740 100644
---- a/dist/ui.cjs.map
-+++ b/dist/ui.cjs.map
-@@ -1 +1 @@
--{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return ;\n\n case NodeType.Button:\n return (\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n \n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n
\n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]}
-\ No newline at end of file
-+{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return ;\n\n case NodeType.Button:\n return (\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n \n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n
\n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]}
-\ No newline at end of file
-diff --git a/dist/ui.d.cts b/dist/ui.d.cts
-index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644
---- a/dist/ui.d.cts
-+++ b/dist/ui.d.cts
-@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component):
- * @param link - The link to validate.
- * @param isOnPhishingList - The function that checks the link against the
- * phishing list.
-+ * @throws If the link is invalid.
- */
- export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void;
- /**
-diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map
-index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644
---- a/dist/ui.d.cts.map
-+++ b/dist/ui.d.cts.map
-@@ -1 +1 @@
--{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"}
-\ No newline at end of file
-+{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"}
-\ No newline at end of file
-diff --git a/dist/ui.d.mts b/dist/ui.d.mts
-index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644
---- a/dist/ui.d.mts
-+++ b/dist/ui.d.mts
-@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component):
- * @param link - The link to validate.
- * @param isOnPhishingList - The function that checks the link against the
- * phishing list.
-+ * @throws If the link is invalid.
- */
- export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void;
- /**
-diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map
-index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644
---- a/dist/ui.d.mts.map
-+++ b/dist/ui.d.mts.map
-@@ -1 +1 @@
--{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"}
-\ No newline at end of file
-+{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"}
-\ No newline at end of file
-diff --git a/dist/ui.mjs b/dist/ui.mjs
-index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644
---- a/dist/ui.mjs
-+++ b/dist/ui.mjs
-@@ -195,13 +195,23 @@ function getMarkdownLinks(text) {
- * @param link - The link to validate.
- * @param isOnPhishingList - The function that checks the link against the
- * phishing list.
-+ * @throws If the link is invalid.
- */
- export function validateLink(link, isOnPhishingList) {
- try {
- const url = new URL(link);
- assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`);
-- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;
-- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');
-+ if (url.protocol === 'mailto:') {
-+ const emails = url.pathname.split(',');
-+ for (const email of emails) {
-+ const hostname = email.split('@')[1];
-+ assert(!hostname.includes(':'));
-+ const href = `https://${hostname}`;
-+ assert(!isOnPhishingList(href), 'The specified URL is not allowed.');
-+ }
-+ return;
-+ }
-+ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');
- }
- catch (error) {
- throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`);
-diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map
-index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644
---- a/dist/ui.mjs.map
-+++ b/dist/ui.mjs.map
-@@ -1 +1 @@
--{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return ;\n\n case NodeType.Button:\n return (\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n \n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n
\n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]}
-\ No newline at end of file
-+{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return ;\n\n case NodeType.Button:\n return (\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n \n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n
\n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]}
-\ No newline at end of file
diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js
index 8a40082d4d80..fffc9ae44f49 100644
--- a/app/scripts/controllers/permissions/specifications.js
+++ b/app/scripts/controllers/permissions/specifications.js
@@ -413,6 +413,7 @@ export const unrestrictedMethods = Object.freeze([
'snap_updateInterface',
'snap_getInterfaceState',
'snap_resolveInterface',
+ 'snap_getCurrencyRate',
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
'metamaskinstitutional_authenticate',
'metamaskinstitutional_reauthenticate',
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 5b3693960113..0c692703f242 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1479,6 +1479,7 @@ export default class MetamaskController extends EventEmitter {
`${this.phishingController.name}:testOrigin`,
`${this.approvalController.name}:hasRequest`,
`${this.approvalController.name}:acceptRequest`,
+ `${this.snapController.name}:get`,
],
});
@@ -5975,6 +5976,19 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger,
'SnapController:getAll',
),
+ getCurrencyRate: (currency) => {
+ const rate = this.multichainRatesController.state.rates[currency];
+ const { fiatCurrency } = this.multichainRatesController.state;
+
+ if (!rate) {
+ return undefined;
+ }
+
+ return {
+ ...rate,
+ currency: fiatCurrency,
+ };
+ },
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
hasPermission: this.permissionController.hasPermission.bind(
this.permissionController,
diff --git a/builds.yml b/builds.yml
index a69bf611a322..bcd035b56bc1 100644
--- a/builds.yml
+++ b/builds.yml
@@ -26,7 +26,7 @@ buildTypes:
- SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY
- ALLOW_LOCAL_SNAPS: false
- REQUIRE_SNAPS_ALLOWLIST: true
- - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html
+ - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html
- ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management
# Main build uses the default browser manifest
manifestOverrides: false
@@ -46,7 +46,7 @@ buildTypes:
- SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY
- ALLOW_LOCAL_SNAPS: false
- REQUIRE_SNAPS_ALLOWLIST: true
- - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html
+ - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html
- ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management
# Modifies how the version is displayed.
# eg. instead of 10.25.0 -> 10.25.0-beta.2
@@ -67,7 +67,7 @@ buildTypes:
- SEGMENT_FLASK_WRITE_KEY
- ALLOW_LOCAL_SNAPS: true
- REQUIRE_SNAPS_ALLOWLIST: false
- - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html
+ - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html
- SUPPORT_LINK: https://support.metamask.io/
- SUPPORT_REQUEST_LINK: https://support.metamask.io/
- INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID
@@ -90,7 +90,7 @@ buildTypes:
- SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY
- ALLOW_LOCAL_SNAPS: false
- REQUIRE_SNAPS_ALLOWLIST: true
- - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html
+ - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html
- MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default
- SUPPORT_LINK: https://support.metamask-institutional.io
- SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io
diff --git a/package.json b/package.json
index fe2f43f63c9a..652b8d4b3afb 100644
--- a/package.json
+++ b/package.json
@@ -229,7 +229,7 @@
"semver@7.3.8": "^7.5.4",
"@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch",
"lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch",
- "@metamask/snaps-sdk": "^6.5.1",
+ "@metamask/snaps-sdk": "^6.9.0",
"@swc/types@0.1.5": "^0.1.6",
"@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch",
"@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch",
@@ -264,8 +264,7 @@
"@metamask/network-controller@npm:^17.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch",
"@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch",
"@metamask/network-controller@npm:^20.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch",
- "path-to-regexp": "1.9.0",
- "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch"
+ "path-to-regexp": "1.9.0"
},
"dependencies": {
"@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch",
@@ -342,7 +341,7 @@
"@metamask/phishing-controller": "^12.0.1",
"@metamask/post-message-stream": "^8.0.0",
"@metamask/ppom-validator": "0.34.0",
- "@metamask/preinstalled-example-snap": "^0.1.0",
+ "@metamask/preinstalled-example-snap": "^0.2.0",
"@metamask/profile-sync-controller": "^0.9.7",
"@metamask/providers": "^14.0.2",
"@metamask/queued-request-controller": "^2.0.0",
@@ -353,11 +352,11 @@
"@metamask/selected-network-controller": "^18.0.1",
"@metamask/signature-controller": "^19.1.0",
"@metamask/smart-transactions-controller": "^13.0.0",
- "@metamask/snaps-controllers": "^9.7.0",
- "@metamask/snaps-execution-environments": "^6.7.2",
- "@metamask/snaps-rpc-methods": "^11.1.1",
- "@metamask/snaps-sdk": "^6.5.1",
- "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch",
+ "@metamask/snaps-controllers": "^9.11.1",
+ "@metamask/snaps-execution-environments": "^6.9.1",
+ "@metamask/snaps-rpc-methods": "^11.5.0",
+ "@metamask/snaps-sdk": "^6.9.0",
+ "@metamask/snaps-utils": "^8.4.1",
"@metamask/transaction-controller": "^37.2.0",
"@metamask/user-operation-controller": "^13.0.0",
"@metamask/utils": "^9.3.0",
diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js
index 2b1a6bc6532d..7fdbf5e1fd24 100644
--- a/test/e2e/snaps/enums.js
+++ b/test/e2e/snaps/enums.js
@@ -1,3 +1,3 @@
module.exports = {
- TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.1/',
+ TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.2',
};
diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js
index 14abe8eceea7..a41b92c9f463 100644
--- a/ui/components/app/metamask-template-renderer/safe-component-list.js
+++ b/ui/components/app/metamask-template-renderer/safe-component-list.js
@@ -43,6 +43,7 @@ import { SnapUICheckbox } from '../snaps/snap-ui-checkbox';
import { SnapUITooltip } from '../snaps/snap-ui-tooltip';
import { SnapUICard } from '../snaps/snap-ui-card';
import { SnapUIAddress } from '../snaps/snap-ui-address';
+import { SnapUIAvatar } from '../snaps/snap-ui-avatar';
import { SnapUISelector } from '../snaps/snap-ui-selector';
import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button';
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
@@ -106,6 +107,7 @@ export const safeComponentList = {
SnapUICard,
SnapUISelector,
SnapUIAddress,
+ SnapUIAvatar,
SnapUIFooterButton,
FormTextField,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx
index 669f7dd30799..539548622135 100644
--- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx
+++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx
@@ -1,5 +1,4 @@
import React, { useMemo } from 'react';
-import { useSelector } from 'react-redux';
import {
CaipAccountId,
isHexString,
@@ -11,32 +10,35 @@ import {
Display,
TextColor,
} from '../../../../helpers/constants/design-system';
-import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon';
-import Jazzicon from '../../../ui/jazzicon';
-import { getUseBlockie } from '../../../../selectors';
import { shortenAddress } from '../../../../helpers/utils/util';
import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils';
+import { SnapUIAvatar } from '../snap-ui-avatar';
export type SnapUIAddressProps = {
// The address must be a CAIP-10 string.
address: string;
- diameter?: number;
+ // This is not currently exposed to Snaps.
+ avatarSize?: 'xs' | 'sm' | 'md' | 'lg';
};
export const SnapUIAddress: React.FunctionComponent = ({
address,
- diameter = 32,
+ avatarSize = 'md',
}) => {
- const parsed = useMemo(() => {
+ const caipIdentifier = useMemo(() => {
if (isHexString(address)) {
// For legacy address inputs we assume them to be Ethereum addresses.
// NOTE: This means the chain ID is not gonna be reliable.
- return parseCaipAccountId(`eip155:1:${address}`);
+ return `eip155:1:${address}`;
}
- return parseCaipAccountId(address as CaipAccountId);
+ return address;
}, [address]);
- const useBlockie = useSelector(getUseBlockie);
+
+ const parsed = useMemo(
+ () => parseCaipAccountId(caipIdentifier as CaipAccountId),
+ [caipIdentifier],
+ );
// For EVM addresses, we make sure they are checksummed.
const transformedAddress =
@@ -47,20 +49,7 @@ export const SnapUIAddress: React.FunctionComponent = ({
return (
- {useBlockie ? (
-
- ) : (
-
- )}
+
{shortenedAddress}
);
diff --git a/ui/components/app/snaps/snap-ui-avatar/index.ts b/ui/components/app/snaps/snap-ui-avatar/index.ts
new file mode 100644
index 000000000000..44fc129d6b39
--- /dev/null
+++ b/ui/components/app/snaps/snap-ui-avatar/index.ts
@@ -0,0 +1 @@
+export * from './snap-ui-avatar';
diff --git a/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx b/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx
new file mode 100644
index 000000000000..7e6de5f3370b
--- /dev/null
+++ b/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx
@@ -0,0 +1,44 @@
+import React, { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { CaipAccountId, parseCaipAccountId } from '@metamask/utils';
+import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon';
+import Jazzicon from '../../../ui/jazzicon';
+import { getUseBlockie } from '../../../../selectors';
+
+export const DIAMETERS: Record = {
+ xs: 16,
+ sm: 24,
+ md: 32,
+ lg: 40,
+};
+
+export type SnapUIAvatarProps = {
+ // The address must be a CAIP-10 string.
+ address: string;
+ size?: 'xs' | 'sm' | 'md' | 'lg';
+};
+
+export const SnapUIAvatar: React.FunctionComponent = ({
+ address,
+ size = 'md',
+}) => {
+ const parsed = useMemo(() => {
+ return parseCaipAccountId(address as CaipAccountId);
+ }, [address]);
+ const useBlockie = useSelector(getUseBlockie);
+
+ return useBlockie ? (
+
+ ) : (
+
+ );
+};
diff --git a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js
index 46439c523a68..a1289543fd45 100644
--- a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js
+++ b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js
@@ -9,18 +9,39 @@ import {
IconSize,
} from '../../../component-library';
import SnapLinkWarning from '../snap-link-warning';
+import useSnapNavigation from '../../../../hooks/snaps/useSnapNavigation';
export const SnapUILink = ({ href, children }) => {
const [isOpen, setIsOpen] = useState(false);
+ const isMetaMaskUrl = href.startsWith('metamask:');
+ const { navigate } = useSnapNavigation();
+
const handleLinkClick = () => {
- setIsOpen(true);
+ if (isMetaMaskUrl) {
+ navigate(href);
+ } else {
+ setIsOpen(true);
+ }
};
const handleModalClose = () => {
setIsOpen(false);
};
+ if (isMetaMaskUrl) {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
<>
diff --git a/ui/components/app/snaps/snap-ui-renderer/components/address.ts b/ui/components/app/snaps/snap-ui-renderer/components/address.ts
index 108ff37f33a5..1e39966df760 100644
--- a/ui/components/app/snaps/snap-ui-renderer/components/address.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/components/address.ts
@@ -5,6 +5,6 @@ export const address: UIComponentFactory = ({ element }) => ({
element: 'SnapUIAddress',
props: {
address: element.props.address,
- diameter: 16,
+ avatarSize: 'xs',
},
});
diff --git a/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts
new file mode 100644
index 000000000000..9572516383b6
--- /dev/null
+++ b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts
@@ -0,0 +1,9 @@
+import { AvatarElement } from '@metamask/snaps-sdk/jsx';
+import { UIComponentFactory } from './types';
+
+export const avatar: UIComponentFactory = ({ element }) => ({
+ element: 'SnapUIAvatar',
+ props: {
+ address: element.props.address,
+ },
+});
diff --git a/ui/components/app/snaps/snap-ui-renderer/components/field.ts b/ui/components/app/snaps/snap-ui-renderer/components/field.ts
index 0bafec17b2bf..169619b9a561 100644
--- a/ui/components/app/snaps/snap-ui-renderer/components/field.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/components/field.ts
@@ -14,6 +14,7 @@ import { radioGroup as radioGroupFn } from './radioGroup';
import { checkbox as checkboxFn } from './checkbox';
import { selector as selectorFn } from './selector';
import { UIComponentFactory, UIComponentParams } from './types';
+import { constructInputProps } from './input';
export const field: UIComponentFactory = ({
element,
@@ -79,9 +80,7 @@ export const field: UIComponentFactory = ({
id: input.props.name,
placeholder: input.props.placeholder,
label: element.props.label,
- textFieldProps: {
- type: input.props.type,
- },
+ ...constructInputProps(input.props),
name: input.props.name,
form,
error: element.props.error !== undefined,
diff --git a/ui/components/app/snaps/snap-ui-renderer/components/heading.ts b/ui/components/app/snaps/snap-ui-renderer/components/heading.ts
index 709868fd4a6e..f0d6dee396d1 100644
--- a/ui/components/app/snaps/snap-ui-renderer/components/heading.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/components/heading.ts
@@ -5,11 +5,24 @@ import {
} from '../../../../../helpers/constants/design-system';
import { UIComponentFactory } from './types';
+export const generateSize = (size: HeadingElement['props']['size']) => {
+ switch (size) {
+ case 'sm':
+ return TextVariant.headingSm;
+ case 'md':
+ return TextVariant.headingMd;
+ case 'lg':
+ return TextVariant.headingLg;
+ default:
+ return TextVariant.headingSm;
+ }
+};
+
export const heading: UIComponentFactory = ({ element }) => ({
element: 'Text',
children: element.props.children,
props: {
- variant: TextVariant.headingSm,
+ variant: generateSize(element.props.size),
overflowWrap: OverflowWrap.Anywhere,
},
});
diff --git a/ui/components/app/snaps/snap-ui-renderer/components/index.ts b/ui/components/app/snaps/snap-ui-renderer/components/index.ts
index 5d3b8fa16789..17a9b6aa37c1 100644
--- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts
@@ -26,6 +26,7 @@ import { container } from './container';
import { selector } from './selector';
import { icon } from './icon';
import { section } from './section';
+import { avatar } from './avatar';
export const COMPONENT_MAPPING = {
Box: box,
@@ -38,6 +39,7 @@ export const COMPONENT_MAPPING = {
Copyable: copyable,
Row: row,
Address: address,
+ Avatar: avatar,
Button: button,
FileInput: fileInput,
Form: form,
diff --git a/ui/components/app/snaps/snap-ui-renderer/components/input.ts b/ui/components/app/snaps/snap-ui-renderer/components/input.ts
index 9cc565d5d7f5..beda6c5ba4cc 100644
--- a/ui/components/app/snaps/snap-ui-renderer/components/input.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/components/input.ts
@@ -1,16 +1,50 @@
-import { InputElement } from '@metamask/snaps-sdk/jsx';
+import { InputElement, NumberInputProps } from '@metamask/snaps-sdk/jsx';
+import { hasProperty } from '@metamask/utils';
import { UIComponentFactory } from './types';
-export const input: UIComponentFactory = ({ element, form }) => ({
- element: 'SnapUIInput',
- props: {
- id: element.props.name,
- placeholder: element.props.placeholder,
- textFieldProps: {
- type: element.props.type,
+export const constructInputProps = (props: InputElement['props']) => {
+ if (!hasProperty(props, 'type')) {
+ return {
+ textFieldProps: {
+ type: 'text',
+ },
+ };
+ }
+
+ switch (props.type) {
+ case 'number': {
+ const { step, min, max, type } = props as NumberInputProps;
+
+ return {
+ textFieldProps: {
+ type,
+ inputProps: {
+ step: step?.toString(),
+ min: min?.toString(),
+ max: max?.toString(),
+ },
+ },
+ };
+ }
+ default:
+ return {
+ textFieldProps: {
+ type: props.type,
+ },
+ };
+ }
+};
+
+export const input: UIComponentFactory = ({ element, form }) => {
+ return {
+ element: 'SnapUIInput',
+ props: {
+ id: element.props.name,
+ placeholder: element.props.placeholder,
+ ...constructInputProps(element.props),
+ name: element.props.name,
+ form,
},
- name: element.props.name,
- form,
- },
-});
+ };
+};
diff --git a/ui/hooks/snaps/useSnapNavigation.ts b/ui/hooks/snaps/useSnapNavigation.ts
new file mode 100644
index 000000000000..047b6fb13d36
--- /dev/null
+++ b/ui/hooks/snaps/useSnapNavigation.ts
@@ -0,0 +1,22 @@
+import { useHistory } from 'react-router-dom';
+import { parseMetaMaskUrl } from '@metamask/snaps-utils';
+import { getSnapRoute } from '../../helpers/utils/util';
+
+const useSnapNavigation = () => {
+ const history = useHistory();
+ const navigate = (url: string) => {
+ let path;
+ const linkData = parseMetaMaskUrl(url);
+ if (linkData.snapId) {
+ path = getSnapRoute(linkData.snapId);
+ } else {
+ path = linkData.path;
+ }
+ history.push(path);
+ };
+ return {
+ navigate,
+ };
+};
+
+export default useSnapNavigation;
diff --git a/yarn.lock b/yarn.lock
index 5b2708b2df0d..6d5582a99ee1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6069,12 +6069,12 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/preinstalled-example-snap@npm:^0.1.0":
- version: 0.1.0
- resolution: "@metamask/preinstalled-example-snap@npm:0.1.0"
+"@metamask/preinstalled-example-snap@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "@metamask/preinstalled-example-snap@npm:0.2.0"
dependencies:
- "@metamask/snaps-sdk": "npm:^6.5.0"
- checksum: 10/0540aa6c20b17171f3a3bcf9ea2a7be551d6abbf16de9bd55dce038c5602c62a3921c7e840b82a325b0db00f26b96f54568854bdcd091558bd3b8fa8c6188023
+ "@metamask/snaps-sdk": "npm:^6.9.0"
+ checksum: 10/f8ad6f42c9bd7ce3b7fc9b45eecda6191320ff762b48c482ba4944a6d7a228682b833c15e56058f26ac7bb10417dfe9de340af1c8eb9bbe5dc03c665426ccb13
languageName: node
linkType: hard
@@ -6272,9 +6272,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snaps-controllers@npm:^9.7.0":
- version: 9.7.0
- resolution: "@metamask/snaps-controllers@npm:9.7.0"
+"@metamask/snaps-controllers@npm:^9.11.1, @metamask/snaps-controllers@npm:^9.7.0":
+ version: 9.11.1
+ resolution: "@metamask/snaps-controllers@npm:9.11.1"
dependencies:
"@metamask/approval-controller": "npm:^7.0.2"
"@metamask/base-controller": "npm:^6.0.2"
@@ -6286,9 +6286,9 @@ __metadata:
"@metamask/post-message-stream": "npm:^8.1.1"
"@metamask/rpc-errors": "npm:^6.3.1"
"@metamask/snaps-registry": "npm:^3.2.1"
- "@metamask/snaps-rpc-methods": "npm:^11.1.1"
- "@metamask/snaps-sdk": "npm:^6.5.0"
- "@metamask/snaps-utils": "npm:^8.1.1"
+ "@metamask/snaps-rpc-methods": "npm:^11.5.0"
+ "@metamask/snaps-sdk": "npm:^6.9.0"
+ "@metamask/snaps-utils": "npm:^8.4.1"
"@metamask/utils": "npm:^9.2.1"
"@xstate/fsm": "npm:^2.0.0"
browserify-zlib: "npm:^0.2.0"
@@ -6301,30 +6301,30 @@ __metadata:
readable-web-to-node-stream: "npm:^3.0.2"
tar-stream: "npm:^3.1.7"
peerDependencies:
- "@metamask/snaps-execution-environments": ^6.7.1
+ "@metamask/snaps-execution-environments": ^6.9.1
peerDependenciesMeta:
"@metamask/snaps-execution-environments":
optional: true
- checksum: 10/8a353819e60330ef3e338a40b1115d4c830b92b1cc0c92afb2b34bf46fbc906e6da5f905654e1d486cacd40b7025ec74d3cd01cb935090035ce9f1021ce5469f
+ checksum: 10/e9d47b62c39cf331d26a9e35dcf5c0452aff70980db31b42b56b11165d8d1dc7e3b5ad6b495644baa0276b18a7d9681bfb059388c4f2fb1b07c6bbc8b8da799b
languageName: node
linkType: hard
-"@metamask/snaps-execution-environments@npm:^6.7.2":
- version: 6.7.2
- resolution: "@metamask/snaps-execution-environments@npm:6.7.2"
+"@metamask/snaps-execution-environments@npm:^6.9.1":
+ version: 6.9.1
+ resolution: "@metamask/snaps-execution-environments@npm:6.9.1"
dependencies:
"@metamask/json-rpc-engine": "npm:^9.0.2"
"@metamask/object-multiplex": "npm:^2.0.0"
"@metamask/post-message-stream": "npm:^8.1.1"
"@metamask/providers": "npm:^17.1.2"
"@metamask/rpc-errors": "npm:^6.3.1"
- "@metamask/snaps-sdk": "npm:^6.5.0"
- "@metamask/snaps-utils": "npm:^8.1.1"
+ "@metamask/snaps-sdk": "npm:^6.8.0"
+ "@metamask/snaps-utils": "npm:^8.4.0"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^9.2.1"
nanoid: "npm:^3.1.31"
readable-stream: "npm:^3.6.2"
- checksum: 10/4b8ec4c0f6e628feeffd92fe4378fd204d2ed78012a1ed5282b24b00c78cebc3b6d7cb1306903b045a2ca887ecc0adafb2c96da4a19f2730a268f4912b36bec3
+ checksum: 10/87fb63e89780ebeb9083c93988167e671ceb3d1c77980a2cd32801f83d285669859bfd248197d3a2d683119b87554f1f835965549ad04587c8c2fa2f01fa1f18
languageName: node
linkType: hard
@@ -6340,63 +6340,32 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snaps-rpc-methods@npm:^11.1.1":
- version: 11.1.1
- resolution: "@metamask/snaps-rpc-methods@npm:11.1.1"
+"@metamask/snaps-rpc-methods@npm:^11.5.0":
+ version: 11.5.0
+ resolution: "@metamask/snaps-rpc-methods@npm:11.5.0"
dependencies:
"@metamask/key-tree": "npm:^9.1.2"
"@metamask/permission-controller": "npm:^11.0.0"
"@metamask/rpc-errors": "npm:^6.3.1"
- "@metamask/snaps-sdk": "npm:^6.5.0"
- "@metamask/snaps-utils": "npm:^8.1.1"
+ "@metamask/snaps-sdk": "npm:^6.9.0"
+ "@metamask/snaps-utils": "npm:^8.4.1"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^9.2.1"
"@noble/hashes": "npm:^1.3.1"
- checksum: 10/e23279dabc6f4ffe2c6c4a7003a624cd5e79b558d7981ec12c23e54a5da25cb7be9bc7bddfa8b2ce84af28a89b42076a2c14ab004b7a976a4426bf1e1de71b5b
+ checksum: 10/a89b79926d5204a70369cd70e5174290805e8f9ede8057a49e347bd0e680d88de40ddfc25b3e54f53a16c3080a736ab73b50ffe50623264564af13f8709a23d3
languageName: node
linkType: hard
-"@metamask/snaps-sdk@npm:^6.5.1":
- version: 6.5.1
- resolution: "@metamask/snaps-sdk@npm:6.5.1"
+"@metamask/snaps-sdk@npm:^6.9.0":
+ version: 6.9.0
+ resolution: "@metamask/snaps-sdk@npm:6.9.0"
dependencies:
"@metamask/key-tree": "npm:^9.1.2"
"@metamask/providers": "npm:^17.1.2"
"@metamask/rpc-errors": "npm:^6.3.1"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^9.2.1"
- checksum: 10/7831fb2ca61a32ad43e971de9307b221f6bd2f65c84a3286f350cfdd2396166c58db6cd2fac9711654a211c8dc2049e591a79ab720b3f5ad562e434f75e95d32
- languageName: node
- linkType: hard
-
-"@metamask/snaps-utils@npm:8.1.1":
- version: 8.1.1
- resolution: "@metamask/snaps-utils@npm:8.1.1"
- dependencies:
- "@babel/core": "npm:^7.23.2"
- "@babel/types": "npm:^7.23.0"
- "@metamask/base-controller": "npm:^6.0.2"
- "@metamask/key-tree": "npm:^9.1.2"
- "@metamask/permission-controller": "npm:^11.0.0"
- "@metamask/rpc-errors": "npm:^6.3.1"
- "@metamask/slip44": "npm:^4.0.0"
- "@metamask/snaps-registry": "npm:^3.2.1"
- "@metamask/snaps-sdk": "npm:^6.5.0"
- "@metamask/superstruct": "npm:^3.1.0"
- "@metamask/utils": "npm:^9.2.1"
- "@noble/hashes": "npm:^1.3.1"
- "@scure/base": "npm:^1.1.1"
- chalk: "npm:^4.1.2"
- cron-parser: "npm:^4.5.0"
- fast-deep-equal: "npm:^3.1.3"
- fast-json-stable-stringify: "npm:^2.1.0"
- fast-xml-parser: "npm:^4.4.1"
- marked: "npm:^12.0.1"
- rfdc: "npm:^1.3.0"
- semver: "npm:^7.5.4"
- ses: "npm:^1.1.0"
- validate-npm-package-name: "npm:^5.0.0"
- checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f
+ checksum: 10/ea2c34c4451f671acc6c3c0ad0d46e770e8b7d0741c1d78a30bc36b883f09a10e9a428b8b564ecd0171da95fdf78bb8ac0de261423a1b35de5d22852300a24ee
languageName: node
linkType: hard
@@ -6431,9 +6400,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch":
- version: 8.1.1
- resolution: "@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch::version=8.1.1&hash=d09097"
+"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1":
+ version: 8.4.1
+ resolution: "@metamask/snaps-utils@npm:8.4.1"
dependencies:
"@babel/core": "npm:^7.23.2"
"@babel/types": "npm:^7.23.0"
@@ -6443,7 +6412,7 @@ __metadata:
"@metamask/rpc-errors": "npm:^6.3.1"
"@metamask/slip44": "npm:^4.0.0"
"@metamask/snaps-registry": "npm:^3.2.1"
- "@metamask/snaps-sdk": "npm:^6.5.0"
+ "@metamask/snaps-sdk": "npm:^6.9.0"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^9.2.1"
"@noble/hashes": "npm:^1.3.1"
@@ -6458,7 +6427,7 @@ __metadata:
semver: "npm:^7.5.4"
ses: "npm:^1.1.0"
validate-npm-package-name: "npm:^5.0.0"
- checksum: 10/6b1d3d70c5ebee684d5b76bf911c66ebd122a0607cefcfc9fffd4bf6882a7acfca655d97be87c0f7f47e59a981b58234578ed8a123e554a36e6c48ff87492655
+ checksum: 10/c68a2fe69dc835c2b996d621fd4698435475d419a85aa557aa000aae0ab7ebb68d2a52f0b28bbab94fff895ece9a94077e3910a21b16d904cff3b9419ca575b6
languageName: node
linkType: hard
@@ -26178,7 +26147,7 @@ __metadata:
"@metamask/post-message-stream": "npm:^8.0.0"
"@metamask/ppom-validator": "npm:0.34.0"
"@metamask/preferences-controller": "npm:^13.0.2"
- "@metamask/preinstalled-example-snap": "npm:^0.1.0"
+ "@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:^2.0.0"
@@ -26189,11 +26158,11 @@ __metadata:
"@metamask/selected-network-controller": "npm:^18.0.1"
"@metamask/signature-controller": "npm:^19.1.0"
"@metamask/smart-transactions-controller": "npm:^13.0.0"
- "@metamask/snaps-controllers": "npm:^9.7.0"
- "@metamask/snaps-execution-environments": "npm:^6.7.2"
- "@metamask/snaps-rpc-methods": "npm:^11.1.1"
- "@metamask/snaps-sdk": "npm:^6.5.1"
- "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch"
+ "@metamask/snaps-controllers": "npm:^9.11.1"
+ "@metamask/snaps-execution-environments": "npm:^6.9.1"
+ "@metamask/snaps-rpc-methods": "npm:^11.5.0"
+ "@metamask/snaps-sdk": "npm:^6.9.0"
+ "@metamask/snaps-utils": "npm:^8.4.1"
"@metamask/test-bundler": "npm:^1.0.0"
"@metamask/test-dapp": "npm:^8.4.0"
"@metamask/transaction-controller": "npm:^37.2.0"
From aebd94a5a3e98623c8e4443f30160380b9654064 Mon Sep 17 00:00:00 2001
From: Ethan Wessel
Date: Wed, 16 Oct 2024 15:07:36 -0700
Subject: [PATCH 16/51] fix: swapQuotesError as a property in the reported
metric (#27712)
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/27712?quickstart=1)
## **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.
---
shared/constants/metametrics.ts | 3 +-
.../send/components/quote-card/index.tsx | 29 ++++++++++++++++++-
ui/components/multichain/pages/send/send.js | 3 ++
ui/ducks/swaps/swaps.js | 4 +--
4 files changed, 35 insertions(+), 4 deletions(-)
diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts
index 544d24ce1271..8107a1040127 100644
--- a/shared/constants/metametrics.ts
+++ b/shared/constants/metametrics.ts
@@ -742,7 +742,8 @@ export enum MetaMetricsEventName {
sendFlowExited = 'Send Flow Exited',
sendRecipientSelected = 'Send Recipient Selected',
sendSwapQuoteError = 'Send Swap Quote Error',
- sendSwapQuoteFetched = 'Send Swap Quote Fetched',
+ sendSwapQuoteRequested = 'Send Swap Quote Requested',
+ sendSwapQuoteReceived = 'Send Swap Quote Received',
sendTokenModalOpened = 'Send Token Modal Opened',
}
diff --git a/ui/components/multichain/pages/send/components/quote-card/index.tsx b/ui/components/multichain/pages/send/components/quote-card/index.tsx
index 99b84e42a29b..9ac05dd6617f 100644
--- a/ui/components/multichain/pages/send/components/quote-card/index.tsx
+++ b/ui/components/multichain/pages/send/components/quote-card/index.tsx
@@ -87,7 +87,7 @@ export function QuoteCard({ scrollRef }: QuoteCardProps) {
if (bestQuote) {
trackEvent(
{
- event: MetaMetricsEventName.sendSwapQuoteFetched,
+ event: MetaMetricsEventName.sendSwapQuoteReceived,
category: MetaMetricsEventCategory.Send,
properties: {
is_first_fetch: isQuoteJustLoaded,
@@ -118,6 +118,33 @@ export function QuoteCard({ scrollRef }: QuoteCardProps) {
return () => clearTimeout(timeout);
}, [timeLeft]);
+ // use to track when a quote is requested and received
+ useEffect(() => {
+ if (isSwapQuoteLoading) {
+ trackEvent(
+ {
+ event: MetaMetricsEventName.sendSwapQuoteRequested,
+ category: MetaMetricsEventCategory.Send,
+ sensitiveProperties: {
+ ...sendAnalytics,
+ },
+ },
+ { excludeMetaMetricsId: false },
+ );
+ } else if (bestQuote) {
+ trackEvent(
+ {
+ event: MetaMetricsEventName.sendSwapQuoteReceived,
+ category: MetaMetricsEventCategory.Send,
+ sensitiveProperties: {
+ ...sendAnalytics,
+ },
+ },
+ { excludeMetaMetricsId: false },
+ );
+ }
+ }, [isSwapQuoteLoading]);
+
const infoText = useMemo(() => {
if (isSwapQuoteLoading) {
return t('swapFetchingQuotes');
diff --git a/ui/components/multichain/pages/send/send.js b/ui/components/multichain/pages/send/send.js
index 3fe8ef3652eb..b77a5e7f5810 100644
--- a/ui/components/multichain/pages/send/send.js
+++ b/ui/components/multichain/pages/send/send.js
@@ -260,6 +260,9 @@ export const SendPage = () => {
{
event: MetaMetricsEventName.sendSwapQuoteError,
category: MetaMetricsEventCategory.Send,
+ properties: {
+ error: swapQuotesError,
+ },
sensitiveProperties: {
...sendAnalytics,
},
diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js
index ece4292fdf14..91ed081eb719 100644
--- a/ui/ducks/swaps/swaps.js
+++ b/ui/ducks/swaps/swaps.js
@@ -751,7 +751,7 @@ export const fetchQuotesAndSetQuoteState = (
const currentSmartTransactionsEnabled =
getCurrentSmartTransactionsEnabled(state);
trackEvent({
- event: 'Quotes Requested',
+ event: MetaMetricsEventName.QuotesRequested,
category: MetaMetricsEventCategory.Swaps,
sensitiveProperties: {
token_from: fromTokenSymbol,
@@ -839,7 +839,7 @@ export const fetchQuotesAndSetQuoteState = (
const tokenToAmountToString = tokenToAmountBN.toString(10);
trackEvent({
- event: 'Quotes Received',
+ event: MetaMetricsEventName.QuotesReceived,
category: MetaMetricsEventCategory.Swaps,
sensitiveProperties: {
token_from: fromTokenSymbol,
From 70e2c0874986b3d050db4adb9c035f9eb69428f5 Mon Sep 17 00:00:00 2001
From: legobeat <109787230+legobeat@users.noreply.github.com>
Date: Wed, 16 Oct 2024 23:21:12 +0000
Subject: [PATCH 17/51] fix(deps): update from eth-rpc-errors to
@metamask/rpc-errors (cause edition) (#24496)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
- Upgrade from obsolete `eth-rpc-errors` to `@metamask/rpc-errors`
- This introduce handling of error causes
See [here](https://github.com/MetaMask/rpc-errors/pull/140) for some
context.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24496?quickstart=1)
## **Related issues**
- #22871
#### Blocked by
- [x] https://github.com/MetaMask/rpc-errors/pull/158
- [x] https://github.com/MetaMask/rpc-errors/pull/144
- [x] https://github.com/MetaMask/rpc-errors/pull/140
#### Blocking
- #22875
## **Manual testing steps**
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I’ve followed [MetaMask 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/scripts/background.js | 4 +-
app/scripts/lib/createMetaRPCHandler.js | 4 +-
app/scripts/lib/createMetaRPCHandler.test.js | 1 +
.../lib/createRPCMethodTrackingMiddleware.js | 2 +-
.../createRPCMethodTrackingMiddleware.test.js | 2 +-
app/scripts/lib/metaRPCClientFactory.js | 4 +-
.../createMethodMiddleware.js | 4 +-
.../createUnsupportedMethodMiddleware.ts | 4 +-
.../handlers/add-ethereum-chain.js | 6 +-
.../handlers/add-ethereum-chain.test.js | 8 +-
.../handlers/ethereum-chain-utils.js | 28 +-
.../mmi-set-account-and-network.js | 4 +-
.../handlers/request-accounts.js | 6 +-
.../handlers/send-metadata.js | 4 +-
.../handlers/switch-ethereum-chain.js | 4 +-
.../handlers/watch-asset.js | 4 +-
.../handlers/watch-asset.test.js | 4 +-
app/scripts/metamask-controller.js | 10 +-
docs/confirmations.md | 4 +-
lavamoat/browserify/beta/policy.json | 289 +++++++++++++++---
lavamoat/browserify/flask/policy.json | 289 +++++++++++++++---
lavamoat/browserify/main/policy.json | 289 +++++++++++++++---
lavamoat/browserify/mmi/policy.json | 289 +++++++++++++++---
lavamoat/build-system/policy.json | 2 +-
package.json | 3 +-
shared/modules/error.test.ts | 2 +-
shared/modules/error.ts | 39 ++-
.../dapp-interactions/provider-api.spec.js | 2 +-
.../qr-hardware-popover.js | 4 +-
.../import-account/import-account.js | 6 +-
.../import-nfts-modal/import-nfts-modal.js | 3 +-
ui/ducks/send/helpers.js | 8 +-
ui/ducks/send/send.js | 10 +-
.../confirm-add-suggested-nft.js | 6 +-
.../confirm-add-suggested-token.js | 4 +-
.../components/confirm/footer/footer.tsx | 4 +-
.../components/confirm/nav/nav.tsx | 4 +-
.../signature-request-original.component.js | 6 +-
.../signature-request-original.container.js | 3 +-
.../signature-request-siwe.js | 4 +-
.../signature-request/signature-request.js | 4 +-
.../templates/add-ethereum-chain.js | 4 +-
.../templates/switch-ethereum-chain.js | 4 +-
ui/pages/error/error.component.js | 6 +-
ui/pages/keychains/reveal-seed.js | 3 +-
.../permissions-connect.component.js | 6 +-
ui/store/actions.ts | 52 ++--
.../institutional/institution-background.ts | 17 +-
yarn.lock | 19 +-
49 files changed, 1198 insertions(+), 290 deletions(-)
diff --git a/app/scripts/background.js b/app/scripts/background.js
index b6fe63b9aff1..9f203b35661d 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -18,7 +18,7 @@ import { isObject } from '@metamask/utils';
import { ApprovalType } from '@metamask/controller-utils';
import PortStream from 'extension-port-stream';
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
import { NotificationServicesController } from '@metamask/notification-services-controller';
@@ -1159,7 +1159,7 @@ export function setupController(
default:
controller.approvalController.reject(
id,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
break;
}
diff --git a/app/scripts/lib/createMetaRPCHandler.js b/app/scripts/lib/createMetaRPCHandler.js
index 77f86d23fe02..9d72620b4013 100644
--- a/app/scripts/lib/createMetaRPCHandler.js
+++ b/app/scripts/lib/createMetaRPCHandler.js
@@ -1,4 +1,4 @@
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { rpcErrors, serializeError } from '@metamask/rpc-errors';
import { isStreamWritable } from './stream-utils';
const createMetaRPCHandler = (api, outStream) => {
@@ -9,7 +9,7 @@ const createMetaRPCHandler = (api, outStream) => {
if (!api[data.method]) {
outStream.write({
jsonrpc: '2.0',
- error: ethErrors.rpc.methodNotFound({
+ error: rpcErrors.methodNotFound({
message: `${data.method} not found`,
}),
id: data.id,
diff --git a/app/scripts/lib/createMetaRPCHandler.test.js b/app/scripts/lib/createMetaRPCHandler.test.js
index 842af632e830..873366d53443 100644
--- a/app/scripts/lib/createMetaRPCHandler.test.js
+++ b/app/scripts/lib/createMetaRPCHandler.test.js
@@ -71,6 +71,7 @@ describe('createMetaRPCHandler', () => {
});
streamTest.on('data', (data) => {
expect(data.error.message).toStrictEqual('foo-error');
+ expect(data.error.data.cause.message).toStrictEqual('foo-error');
streamTest.end();
});
});
diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js
index b50905b8cb65..a5f12687f89e 100644
--- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js
+++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js
@@ -1,5 +1,5 @@
import { ApprovalType, detectSIWE } from '@metamask/controller-utils';
-import { errorCodes } from 'eth-rpc-errors';
+import { errorCodes } from '@metamask/rpc-errors';
import { isValidAddress } from 'ethereumjs-util';
import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app';
import {
diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js
index b96c708be2d3..01daaf2974a4 100644
--- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js
+++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js
@@ -1,4 +1,4 @@
-import { errorCodes } from 'eth-rpc-errors';
+import { errorCodes } from '@metamask/rpc-errors';
import { detectSIWE } from '@metamask/controller-utils';
import MetaMetricsController from '../controllers/metametrics';
diff --git a/app/scripts/lib/metaRPCClientFactory.js b/app/scripts/lib/metaRPCClientFactory.js
index 3aae9962dbdb..2451189836e9 100644
--- a/app/scripts/lib/metaRPCClientFactory.js
+++ b/app/scripts/lib/metaRPCClientFactory.js
@@ -1,4 +1,4 @@
-import { EthereumRpcError } from 'eth-rpc-errors';
+import { JsonRpcError } from '@metamask/rpc-errors';
import SafeEventEmitter from '@metamask/safe-event-emitter';
import createRandomId from '../../../shared/modules/random-id';
import { TEN_SECONDS_IN_MILLISECONDS } from '../../../shared/lib/transactions-controller-utils';
@@ -77,7 +77,7 @@ class MetaRPCClient {
}
if (error) {
- const e = new EthereumRpcError(error.code, error.message, error.data);
+ const e = new JsonRpcError(error.code, error.message, error.data);
// preserve the stack from serializeError
e.stack = error.stack;
if (cb) {
diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
index e4b436163fc6..cee4e7763255 100644
--- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
+++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
@@ -1,7 +1,7 @@
import { permissionRpcMethods } from '@metamask/permission-controller';
+import { rpcErrors } from '@metamask/rpc-errors';
import { selectHooks } from '@metamask/snaps-rpc-methods';
import { hasProperty } from '@metamask/utils';
-import { ethErrors } from 'eth-rpc-errors';
import { handlers as localHandlers, legacyHandlers } from './handlers';
const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers];
@@ -67,7 +67,7 @@ function makeMethodMiddlewareMaker(handlers) {
return end(
error instanceof Error
? error
- : ethErrors.rpc.internal({ data: error }),
+ : rpcErrors.internal({ data: error }),
);
}
}
diff --git a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts
index 193cc54b5a38..12abc82d4b21 100644
--- a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts
+++ b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import type { JsonRpcMiddleware } from 'json-rpc-engine';
import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network';
@@ -12,7 +12,7 @@ export function createUnsupportedMethodMiddleware(): JsonRpcMiddleware<
> {
return async function unsupportedMethodMiddleware(req, _res, next, end) {
if ((UNSUPPORTED_RPC_METHODS as Set).has(req.method)) {
- return end(ethErrors.rpc.methodNotSupported());
+ return end(rpcErrors.methodNotSupported());
}
return next();
};
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js
index 2f4727fdab36..afcc2e167043 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js
@@ -1,7 +1,7 @@
-import { ApprovalType } from '@metamask/controller-utils';
import * as URI from 'uri-js';
+import { ApprovalType } from '@metamask/controller-utils';
import { RpcEndpointType } from '@metamask/network-controller';
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import { cloneDeep } from 'lodash';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
import {
@@ -73,7 +73,7 @@ async function addEthereumChainHandler(
existingNetwork.nativeCurrency !== ticker
) {
return end(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\n${ticker}`,
}),
);
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js
index 945953cff562..ee0c9d3f732b 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
import addEthereumChain from './add-ethereum-chain';
@@ -350,7 +350,7 @@ describe('addEthereumChainHandler', () => {
);
expect(mockEnd).toHaveBeenCalledWith(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\ninvalid_chain_id`,
}),
);
@@ -573,7 +573,7 @@ describe('addEthereumChainHandler', () => {
);
expect(mockEnd).toHaveBeenCalledWith(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `Received unexpected keys on object parameter. Unsupported keys:\n${unexpectedParam}`,
}),
);
@@ -657,7 +657,7 @@ describe('addEthereumChainHandler', () => {
);
expect(mockEnd).toHaveBeenCalledWith(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\nWRONG`,
}),
);
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js
index 080fef549564..10973e052715 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js
@@ -1,4 +1,4 @@
-import { errorCodes, ethErrors } from 'eth-rpc-errors';
+import { errorCodes, rpcErrors } from '@metamask/rpc-errors';
import {
isPrefixedFormattedHexString,
isSafeChainId,
@@ -11,13 +11,13 @@ import { getValidUrl } from '../../util';
export function validateChainId(chainId) {
const _chainId = typeof chainId === 'string' && chainId.toLowerCase();
if (!isPrefixedFormattedHexString(_chainId)) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`,
});
}
if (!isSafeChainId(parseInt(_chainId, 16))) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`,
});
}
@@ -27,7 +27,7 @@ export function validateChainId(chainId) {
export function validateSwitchEthereumChainParams(req, end) {
if (!req.params?.[0] || typeof req.params[0] !== 'object') {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
req.params,
)}`,
@@ -36,7 +36,7 @@ export function validateSwitchEthereumChainParams(req, end) {
const { chainId, ...otherParams } = req.params[0];
if (Object.keys(otherParams).length > 0) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Received unexpected keys on object parameter. Unsupported keys:\n${Object.keys(
otherParams,
)}`,
@@ -48,7 +48,7 @@ export function validateSwitchEthereumChainParams(req, end) {
export function validateAddEthereumChainParams(params, end) {
if (!params || typeof params !== 'object') {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
params,
)}`,
@@ -70,14 +70,14 @@ export function validateAddEthereumChainParams(params, end) {
);
if (otherKeys.length > 0) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Received unexpected keys on object parameter. Unsupported keys:\n${otherKeys}`,
});
}
const _chainId = validateChainId(chainId, end);
if (!rpcUrls || !Array.isArray(rpcUrls) || rpcUrls.length === 0) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`,
});
}
@@ -100,13 +100,13 @@ export function validateAddEthereumChainParams(params, end) {
: null;
if (!firstValidRPCUrl) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`,
});
}
if (typeof chainName !== 'string' || !chainName) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected non-empty string 'chainName'. Received:\n${chainName}`,
});
}
@@ -116,18 +116,18 @@ export function validateAddEthereumChainParams(params, end) {
if (nativeCurrency !== null) {
if (typeof nativeCurrency !== 'object' || Array.isArray(nativeCurrency)) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected null or object 'nativeCurrency'. Received:\n${nativeCurrency}`,
});
}
if (nativeCurrency.decimals !== 18) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected the number 18 for 'nativeCurrency.decimals' when 'nativeCurrency' is provided. Received: ${nativeCurrency.decimals}`,
});
}
if (!nativeCurrency.symbol || typeof nativeCurrency.symbol !== 'string') {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected a string 'nativeCurrency.symbol'. Received: ${nativeCurrency.symbol}`,
});
}
@@ -138,7 +138,7 @@ export function validateAddEthereumChainParams(params, end) {
ticker !== UNKNOWN_TICKER_SYMBOL &&
(typeof ticker !== 'string' || ticker.length < 1 || ticker.length > 6)
) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected 1-6 character string 'nativeCurrency.symbol'. Received:\n${ticker}`,
});
}
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js
index 9dca692601a3..6c3dc41da9d2 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js
@@ -1,5 +1,5 @@
-import { ethErrors } from 'eth-rpc-errors';
import { isAllowedRPCOrigin } from '@metamask-institutional/rpc-allowlist';
+import { rpcErrors } from '@metamask/rpc-errors';
import { MESSAGE_TYPE } from '../../../../../../shared/constants/app';
const mmiSetAccountAndNetwork = {
@@ -46,7 +46,7 @@ async function mmiSetAccountAndNetworkHandler(
if (!req.params?.[0] || typeof req.params[0] !== 'object') {
return end(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
req.params,
)}`,
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js
index f90fb5bd0d42..04977fe465d9 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
import {
MetaMetricsEventName,
@@ -71,7 +71,7 @@ async function requestEthereumAccountsHandler(
},
) {
if (locks.has(origin)) {
- res.error = ethErrors.rpc.resourceUnavailable(
+ res.error = rpcErrors.resourceUnavailable(
`Already processing ${MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS}. Please wait.`,
);
return end();
@@ -132,7 +132,7 @@ async function requestEthereumAccountsHandler(
} else {
// This should never happen, because it should be caught in the
// above catch clause
- res.error = ethErrors.rpc.internal(
+ res.error = rpcErrors.internal(
'Accounts unexpectedly unavailable. Please report this bug.',
);
}
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js
index a32fa497f248..35ec117a1f63 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
/**
@@ -50,7 +50,7 @@ function sendMetadataHandler(
origin,
});
} else {
- return end(ethErrors.rpc.invalidParams({ data: params }));
+ return end(rpcErrors.invalidParams({ data: params }));
}
res.result = true;
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js
index f43973e4ba57..5f907bef4d4b 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
import {
validateSwitchEthereumChainParams,
@@ -57,7 +57,7 @@ async function switchEthereumChainHandler(
if (!networkClientIdToSwitchTo) {
return end(
- ethErrors.provider.custom({
+ providerErrors.custom({
code: 4902,
message: `Unrecognized chain ID "${chainId}". Try adding the chain using ${MESSAGE_TYPE.ADD_ETHEREUM_CHAIN} first.`,
}),
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js
index 6bce7d7dca18..fdfacb373c77 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js
@@ -1,5 +1,5 @@
import { ERC1155, ERC721 } from '@metamask/controller-utils';
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
const watchAsset = {
@@ -51,7 +51,7 @@ async function watchAssetHandler(
typeof tokenId !== 'string'
) {
return end(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `Expected parameter 'tokenId' to be type 'string'. Received type '${typeof tokenId}'`,
}),
);
diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js
index eebe8a470ead..73efdd2a2798 100644
--- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js
+++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js
@@ -1,5 +1,5 @@
import { ERC20, ERC721 } from '@metamask/controller-utils';
-import { ethErrors } from 'eth-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import watchAssetHandler from './watch-asset';
describe('watchAssetHandler', () => {
@@ -95,7 +95,7 @@ describe('watchAssetHandler', () => {
});
expect(mockEnd).toHaveBeenCalledWith(
- ethErrors.rpc.invalidParams({
+ rpcErrors.invalidParams({
message: `Expected parameter 'tokenId' to be type 'string'. Received type 'number'`,
}),
);
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 0c692703f242..87f7570f2d19 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -27,9 +27,9 @@ import createFilterMiddleware from '@metamask/eth-json-rpc-filters';
import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager';
import {
errorCodes as rpcErrorCodes,
- EthereumRpcError,
- ethErrors,
-} from 'eth-rpc-errors';
+ JsonRpcError,
+ providerErrors,
+} from '@metamask/rpc-errors';
import { Mutex } from 'await-semaphore';
import log from 'loglevel';
@@ -471,7 +471,7 @@ export default class MetamaskController extends EventEmitter {
this.encryptionPublicKeyController.clearUnapproved();
this.decryptMessageController.clearUnapproved();
this.signatureController.clearUnapproved();
- this.approvalController.clear(ethErrors.provider.userRejectedRequest());
+ this.approvalController.clear(providerErrors.userRejectedRequest());
};
this.queuedRequestController = new QueuedRequestController({
@@ -6726,7 +6726,7 @@ export default class MetamaskController extends EventEmitter {
try {
this.approvalController.reject(
id,
- new EthereumRpcError(error.code, error.message, error.data),
+ new JsonRpcError(error.code, error.message, error.data),
);
} catch (exp) {
if (!(exp instanceof ApprovalRequestNotFoundError)) {
diff --git a/docs/confirmations.md b/docs/confirmations.md
index 7af838d2053e..86577fc8f691 100644
--- a/docs/confirmations.md
+++ b/docs/confirmations.md
@@ -168,7 +168,7 @@ function getValues(pendingApproval, t, actions, _history) {
onCancel: () =>
actions.rejectPendingApproval(
pendingApproval.id,
- ethErrors.provider.userRejectedRequest().serialize(),
+ providerErrors.userRejectedRequest().serialize(),
),
networkDisplay: true,
};
@@ -401,4 +401,4 @@ When an approval flow is created, this is reflected in the state and the UI will
### Custom Success Approval
-[](assets/confirmation.png)
\ No newline at end of file
+[](assets/confirmation.png)
diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json
index d7522783f9fc..880b542673ea 100644
--- a/lavamoat/browserify/beta/policy.json
+++ b/lavamoat/browserify/beta/policy.json
@@ -662,8 +662,8 @@
},
"packages": {
"@metamask/approval-controller>@metamask/base-controller": true,
- "@metamask/approval-controller>nanoid": true,
- "@metamask/rpc-errors": true
+ "@metamask/approval-controller>@metamask/rpc-errors": true,
+ "@metamask/approval-controller>nanoid": true
}
},
"@metamask/approval-controller>@metamask/base-controller": {
@@ -674,6 +674,12 @@
"immer": true
}
},
+ "@metamask/approval-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/approval-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@@ -699,13 +705,13 @@
"@ethersproject/providers": true,
"@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/polling-controller": true,
+ "@metamask/assets-controllers>@metamask/rpc-errors": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/eth-query": true,
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
- "@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"cockatiel": true,
@@ -727,6 +733,12 @@
"uuid": true
}
},
+ "@metamask/assets-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/base-controller": {
"globals": {
"setTimeout": true
@@ -851,11 +863,32 @@
},
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -886,14 +919,20 @@
"setTimeout": true
},
"packages": {
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-middleware>klona": true,
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true
}
},
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/eth-ledger-bridge-keyring": {
"globals": {
"addEventListener": true,
@@ -1505,9 +1544,9 @@
"@metamask/network-controller>@metamask/eth-json-rpc-infura": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware": true,
"@metamask/network-controller>@metamask/eth-json-rpc-provider": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/swappable-obj-proxy": true,
"@metamask/network-controller>reselect": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true,
"browserify>assert": true,
@@ -1551,8 +1590,8 @@
"packages": {
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"node-fetch": true
}
},
@@ -1564,11 +1603,32 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1595,7 +1655,7 @@
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true,
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"bn.js": true,
"pify": true
@@ -1618,12 +1678,24 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-provider": {
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"uuid": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/network-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/network-controller>reselect": {
"globals": {
"WeakRef": true,
@@ -1832,9 +1904,9 @@
"packages": {
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
+ "@metamask/permission-controller>@metamask/rpc-errors": true,
"@metamask/permission-controller>@metamask/utils": true,
"@metamask/permission-controller>nanoid": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"deep-freeze-strict": true,
"immer": true
@@ -1848,6 +1920,27 @@
"immer": true
}
},
+ "@metamask/permission-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/permission-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1938,9 +2031,9 @@
"@metamask/eth-query>json-rpc-random-id": true,
"@metamask/ppom-validator>@metamask/base-controller": true,
"@metamask/ppom-validator>@metamask/controller-utils": true,
+ "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
- "@metamask/rpc-errors": true,
"await-semaphore": true,
"browserify>buffer": true
}
@@ -1971,6 +2064,27 @@
"eth-ens-namehash": true
}
},
+ "@metamask/ppom-validator>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/ppom-validator>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2096,8 +2210,8 @@
"@metamask/queued-request-controller": {
"packages": {
"@metamask/queued-request-controller>@metamask/base-controller": true,
+ "@metamask/queued-request-controller>@metamask/rpc-errors": true,
"@metamask/queued-request-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/selected-network-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true
}
@@ -2110,6 +2224,27 @@
"immer": true
}
},
+ "@metamask/queued-request-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/queued-request-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2131,8 +2266,8 @@
},
"packages": {
"@metamask/rate-limit-controller>@metamask/base-controller": true,
- "@metamask/rate-limit-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": true,
+ "@metamask/rate-limit-controller>@metamask/utils": true
}
},
"@metamask/rate-limit-controller>@metamask/base-controller": {
@@ -2143,6 +2278,27 @@
"immer": true
}
},
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/rate-limit-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2160,8 +2316,8 @@
},
"@metamask/rpc-errors": {
"packages": {
- "@metamask/utils": true,
- "eth-rpc-errors>fast-safe-stringify": true
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
}
},
"@metamask/rpc-methods-flask>nanoid": {
@@ -2310,11 +2466,11 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true,
"bn.js": true,
@@ -2364,6 +2520,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2449,11 +2611,11 @@
"packages": {
"@metamask/object-multiplex": true,
"@metamask/post-message-stream": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true,
"@metamask/snaps-controllers>@metamask/permission-controller": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
"@metamask/snaps-controllers>get-npm-tarball-url": true,
@@ -2486,8 +2648,14 @@
},
"@metamask/snaps-controllers>@metamask/json-rpc-engine": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
"@metamask/utils": true
}
},
@@ -2508,15 +2676,21 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>nanoid": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
}
},
+ "@metamask/snaps-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-controllers>concat-stream": {
"packages": {
"browserify>buffer": true,
@@ -2574,8 +2748,8 @@
},
"@metamask/snaps-rpc-methods": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils": true,
@@ -2590,10 +2764,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2612,12 +2786,18 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-sdk": {
"globals": {
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/snaps-sdk>@metamask/rpc-errors": true,
"@metamask/utils": true,
"@metamask/utils>@metamask/superstruct": true
}
@@ -2631,6 +2811,12 @@
"@noble/hashes": true
}
},
+ "@metamask/snaps-sdk>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils": {
"globals": {
"File": true,
@@ -2647,10 +2833,10 @@
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils>@metamask/permission-controller": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/snaps-utils>@metamask/slip44": true,
"@metamask/snaps-utils>cron-parser": true,
"@metamask/snaps-utils>fast-json-stable-stringify": true,
@@ -2680,10 +2866,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-utils>@metamask/base-controller": true,
"@metamask/snaps-utils>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2694,6 +2880,12 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-utils>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/message-signing-snap>@noble/curves": true,
@@ -2766,8 +2958,8 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"browserify>buffer": true,
@@ -2794,6 +2986,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/user-operation-controller": {
"globals": {
"fetch": true
@@ -2803,9 +3001,9 @@
"@metamask/eth-query": true,
"@metamask/gas-fee-controller": true,
"@metamask/gas-fee-controller>@metamask/polling-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller": true,
"@metamask/user-operation-controller>@metamask/base-controller": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors": true,
"@metamask/user-operation-controller>@metamask/utils": true,
"bn.js": true,
"lodash": true,
@@ -2822,6 +3020,27 @@
"immer": true
}
},
+ "@metamask/user-operation-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true
+ }
+ },
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/user-operation-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -4067,11 +4286,6 @@
"@metamask/ethjs-query": true
}
},
- "eth-rpc-errors": {
- "packages": {
- "eth-rpc-errors>fast-safe-stringify": true
- }
- },
"ethereumjs-util": {
"packages": {
"bn.js": true,
@@ -4525,8 +4739,8 @@
},
"json-rpc-engine": {
"packages": {
- "eth-rpc-errors": true,
- "json-rpc-engine>@metamask/safe-event-emitter": true
+ "json-rpc-engine>@metamask/safe-event-emitter": true,
+ "json-rpc-engine>eth-rpc-errors": true
}
},
"json-rpc-engine>@metamask/safe-event-emitter": {
@@ -4537,6 +4751,11 @@
"webpack>events": true
}
},
+ "json-rpc-engine>eth-rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
"json-rpc-middleware-stream": {
"globals": {
"console.warn": true,
diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json
index d7522783f9fc..880b542673ea 100644
--- a/lavamoat/browserify/flask/policy.json
+++ b/lavamoat/browserify/flask/policy.json
@@ -662,8 +662,8 @@
},
"packages": {
"@metamask/approval-controller>@metamask/base-controller": true,
- "@metamask/approval-controller>nanoid": true,
- "@metamask/rpc-errors": true
+ "@metamask/approval-controller>@metamask/rpc-errors": true,
+ "@metamask/approval-controller>nanoid": true
}
},
"@metamask/approval-controller>@metamask/base-controller": {
@@ -674,6 +674,12 @@
"immer": true
}
},
+ "@metamask/approval-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/approval-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@@ -699,13 +705,13 @@
"@ethersproject/providers": true,
"@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/polling-controller": true,
+ "@metamask/assets-controllers>@metamask/rpc-errors": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/eth-query": true,
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
- "@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"cockatiel": true,
@@ -727,6 +733,12 @@
"uuid": true
}
},
+ "@metamask/assets-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/base-controller": {
"globals": {
"setTimeout": true
@@ -851,11 +863,32 @@
},
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -886,14 +919,20 @@
"setTimeout": true
},
"packages": {
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-middleware>klona": true,
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true
}
},
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/eth-ledger-bridge-keyring": {
"globals": {
"addEventListener": true,
@@ -1505,9 +1544,9 @@
"@metamask/network-controller>@metamask/eth-json-rpc-infura": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware": true,
"@metamask/network-controller>@metamask/eth-json-rpc-provider": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/swappable-obj-proxy": true,
"@metamask/network-controller>reselect": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true,
"browserify>assert": true,
@@ -1551,8 +1590,8 @@
"packages": {
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"node-fetch": true
}
},
@@ -1564,11 +1603,32 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1595,7 +1655,7 @@
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true,
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"bn.js": true,
"pify": true
@@ -1618,12 +1678,24 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-provider": {
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"uuid": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/network-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/network-controller>reselect": {
"globals": {
"WeakRef": true,
@@ -1832,9 +1904,9 @@
"packages": {
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
+ "@metamask/permission-controller>@metamask/rpc-errors": true,
"@metamask/permission-controller>@metamask/utils": true,
"@metamask/permission-controller>nanoid": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"deep-freeze-strict": true,
"immer": true
@@ -1848,6 +1920,27 @@
"immer": true
}
},
+ "@metamask/permission-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/permission-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1938,9 +2031,9 @@
"@metamask/eth-query>json-rpc-random-id": true,
"@metamask/ppom-validator>@metamask/base-controller": true,
"@metamask/ppom-validator>@metamask/controller-utils": true,
+ "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
- "@metamask/rpc-errors": true,
"await-semaphore": true,
"browserify>buffer": true
}
@@ -1971,6 +2064,27 @@
"eth-ens-namehash": true
}
},
+ "@metamask/ppom-validator>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/ppom-validator>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2096,8 +2210,8 @@
"@metamask/queued-request-controller": {
"packages": {
"@metamask/queued-request-controller>@metamask/base-controller": true,
+ "@metamask/queued-request-controller>@metamask/rpc-errors": true,
"@metamask/queued-request-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/selected-network-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true
}
@@ -2110,6 +2224,27 @@
"immer": true
}
},
+ "@metamask/queued-request-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/queued-request-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2131,8 +2266,8 @@
},
"packages": {
"@metamask/rate-limit-controller>@metamask/base-controller": true,
- "@metamask/rate-limit-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": true,
+ "@metamask/rate-limit-controller>@metamask/utils": true
}
},
"@metamask/rate-limit-controller>@metamask/base-controller": {
@@ -2143,6 +2278,27 @@
"immer": true
}
},
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/rate-limit-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2160,8 +2316,8 @@
},
"@metamask/rpc-errors": {
"packages": {
- "@metamask/utils": true,
- "eth-rpc-errors>fast-safe-stringify": true
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
}
},
"@metamask/rpc-methods-flask>nanoid": {
@@ -2310,11 +2466,11 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true,
"bn.js": true,
@@ -2364,6 +2520,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2449,11 +2611,11 @@
"packages": {
"@metamask/object-multiplex": true,
"@metamask/post-message-stream": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true,
"@metamask/snaps-controllers>@metamask/permission-controller": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
"@metamask/snaps-controllers>get-npm-tarball-url": true,
@@ -2486,8 +2648,14 @@
},
"@metamask/snaps-controllers>@metamask/json-rpc-engine": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
"@metamask/utils": true
}
},
@@ -2508,15 +2676,21 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>nanoid": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
}
},
+ "@metamask/snaps-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-controllers>concat-stream": {
"packages": {
"browserify>buffer": true,
@@ -2574,8 +2748,8 @@
},
"@metamask/snaps-rpc-methods": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils": true,
@@ -2590,10 +2764,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2612,12 +2786,18 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-sdk": {
"globals": {
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/snaps-sdk>@metamask/rpc-errors": true,
"@metamask/utils": true,
"@metamask/utils>@metamask/superstruct": true
}
@@ -2631,6 +2811,12 @@
"@noble/hashes": true
}
},
+ "@metamask/snaps-sdk>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils": {
"globals": {
"File": true,
@@ -2647,10 +2833,10 @@
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils>@metamask/permission-controller": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/snaps-utils>@metamask/slip44": true,
"@metamask/snaps-utils>cron-parser": true,
"@metamask/snaps-utils>fast-json-stable-stringify": true,
@@ -2680,10 +2866,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-utils>@metamask/base-controller": true,
"@metamask/snaps-utils>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2694,6 +2880,12 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-utils>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/message-signing-snap>@noble/curves": true,
@@ -2766,8 +2958,8 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"browserify>buffer": true,
@@ -2794,6 +2986,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/user-operation-controller": {
"globals": {
"fetch": true
@@ -2803,9 +3001,9 @@
"@metamask/eth-query": true,
"@metamask/gas-fee-controller": true,
"@metamask/gas-fee-controller>@metamask/polling-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller": true,
"@metamask/user-operation-controller>@metamask/base-controller": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors": true,
"@metamask/user-operation-controller>@metamask/utils": true,
"bn.js": true,
"lodash": true,
@@ -2822,6 +3020,27 @@
"immer": true
}
},
+ "@metamask/user-operation-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true
+ }
+ },
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/user-operation-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -4067,11 +4286,6 @@
"@metamask/ethjs-query": true
}
},
- "eth-rpc-errors": {
- "packages": {
- "eth-rpc-errors>fast-safe-stringify": true
- }
- },
"ethereumjs-util": {
"packages": {
"bn.js": true,
@@ -4525,8 +4739,8 @@
},
"json-rpc-engine": {
"packages": {
- "eth-rpc-errors": true,
- "json-rpc-engine>@metamask/safe-event-emitter": true
+ "json-rpc-engine>@metamask/safe-event-emitter": true,
+ "json-rpc-engine>eth-rpc-errors": true
}
},
"json-rpc-engine>@metamask/safe-event-emitter": {
@@ -4537,6 +4751,11 @@
"webpack>events": true
}
},
+ "json-rpc-engine>eth-rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
"json-rpc-middleware-stream": {
"globals": {
"console.warn": true,
diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json
index d7522783f9fc..880b542673ea 100644
--- a/lavamoat/browserify/main/policy.json
+++ b/lavamoat/browserify/main/policy.json
@@ -662,8 +662,8 @@
},
"packages": {
"@metamask/approval-controller>@metamask/base-controller": true,
- "@metamask/approval-controller>nanoid": true,
- "@metamask/rpc-errors": true
+ "@metamask/approval-controller>@metamask/rpc-errors": true,
+ "@metamask/approval-controller>nanoid": true
}
},
"@metamask/approval-controller>@metamask/base-controller": {
@@ -674,6 +674,12 @@
"immer": true
}
},
+ "@metamask/approval-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/approval-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@@ -699,13 +705,13 @@
"@ethersproject/providers": true,
"@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/polling-controller": true,
+ "@metamask/assets-controllers>@metamask/rpc-errors": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/eth-query": true,
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
- "@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"cockatiel": true,
@@ -727,6 +733,12 @@
"uuid": true
}
},
+ "@metamask/assets-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/base-controller": {
"globals": {
"setTimeout": true
@@ -851,11 +863,32 @@
},
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -886,14 +919,20 @@
"setTimeout": true
},
"packages": {
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-middleware>klona": true,
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true
}
},
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/eth-ledger-bridge-keyring": {
"globals": {
"addEventListener": true,
@@ -1505,9 +1544,9 @@
"@metamask/network-controller>@metamask/eth-json-rpc-infura": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware": true,
"@metamask/network-controller>@metamask/eth-json-rpc-provider": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/swappable-obj-proxy": true,
"@metamask/network-controller>reselect": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true,
"browserify>assert": true,
@@ -1551,8 +1590,8 @@
"packages": {
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"node-fetch": true
}
},
@@ -1564,11 +1603,32 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1595,7 +1655,7 @@
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true,
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"bn.js": true,
"pify": true
@@ -1618,12 +1678,24 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-provider": {
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"uuid": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/network-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/network-controller>reselect": {
"globals": {
"WeakRef": true,
@@ -1832,9 +1904,9 @@
"packages": {
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
+ "@metamask/permission-controller>@metamask/rpc-errors": true,
"@metamask/permission-controller>@metamask/utils": true,
"@metamask/permission-controller>nanoid": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"deep-freeze-strict": true,
"immer": true
@@ -1848,6 +1920,27 @@
"immer": true
}
},
+ "@metamask/permission-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/permission-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1938,9 +2031,9 @@
"@metamask/eth-query>json-rpc-random-id": true,
"@metamask/ppom-validator>@metamask/base-controller": true,
"@metamask/ppom-validator>@metamask/controller-utils": true,
+ "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
- "@metamask/rpc-errors": true,
"await-semaphore": true,
"browserify>buffer": true
}
@@ -1971,6 +2064,27 @@
"eth-ens-namehash": true
}
},
+ "@metamask/ppom-validator>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/ppom-validator>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2096,8 +2210,8 @@
"@metamask/queued-request-controller": {
"packages": {
"@metamask/queued-request-controller>@metamask/base-controller": true,
+ "@metamask/queued-request-controller>@metamask/rpc-errors": true,
"@metamask/queued-request-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/selected-network-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true
}
@@ -2110,6 +2224,27 @@
"immer": true
}
},
+ "@metamask/queued-request-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/queued-request-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2131,8 +2266,8 @@
},
"packages": {
"@metamask/rate-limit-controller>@metamask/base-controller": true,
- "@metamask/rate-limit-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": true,
+ "@metamask/rate-limit-controller>@metamask/utils": true
}
},
"@metamask/rate-limit-controller>@metamask/base-controller": {
@@ -2143,6 +2278,27 @@
"immer": true
}
},
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/rate-limit-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2160,8 +2316,8 @@
},
"@metamask/rpc-errors": {
"packages": {
- "@metamask/utils": true,
- "eth-rpc-errors>fast-safe-stringify": true
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
}
},
"@metamask/rpc-methods-flask>nanoid": {
@@ -2310,11 +2466,11 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true,
"bn.js": true,
@@ -2364,6 +2520,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2449,11 +2611,11 @@
"packages": {
"@metamask/object-multiplex": true,
"@metamask/post-message-stream": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true,
"@metamask/snaps-controllers>@metamask/permission-controller": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
"@metamask/snaps-controllers>get-npm-tarball-url": true,
@@ -2486,8 +2648,14 @@
},
"@metamask/snaps-controllers>@metamask/json-rpc-engine": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
"@metamask/utils": true
}
},
@@ -2508,15 +2676,21 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>nanoid": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
}
},
+ "@metamask/snaps-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-controllers>concat-stream": {
"packages": {
"browserify>buffer": true,
@@ -2574,8 +2748,8 @@
},
"@metamask/snaps-rpc-methods": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils": true,
@@ -2590,10 +2764,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2612,12 +2786,18 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-sdk": {
"globals": {
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/snaps-sdk>@metamask/rpc-errors": true,
"@metamask/utils": true,
"@metamask/utils>@metamask/superstruct": true
}
@@ -2631,6 +2811,12 @@
"@noble/hashes": true
}
},
+ "@metamask/snaps-sdk>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils": {
"globals": {
"File": true,
@@ -2647,10 +2833,10 @@
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils>@metamask/permission-controller": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/snaps-utils>@metamask/slip44": true,
"@metamask/snaps-utils>cron-parser": true,
"@metamask/snaps-utils>fast-json-stable-stringify": true,
@@ -2680,10 +2866,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-utils>@metamask/base-controller": true,
"@metamask/snaps-utils>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2694,6 +2880,12 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-utils>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/message-signing-snap>@noble/curves": true,
@@ -2766,8 +2958,8 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"browserify>buffer": true,
@@ -2794,6 +2986,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/user-operation-controller": {
"globals": {
"fetch": true
@@ -2803,9 +3001,9 @@
"@metamask/eth-query": true,
"@metamask/gas-fee-controller": true,
"@metamask/gas-fee-controller>@metamask/polling-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller": true,
"@metamask/user-operation-controller>@metamask/base-controller": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors": true,
"@metamask/user-operation-controller>@metamask/utils": true,
"bn.js": true,
"lodash": true,
@@ -2822,6 +3020,27 @@
"immer": true
}
},
+ "@metamask/user-operation-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true
+ }
+ },
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/user-operation-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -4067,11 +4286,6 @@
"@metamask/ethjs-query": true
}
},
- "eth-rpc-errors": {
- "packages": {
- "eth-rpc-errors>fast-safe-stringify": true
- }
- },
"ethereumjs-util": {
"packages": {
"bn.js": true,
@@ -4525,8 +4739,8 @@
},
"json-rpc-engine": {
"packages": {
- "eth-rpc-errors": true,
- "json-rpc-engine>@metamask/safe-event-emitter": true
+ "json-rpc-engine>@metamask/safe-event-emitter": true,
+ "json-rpc-engine>eth-rpc-errors": true
}
},
"json-rpc-engine>@metamask/safe-event-emitter": {
@@ -4537,6 +4751,11 @@
"webpack>events": true
}
},
+ "json-rpc-engine>eth-rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
"json-rpc-middleware-stream": {
"globals": {
"console.warn": true,
diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json
index 3df824f29c78..25756f84ccc4 100644
--- a/lavamoat/browserify/mmi/policy.json
+++ b/lavamoat/browserify/mmi/policy.json
@@ -754,8 +754,8 @@
},
"packages": {
"@metamask/approval-controller>@metamask/base-controller": true,
- "@metamask/approval-controller>nanoid": true,
- "@metamask/rpc-errors": true
+ "@metamask/approval-controller>@metamask/rpc-errors": true,
+ "@metamask/approval-controller>nanoid": true
}
},
"@metamask/approval-controller>@metamask/base-controller": {
@@ -766,6 +766,12 @@
"immer": true
}
},
+ "@metamask/approval-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/approval-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@@ -791,13 +797,13 @@
"@ethersproject/providers": true,
"@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/polling-controller": true,
+ "@metamask/assets-controllers>@metamask/rpc-errors": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/eth-query": true,
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
- "@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"cockatiel": true,
@@ -819,6 +825,12 @@
"uuid": true
}
},
+ "@metamask/assets-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/base-controller": {
"globals": {
"setTimeout": true
@@ -943,11 +955,32 @@
},
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -978,14 +1011,20 @@
"setTimeout": true
},
"packages": {
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true,
"@metamask/eth-json-rpc-middleware>klona": true,
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true
}
},
+ "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/eth-ledger-bridge-keyring": {
"globals": {
"addEventListener": true,
@@ -1597,9 +1636,9 @@
"@metamask/network-controller>@metamask/eth-json-rpc-infura": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware": true,
"@metamask/network-controller>@metamask/eth-json-rpc-provider": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/swappable-obj-proxy": true,
"@metamask/network-controller>reselect": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/utils": true,
"browserify>assert": true,
@@ -1643,8 +1682,8 @@
"packages": {
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"node-fetch": true
}
},
@@ -1656,11 +1695,32 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": {
"packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true,
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -1687,7 +1747,7 @@
"@metamask/eth-json-rpc-middleware>safe-stable-stringify": true,
"@metamask/eth-sig-util": true,
"@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true,
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"bn.js": true,
"pify": true
@@ -1710,12 +1770,24 @@
},
"@metamask/network-controller>@metamask/eth-json-rpc-provider": {
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"uuid": true
}
},
+ "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/network-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/network-controller>reselect": {
"globals": {
"WeakRef": true,
@@ -1924,9 +1996,9 @@
"packages": {
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
+ "@metamask/permission-controller>@metamask/rpc-errors": true,
"@metamask/permission-controller>@metamask/utils": true,
"@metamask/permission-controller>nanoid": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"deep-freeze-strict": true,
"immer": true
@@ -1940,6 +2012,27 @@
"immer": true
}
},
+ "@metamask/permission-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/permission-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2030,9 +2123,9 @@
"@metamask/eth-query>json-rpc-random-id": true,
"@metamask/ppom-validator>@metamask/base-controller": true,
"@metamask/ppom-validator>@metamask/controller-utils": true,
+ "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
- "@metamask/rpc-errors": true,
"await-semaphore": true,
"browserify>buffer": true
}
@@ -2063,6 +2156,27 @@
"eth-ens-namehash": true
}
},
+ "@metamask/ppom-validator>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/ppom-validator>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2188,8 +2302,8 @@
"@metamask/queued-request-controller": {
"packages": {
"@metamask/queued-request-controller>@metamask/base-controller": true,
+ "@metamask/queued-request-controller>@metamask/rpc-errors": true,
"@metamask/queued-request-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true,
"@metamask/selected-network-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true
}
@@ -2202,6 +2316,27 @@
"immer": true
}
},
+ "@metamask/queued-request-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/queued-request-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2223,8 +2358,8 @@
},
"packages": {
"@metamask/rate-limit-controller>@metamask/base-controller": true,
- "@metamask/rate-limit-controller>@metamask/utils": true,
- "@metamask/rpc-errors": true
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": true,
+ "@metamask/rate-limit-controller>@metamask/utils": true
}
},
"@metamask/rate-limit-controller>@metamask/base-controller": {
@@ -2235,6 +2370,27 @@
"immer": true
}
},
+ "@metamask/rate-limit-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true,
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
+ "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/rate-limit-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2252,8 +2408,8 @@
},
"@metamask/rpc-errors": {
"packages": {
- "@metamask/utils": true,
- "eth-rpc-errors>fast-safe-stringify": true
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
}
},
"@metamask/rpc-methods-flask>nanoid": {
@@ -2402,11 +2558,11 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true,
"@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true,
"bn.js": true,
@@ -2456,6 +2612,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -2541,11 +2703,11 @@
"packages": {
"@metamask/object-multiplex": true,
"@metamask/post-message-stream": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true,
"@metamask/snaps-controllers>@metamask/permission-controller": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
"@metamask/snaps-controllers>get-npm-tarball-url": true,
@@ -2578,8 +2740,14 @@
},
"@metamask/snaps-controllers>@metamask/json-rpc-engine": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/safe-event-emitter": true,
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true,
+ "@metamask/utils": true
+ }
+ },
+ "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
"@metamask/utils": true
}
},
@@ -2600,15 +2768,21 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
+ "@metamask/snaps-controllers>@metamask/rpc-errors": true,
"@metamask/snaps-controllers>nanoid": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
}
},
+ "@metamask/snaps-controllers>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-controllers>concat-stream": {
"packages": {
"browserify>buffer": true,
@@ -2666,8 +2840,8 @@
},
"@metamask/snaps-rpc-methods": {
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils": true,
@@ -2682,10 +2856,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2704,12 +2878,18 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-rpc-methods>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-sdk": {
"globals": {
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
+ "@metamask/snaps-sdk>@metamask/rpc-errors": true,
"@metamask/utils": true,
"@metamask/utils>@metamask/superstruct": true
}
@@ -2723,6 +2903,12 @@
"@noble/hashes": true
}
},
+ "@metamask/snaps-sdk>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils": {
"globals": {
"File": true,
@@ -2739,10 +2925,10 @@
"fetch": true
},
"packages": {
- "@metamask/rpc-errors": true,
"@metamask/snaps-sdk": true,
"@metamask/snaps-sdk>@metamask/key-tree": true,
"@metamask/snaps-utils>@metamask/permission-controller": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/snaps-utils>@metamask/slip44": true,
"@metamask/snaps-utils>cron-parser": true,
"@metamask/snaps-utils>fast-json-stable-stringify": true,
@@ -2772,10 +2958,10 @@
},
"packages": {
"@metamask/controller-utils": true,
- "@metamask/rpc-errors": true,
"@metamask/snaps-controllers>@metamask/json-rpc-engine": true,
"@metamask/snaps-utils>@metamask/base-controller": true,
"@metamask/snaps-utils>@metamask/permission-controller>nanoid": true,
+ "@metamask/snaps-utils>@metamask/rpc-errors": true,
"@metamask/utils": true,
"deep-freeze-strict": true,
"immer": true
@@ -2786,6 +2972,12 @@
"crypto.getRandomValues": true
}
},
+ "@metamask/snaps-utils>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/message-signing-snap>@noble/curves": true,
@@ -2858,8 +3050,8 @@
"@metamask/metamask-eth-abis": true,
"@metamask/name-controller>async-mutex": true,
"@metamask/network-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller>@metamask/nonce-tracker": true,
+ "@metamask/transaction-controller>@metamask/rpc-errors": true,
"@metamask/utils": true,
"bn.js": true,
"browserify>buffer": true,
@@ -2886,6 +3078,12 @@
"@swc/helpers>tslib": true
}
},
+ "@metamask/transaction-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/utils": true
+ }
+ },
"@metamask/user-operation-controller": {
"globals": {
"fetch": true
@@ -2895,9 +3093,9 @@
"@metamask/eth-query": true,
"@metamask/gas-fee-controller": true,
"@metamask/gas-fee-controller>@metamask/polling-controller": true,
- "@metamask/rpc-errors": true,
"@metamask/transaction-controller": true,
"@metamask/user-operation-controller>@metamask/base-controller": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors": true,
"@metamask/user-operation-controller>@metamask/utils": true,
"bn.js": true,
"lodash": true,
@@ -2914,6 +3112,27 @@
"immer": true
}
},
+ "@metamask/user-operation-controller>@metamask/rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true,
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true
+ }
+ },
+ "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": {
+ "globals": {
+ "TextDecoder": true,
+ "TextEncoder": true
+ },
+ "packages": {
+ "@metamask/utils>@metamask/superstruct": true,
+ "@metamask/utils>@scure/base": true,
+ "@metamask/utils>pony-cause": true,
+ "@noble/hashes": true,
+ "browserify>buffer": true,
+ "nock>debug": true,
+ "semver": true
+ }
+ },
"@metamask/user-operation-controller>@metamask/utils": {
"globals": {
"TextDecoder": true,
@@ -4159,11 +4378,6 @@
"@metamask/ethjs-query": true
}
},
- "eth-rpc-errors": {
- "packages": {
- "eth-rpc-errors>fast-safe-stringify": true
- }
- },
"ethereumjs-util": {
"packages": {
"bn.js": true,
@@ -4617,8 +4831,8 @@
},
"json-rpc-engine": {
"packages": {
- "eth-rpc-errors": true,
- "json-rpc-engine>@metamask/safe-event-emitter": true
+ "json-rpc-engine>@metamask/safe-event-emitter": true,
+ "json-rpc-engine>eth-rpc-errors": true
}
},
"json-rpc-engine>@metamask/safe-event-emitter": {
@@ -4629,6 +4843,11 @@
"webpack>events": true
}
},
+ "json-rpc-engine>eth-rpc-errors": {
+ "packages": {
+ "@metamask/rpc-errors>fast-safe-stringify": true
+ }
+ },
"json-rpc-middleware-stream": {
"globals": {
"console.warn": true,
diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json
index 6e3b319da1e8..e7ce64ceec23 100644
--- a/lavamoat/build-system/policy.json
+++ b/lavamoat/build-system/policy.json
@@ -1995,7 +1995,7 @@
"Buffer.isBuffer": true
},
"packages": {
- "eth-rpc-errors>fast-safe-stringify": true
+ "@metamask/rpc-errors>fast-safe-stringify": true
}
},
"browserify>string_decoder": {
diff --git a/package.json b/package.json
index 652b8d4b3afb..7efae54424ff 100644
--- a/package.json
+++ b/package.json
@@ -346,7 +346,7 @@
"@metamask/providers": "^14.0.2",
"@metamask/queued-request-controller": "^2.0.0",
"@metamask/rate-limit-controller": "^6.0.0",
- "@metamask/rpc-errors": "^6.2.1",
+ "@metamask/rpc-errors": "^7.0.0",
"@metamask/safe-event-emitter": "^3.1.1",
"@metamask/scure-bip39": "^2.0.3",
"@metamask/selected-network-controller": "^18.0.1",
@@ -390,7 +390,6 @@
"eth-ens-namehash": "^2.0.8",
"eth-lattice-keyring": "^0.12.4",
"eth-method-registry": "^4.0.0",
- "eth-rpc-errors": "^4.0.2",
"ethereumjs-util": "^7.0.10",
"extension-port-stream": "^3.0.0",
"fast-json-patch": "^3.1.1",
diff --git a/shared/modules/error.test.ts b/shared/modules/error.test.ts
index 247ef302d09e..7fab2ee4e2e4 100644
--- a/shared/modules/error.test.ts
+++ b/shared/modules/error.test.ts
@@ -24,7 +24,7 @@ describe('error module', () => {
expect(log.error).toHaveBeenCalledWith('test');
});
- it('calls loglevel.error with the parameter passed in when parameter is not an instance of Error', () => {
+ it('calls loglevel.error with string representation of parameter passed in when parameter is not an instance of Error', () => {
logErrorWithMessage({ test: 'test' });
expect(log.error).toHaveBeenCalledWith({ test: 'test' });
});
diff --git a/shared/modules/error.ts b/shared/modules/error.ts
index fa212365570f..04b754625257 100644
--- a/shared/modules/error.ts
+++ b/shared/modules/error.ts
@@ -1,24 +1,33 @@
import log from 'loglevel';
+import {
+ getErrorMessage as _getErrorMessage,
+ hasProperty,
+ isObject,
+ isErrorWithMessage,
+} from '@metamask/utils';
+
+export { isErrorWithMessage } from '@metamask/utils';
/**
- * Type guard for determining whether the given value is an error object with a
- * `message` property, such as an instance of Error.
- *
- * TODO: Remove once this becomes available at @metamask/utils
+ * Attempts to obtain the message from a possible error object, defaulting to an
+ * empty string if it is impossible to do so.
*
- * @param error - The object to check.
- * @returns True or false, depending on the result.
+ * @param error - The possible error to get the message from.
+ * @returns The message if `error` is an object with a `message` property;
+ * the string version of `error` if it is not `undefined` or `null`; otherwise
+ * an empty string.
*/
-export function isErrorWithMessage(
- error: unknown,
-): error is { message: string } {
- return typeof error === 'object' && error !== null && 'message' in error;
+// TODO: Remove completely once changes implemented in @metamask/utils
+export function getErrorMessage(error: unknown): string {
+ return isErrorWithMessage(error) &&
+ hasProperty(error, 'cause') &&
+ isObject(error.cause) &&
+ hasProperty(error.cause, 'message') &&
+ typeof error.cause.message === 'string'
+ ? error.cause.message
+ : _getErrorMessage(error);
}
export function logErrorWithMessage(error: unknown) {
- if (isErrorWithMessage(error)) {
- log.error(error.message);
- } else {
- log.error(error);
- }
+ log.error(isErrorWithMessage(error) ? getErrorMessage(error) : error);
}
diff --git a/test/e2e/tests/dapp-interactions/provider-api.spec.js b/test/e2e/tests/dapp-interactions/provider-api.spec.js
index 80cca60afb95..1c20b9fb2f6e 100644
--- a/test/e2e/tests/dapp-interactions/provider-api.spec.js
+++ b/test/e2e/tests/dapp-interactions/provider-api.spec.js
@@ -1,5 +1,5 @@
const { strict: assert } = require('assert');
-const { errorCodes } = require('eth-rpc-errors');
+const { errorCodes } = require('@metamask/rpc-errors');
const {
defaultGanacheOptions,
withFixtures,
diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js
index 40d577380f49..ad0dff033ae9 100644
--- a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js
+++ b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import { getCurrentQRHardwareState } from '../../../selectors';
import Popover from '../../ui/popover';
import { useI18nContext } from '../../../hooks/useI18nContext';
@@ -44,7 +44,7 @@ const QRHardwarePopover = () => {
dispatch(
rejectPendingApproval(
_txData.id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
),
);
dispatch(cancelTx(_txData));
diff --git a/ui/components/multichain/import-account/import-account.js b/ui/components/multichain/import-account/import-account.js
index cf5d13494e07..a37958074003 100644
--- a/ui/components/multichain/import-account/import-account.js
+++ b/ui/components/multichain/import-account/import-account.js
@@ -1,6 +1,7 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
+import { getErrorMessage } from '../../../../shared/modules/error';
import {
MetaMetricsEventAccountImportType,
MetaMetricsEventAccountType,
@@ -50,8 +51,9 @@ export const ImportAccount = ({ onActionComplete }) => {
return false;
}
} catch (error) {
- trackImportEvent(strategy, error.message);
- translateWarning(error.message);
+ const message = getErrorMessage(error);
+ trackImportEvent(strategy, message);
+ translateWarning(message);
return false;
}
diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js
index 3d40209ba62b..8b85e5cf1a4b 100644
--- a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js
+++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React, { useContext, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
+import { getErrorMessage } from '../../../../shared/modules/error';
import {
MetaMetricsEventName,
MetaMetricsTokenEventSource,
@@ -95,7 +96,7 @@ export const ImportNftsModal = ({ onClose }) => {
dispatch(updateNftDropDownState(newNftDropdownState));
} catch (error) {
- const { message } = error;
+ const message = getErrorMessage(error);
dispatch(setNewNftAddedMessage(message));
setNftAddFailed(true);
return;
diff --git a/ui/ducks/send/helpers.js b/ui/ducks/send/helpers.js
index 43582c6e3504..b48e05034f59 100644
--- a/ui/ducks/send/helpers.js
+++ b/ui/ducks/send/helpers.js
@@ -2,6 +2,7 @@ import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util';
import abi from 'human-standard-token-abi';
import BigNumber from 'bignumber.js';
import { TransactionEnvelopeType } from '@metamask/transaction-controller';
+import { getErrorMessage } from '../../../shared/modules/error';
import { GAS_LIMITS, MIN_GAS_LIMIT_HEX } from '../../../shared/constants/gas';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
@@ -157,13 +158,14 @@ export async function estimateGasLimitForSend({
);
return addHexPrefix(estimateWithBuffer);
} catch (error) {
+ const errorMessage = getErrorMessage(error);
const simulationFailed =
- error.message.includes('Transaction execution error.') ||
- error.message.includes(
+ errorMessage.includes('Transaction execution error.') ||
+ errorMessage.includes(
'gas required exceeds allowance or always failing transaction',
) ||
(CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId] &&
- error.message.includes('gas required exceeds allowance'));
+ errorMessage.includes('gas required exceeds allowance'));
if (simulationFailed) {
const estimateWithBuffer = addGasBuffer(
paramsForGasEstimate?.gas ?? gasLimit,
diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js
index cdbe7d2daa86..700fd466004d 100644
--- a/ui/ducks/send/send.js
+++ b/ui/ducks/send/send.js
@@ -7,11 +7,12 @@ import BigNumber from 'bignumber.js';
import { addHexPrefix, zeroAddress } from 'ethereumjs-util';
import { cloneDeep, debounce } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
+import { providerErrors } from '@metamask/rpc-errors';
import {
TransactionEnvelopeType,
TransactionType,
} from '@metamask/transaction-controller';
-import { ethErrors } from 'eth-rpc-errors';
+import { getErrorMessage } from '../../../shared/modules/error';
import {
decimalToHex,
hexToDecimal,
@@ -2702,12 +2703,13 @@ export function updateSendAsset(
details.tokenId,
);
} catch (err) {
- if (err.message.includes('Unable to verify ownership.')) {
+ const message = getErrorMessage(err);
+ if (message.includes('Unable to verify ownership.')) {
// this would indicate that either our attempts to verify ownership failed because of network issues,
// or, somehow a token has been added to NFTs state with an incorrect chainId.
} else {
// Any other error is unexpected and should be surfaced.
- dispatch(displayWarning(err.message));
+ dispatch(displayWarning(err));
}
}
@@ -2965,7 +2967,7 @@ export function signTransaction(history) {
await dispatch(
rejectPendingApproval(
unapprovedSendTx.id,
- ethErrors.provider.userRejectedRequest().serialize(),
+ providerErrors.userRejectedRequest().serialize(),
),
);
}
diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js
index 822db143d29a..dda856d64abd 100644
--- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js
+++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js
@@ -1,7 +1,7 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import { getTokenTrackerLink } from '@metamask/etherscan-link';
import classnames from 'classnames';
import { PageContainerFooter } from '../../components/ui/page-container';
@@ -125,7 +125,7 @@ const ConfirmAddSuggestedNFT = () => {
return dispatch(
rejectPendingApproval(
id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
),
);
}),
@@ -421,7 +421,7 @@ const ConfirmAddSuggestedNFT = () => {
rejectPendingApproval(
id,
serializeError(
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
),
),
);
diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js
index c3e1a3f73bf0..f099ea80bfd5 100644
--- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js
+++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js
@@ -1,7 +1,7 @@
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import {
BannerAlert,
Button,
@@ -147,7 +147,7 @@ const ConfirmAddSuggestedToken = () => {
dispatch(
rejectPendingApproval(
id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
),
),
),
diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx
index cc9b39609030..a37812899ec9 100644
--- a/ui/pages/confirmations/components/confirm/footer/footer.tsx
+++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx
@@ -1,5 +1,5 @@
import { TransactionMeta } from '@metamask/transaction-controller';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ConfirmAlertModal } from '../../../../../components/app/alert-system/confirm-alert-modal';
@@ -201,7 +201,7 @@ const Footer = () => {
return;
}
- const error = ethErrors.provider.userRejectedRequest();
+ const error = providerErrors.userRejectedRequest();
error.data = { location };
dispatch(
diff --git a/ui/pages/confirmations/components/confirm/nav/nav.tsx b/ui/pages/confirmations/components/confirm/nav/nav.tsx
index 6546b882b784..de0637a9f641 100644
--- a/ui/pages/confirmations/components/confirm/nav/nav.tsx
+++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx
@@ -1,4 +1,4 @@
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
@@ -78,7 +78,7 @@ const Nav = () => {
dispatch(
rejectPendingApproval(
conf.id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
),
);
});
diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js
index f9c9dbe9c0a1..026135a52685 100644
--- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js
+++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ObjectInspector } from 'react-inspector';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import { SubjectType } from '@metamask/permission-controller';
import LedgerInstructionField from '../ledger-instruction-field';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
@@ -275,7 +275,7 @@ export default class SignatureRequestOriginal extends Component {
await rejectPendingApproval(
id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
);
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
@@ -304,7 +304,7 @@ export default class SignatureRequestOriginal extends Component {
onCancel={async () => {
await rejectPendingApproval(
txData.id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
);
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js
index 817f9f8699d4..0ac6b877fa72 100644
--- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js
+++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js
@@ -11,6 +11,7 @@ import {
} from '../../../../store/actions';
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
// eslint-disable-next-line import/order
+import { getErrorMessage } from '../../../../../shared/modules/error';
import {
mmiActionsFactory,
setPersonalMessageInProgress,
@@ -173,7 +174,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
} catch (err) {
await dispatchProps.setWaitForConfirmDeepLinkDialog(true);
await dispatchProps.showTransactionsFailedModal({
- errorMessage: err.message,
+ errorMessage: getErrorMessage(err),
closeNotification: true,
operationFailed: true,
});
diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js
index 1ade6dd1a630..e1effe5c8ff3 100644
--- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js
+++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js
@@ -4,7 +4,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import log from 'loglevel';
import { isValidSIWEOrigin } from '@metamask/controller-utils';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import { BannerAlert, Text } from '../../../../components/component-library';
import Popover from '../../../../components/ui/popover';
import Checkbox from '../../../../components/ui/check-box';
@@ -102,7 +102,7 @@ export default function SignatureRequestSIWE({ txData, warnings }) {
await dispatch(
rejectPendingApproval(
id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
),
);
} catch (e) {
diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js
index f15c7045e2d7..ce6967b50f70 100644
--- a/ui/pages/confirmations/components/signature-request/signature-request.js
+++ b/ui/pages/confirmations/components/signature-request/signature-request.js
@@ -8,7 +8,7 @@ import {
} from 'react-redux';
import PropTypes from 'prop-types';
import { memoize } from 'lodash';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import {
resolvePendingApproval,
completedTx,
@@ -176,7 +176,7 @@ const SignatureRequest = ({ txData, warnings }) => {
await dispatch(
rejectPendingApproval(
id,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
),
);
trackEvent({
diff --git a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js
index d14048897e39..998751c6e3a7 100644
--- a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js
+++ b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import React from 'react';
import { RpcEndpointType } from '@metamask/network-controller';
@@ -564,7 +564,7 @@ function getValues(pendingApproval, t, actions, history, data) {
onCancel: () =>
actions.rejectPendingApproval(
pendingApproval.id,
- ethErrors.provider.userRejectedRequest().serialize(),
+ providerErrors.userRejectedRequest().serialize(),
),
networkDisplay: !originIsMetaMask,
};
diff --git a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js
index c03d03d3891a..7dbdb8c9c757 100644
--- a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js
+++ b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js
@@ -1,4 +1,4 @@
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import {
JustifyContent,
SEVERITIES,
@@ -85,7 +85,7 @@ function getValues(pendingApproval, t, actions) {
onCancel: () =>
actions.rejectPendingApproval(
pendingApproval.id,
- ethErrors.provider.userRejectedRequest().serialize(),
+ providerErrors.userRejectedRequest().serialize(),
),
networkDisplay: true,
};
diff --git a/ui/pages/error/error.component.js b/ui/pages/error/error.component.js
index 57a8e40c6473..f7ab9c593d40 100644
--- a/ui/pages/error/error.component.js
+++ b/ui/pages/error/error.component.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
// 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,
@@ -72,6 +73,7 @@ class ErrorPage extends PureComponent {
const message = isPopup
? t('errorPagePopupMessage', [supportLink])
: t('errorPageMessage', [supportLink]);
+ const errorMessage = getErrorMessage(error);
return (
@@ -81,8 +83,8 @@ class ErrorPage extends PureComponent {
{t('errorDetails')}
- {error.message
- ? this.renderErrorDetail(t('errorMessage', [error.message]))
+ {errorMessage
+ ? this.renderErrorDetail(t('errorMessage', [errorMessage]))
: null}
{error.code
? this.renderErrorDetail(t('errorCode', [error.code]))
diff --git a/ui/pages/keychains/reveal-seed.js b/ui/pages/keychains/reveal-seed.js
index 492f37545138..cf3e285eba64 100644
--- a/ui/pages/keychains/reveal-seed.js
+++ b/ui/pages/keychains/reveal-seed.js
@@ -2,6 +2,7 @@ import qrCode from 'qrcode-generator';
import React, { useContext, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
+import { getErrorMessage } from '../../../shared/modules/error';
import {
MetaMetricsEventCategory,
MetaMetricsEventKeyType,
@@ -97,7 +98,7 @@ export default function RevealSeedPage() {
reason: e.message, // 'incorrect_password',
},
});
- setError(e.message);
+ setError(getErrorMessage(e));
});
};
diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js
index 417a82777b36..e32f85609406 100644
--- a/ui/pages/permissions-connect/permissions-connect.component.js
+++ b/ui/pages/permissions-connect/permissions-connect.component.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
import { SubjectType } from '@metamask/permission-controller';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
@@ -443,7 +443,7 @@ export default class PermissionConnect extends Component {
rejectSnapInstall={(requestId) => {
rejectPendingApproval(
requestId,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
);
this.setState({ permissionsApproved: true });
}}
@@ -469,7 +469,7 @@ export default class PermissionConnect extends Component {
rejectSnapUpdate={(requestId) => {
rejectPendingApproval(
requestId,
- serializeError(ethErrors.provider.userRejectedRequest()),
+ serializeError(providerErrors.userRejectedRequest()),
);
this.setState({ permissionsApproved: false });
}}
diff --git a/ui/store/actions.ts b/ui/store/actions.ts
index 9c5ab7ebb45e..43c7fb189822 100644
--- a/ui/store/actions.ts
+++ b/ui/store/actions.ts
@@ -9,7 +9,8 @@ import { captureException } from '@sentry/browser';
import { capitalize, isEqual } from 'lodash';
import { ThunkAction } from 'redux-thunk';
import { Action, AnyAction } from 'redux';
-import { ethErrors, serializeError } from 'eth-rpc-errors';
+import { providerErrors, serializeError } from '@metamask/rpc-errors';
+import type { DataWithOptionalCause } from '@metamask/rpc-errors';
import type { Hex, Json } from '@metamask/utils';
import {
AssetsContractController,
@@ -111,6 +112,7 @@ import {
import { decimalToHex } from '../../shared/modules/conversion.utils';
import { TxGasFees, PriorityLevels } from '../../shared/constants/gas';
import {
+ getErrorMessage,
isErrorWithMessage,
logErrorWithMessage,
} from '../../shared/modules/error';
@@ -228,7 +230,7 @@ export function createNewVaultAndRestore(
dispatch(hideLoadingIndication());
})
.catch((err) => {
- dispatch(displayWarning(err.message));
+ dispatch(displayWarning(err));
dispatch(hideLoadingIndication());
return Promise.reject(err);
});
@@ -248,7 +250,7 @@ export function createNewVaultAndGetSeedPhrase(
} catch (error) {
dispatch(displayWarning(error));
if (isErrorWithMessage(error)) {
- throw new Error(error.message);
+ throw new Error(getErrorMessage(error));
} else {
throw error;
}
@@ -272,7 +274,7 @@ export function unlockAndGetSeedPhrase(
} catch (error) {
dispatch(displayWarning(error));
if (isErrorWithMessage(error)) {
- throw new Error(error.message);
+ throw new Error(getErrorMessage(error));
} else {
throw error;
}
@@ -375,7 +377,7 @@ export function resetAccount(): ThunkAction<
dispatch(hideLoadingIndication());
if (err) {
if (isErrorWithMessage(err)) {
- dispatch(displayWarning(err.message));
+ dispatch(displayWarning(err));
}
reject(err);
return;
@@ -575,11 +577,12 @@ export function connectHardware(
);
} catch (error) {
logErrorWithMessage(error);
+ const message = getErrorMessage(error);
if (
deviceName === HardwareDeviceNames.ledger &&
ledgerTransportType === LedgerTransportTypes.webhid &&
isErrorWithMessage(error) &&
- error.message.match('Failed to open the device')
+ message.match('Failed to open the device')
) {
dispatch(displayWarning(t('ledgerDeviceOpenFailureMessage')));
throw new Error(t('ledgerDeviceOpenFailureMessage'));
@@ -1378,10 +1381,7 @@ export function cancelTx(
return new Promise((resolve, reject) => {
callBackgroundMethod(
'rejectPendingApproval',
- [
- String(txMeta.id),
- ethErrors.provider.userRejectedRequest().serialize(),
- ],
+ [String(txMeta.id), providerErrors.userRejectedRequest().serialize()],
(error) => {
if (error) {
reject(error);
@@ -1427,10 +1427,7 @@ export function cancelTxs(
new Promise((resolve, reject) => {
callBackgroundMethod(
'rejectPendingApproval',
- [
- String(id),
- ethErrors.provider.userRejectedRequest().serialize(),
- ],
+ [String(id), providerErrors.userRejectedRequest().serialize()],
(err) => {
if (err) {
reject(err);
@@ -1666,7 +1663,7 @@ export function lockMetamask(): ThunkAction<
return backgroundSetLocked()
.then(() => forceUpdateMetamaskState(dispatch))
.catch((error) => {
- dispatch(displayWarning(error.message));
+ dispatch(displayWarning(getErrorMessage(error)));
return Promise.reject(error);
})
.then(() => {
@@ -2059,15 +2056,17 @@ export function addNftVerifyOwnership(
tokenID,
]);
} catch (error) {
- if (
- isErrorWithMessage(error) &&
- (error.message.includes('This NFT is not owned by the user') ||
- error.message.includes('Unable to verify ownership'))
- ) {
- throw error;
- } else {
- logErrorWithMessage(error);
- dispatch(displayWarning(error));
+ if (isErrorWithMessage(error)) {
+ const message = getErrorMessage(error);
+ if (
+ message.includes('This NFT is not owned by the user') ||
+ message.includes('Unable to verify ownership')
+ ) {
+ throw error;
+ } else {
+ logErrorWithMessage(error);
+ dispatch(displayWarning(error));
+ }
}
} finally {
await forceUpdateMetamaskState(dispatch);
@@ -2810,7 +2809,8 @@ export function displayWarning(payload: unknown): PayloadAction {
if (isErrorWithMessage(payload)) {
return {
type: actionConstants.DISPLAY_WARNING,
- payload: payload.message,
+ payload:
+ (payload as DataWithOptionalCause)?.cause?.message || payload.message,
};
} else if (typeof payload === 'string') {
return {
@@ -4061,7 +4061,7 @@ export function rejectAllMessages(
): ThunkAction {
return async (dispatch: MetaMaskReduxDispatch) => {
const userRejectionError = serializeError(
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
await Promise.all(
messageList.map(
diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts
index 1604953f9b99..c1d2cfa062a5 100644
--- a/ui/store/institutional/institution-background.ts
+++ b/ui/store/institutional/institution-background.ts
@@ -12,7 +12,10 @@ import {
submitRequestToBackground,
} from '../background-connection';
import { MetaMaskReduxDispatch, MetaMaskReduxState } from '../store';
-import { isErrorWithMessage } from '../../../shared/modules/error';
+import {
+ isErrorWithMessage,
+ getErrorMessage,
+} from '../../../shared/modules/error';
import { ConnectionRequest } from '../../../shared/constants/mmi-controller';
export function showInteractiveReplacementTokenBanner({
@@ -34,8 +37,8 @@ export function showInteractiveReplacementTokenBanner({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (err) {
- dispatch(displayWarning(err.message));
- throw new Error(err.message);
+ dispatch(displayWarning(err));
+ throw new Error(getErrorMessage(err));
}
}
};
@@ -80,7 +83,7 @@ export function setTypedMessageInProgress(msgId: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
log.error(error);
- dispatch(displayWarning(error.message));
+ dispatch(displayWarning(error));
} finally {
await forceUpdateMetamaskState(dispatch);
dispatch(hideLoadingIndication());
@@ -97,7 +100,7 @@ export function setPersonalMessageInProgress(msgId: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
log.error(error);
- dispatch(displayWarning(error.message));
+ dispatch(displayWarning(error));
} finally {
await forceUpdateMetamaskState(dispatch);
dispatch(hideLoadingIndication());
@@ -135,7 +138,7 @@ export function mmiActionsFactory() {
} catch (error) {
dispatch(displayWarning(error));
if (isErrorWithMessage(error)) {
- throw new Error(error.message);
+ throw new Error(getErrorMessage(error));
} else {
throw error;
}
@@ -157,7 +160,7 @@ export function mmiActionsFactory() {
return () => {
callBackgroundMethod(name, [payload], (err) => {
if (isErrorWithMessage(err)) {
- throw new Error(err.message);
+ throw new Error(getErrorMessage(err));
}
});
};
diff --git a/yarn.lock b/yarn.lock
index 6d5582a99ee1..825c8bf06d8a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6170,12 +6170,22 @@ __metadata:
linkType: hard
"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1":
- version: 6.3.1
- resolution: "@metamask/rpc-errors@npm:6.3.1"
+ version: 6.4.0
+ resolution: "@metamask/rpc-errors@npm:6.4.0"
+ dependencies:
+ "@metamask/utils": "npm:^9.0.0"
+ fast-safe-stringify: "npm:^2.0.6"
+ checksum: 10/9a17525aa8ce9ac142a94c04000dba7f0635e8e155c6c045f57eca36cc78c255318cca2fad4571719a427dfd2df64b70bc6442989523a8de555480668d666ad5
+ languageName: node
+ linkType: hard
+
+"@metamask/rpc-errors@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "@metamask/rpc-errors@npm:7.0.0"
dependencies:
"@metamask/utils": "npm:^9.0.0"
fast-safe-stringify: "npm:^2.0.6"
- checksum: 10/f968fb490b13b632c2ad4770a144d67cecdff8d539cb8b489c732b08dab7a62fae65d7a2908ce8c5b77260317aa618948a52463f093fa8d9f84aee1c5f6f5daf
+ checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd
languageName: node
linkType: hard
@@ -26152,7 +26162,7 @@ __metadata:
"@metamask/providers": "npm:^14.0.2"
"@metamask/queued-request-controller": "npm:^2.0.0"
"@metamask/rate-limit-controller": "npm:^6.0.0"
- "@metamask/rpc-errors": "npm:^6.2.1"
+ "@metamask/rpc-errors": "npm:^7.0.0"
"@metamask/safe-event-emitter": "npm:^3.1.1"
"@metamask/scure-bip39": "npm:^2.0.3"
"@metamask/selected-network-controller": "npm:^18.0.1"
@@ -26306,7 +26316,6 @@ __metadata:
eth-ens-namehash: "npm:^2.0.8"
eth-lattice-keyring: "npm:^0.12.4"
eth-method-registry: "npm:^4.0.0"
- eth-rpc-errors: "npm:^4.0.2"
ethereumjs-util: "npm:^7.0.10"
ethers: "npm:5.7.0"
extension-port-stream: "npm:^3.0.0"
From 55d09729f2bdc2359094c5524b6b43c5e00cde94 Mon Sep 17 00:00:00 2001
From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com>
Date: Thu, 17 Oct 2024 07:29:50 +0400
Subject: [PATCH 18/51] fix: SonarCloud for forks (#27700)
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/27700?quickstart=1)
This PR fixes SonarCloud for forks.
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27135
## **Manual testing steps**
1. SonarCloud analysis is successfully reported from a fork
## **Screenshots/Recordings**
Not applicable
## **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: Erik Marks <25517051+rekmarks@users.noreply.github.com>
Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
Co-authored-by: Mark Stacey
---
.github/workflows/main.yml | 9 --------
.github/workflows/sonarcloud.yml | 36 ++++++++++++++++++++++++++++----
sonar-project.properties | 4 ++++
3 files changed, 36 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5d1b4d73bdab..f3cc68bebcec 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -32,21 +32,12 @@ jobs:
name: Run tests
uses: ./.github/workflows/run-tests.yml
- sonarcloud:
- name: SonarCloud
- uses: ./.github/workflows/sonarcloud.yml
- secrets:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- needs:
- - run-tests
-
all-jobs-completed:
name: All jobs completed
runs-on: ubuntu-latest
needs:
- check-workflows
- run-tests
- - sonarcloud
outputs:
PASSED: ${{ steps.set-output.outputs.PASSED }}
steps:
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index 460d5c140462..9ca9f02e2ae5 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -1,19 +1,33 @@
+# This GitHub action will checkout and scan third party code.
+# Please ensure that any changes to this action do not perform
+# actions that may result in code from that branch being executed
+# such as installing dependencies or running build scripts.
+
name: SonarCloud
on:
- workflow_call:
- secrets:
- SONAR_TOKEN:
- required: true
+ workflow_run:
+ workflows:
+ - Run tests
+ types:
+ - completed
+
+permissions:
+ actions: read
jobs:
sonarcloud:
+ # Only scan code from non-forked repositories that have passed the tests
+ # This will skip scanning the code for forks, but will run for the main repository on PRs from forks
+ if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.repository.fork == false }}
name: SonarCloud
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
+ repository: ${{ github.event.workflow_run.head_repository.full_name }} # Use the repository that triggered the workflow
+ ref: ${{ github.event.workflow_run.head_branch }} # Use the branch that triggered the workflow
fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis
- name: Download artifacts
@@ -21,6 +35,20 @@ jobs:
with:
name: lcov.info
path: coverage
+ github-token: ${{ github.token }} # This is required when downloading artifacts from a different repository or from a different workflow run.
+ run-id: ${{ github.event.workflow_run.id }} # Use the workflow id that triggered the workflow
+
+ - name: Download sonar-project.properties
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPOSITORY: MetaMask/metamask-extension
+ run: |
+ sonar_project_properties=$(gh api -H "Accept: application/vnd.github.raw" "repos/$REPOSITORY/contents/sonar-project.properties")
+ if [ -z "$sonar_project_properties" ]; then
+ echo "::error::sonar-project.properties not found in $REPOSITORY. Please make sure this file exists on the default branch."
+ exit 1
+ fi
+ echo "$sonar_project_properties" > sonar-project.properties
- name: SonarCloud Scan
# This is SonarSource/sonarcloud-github-action@v2.0.0
diff --git a/sonar-project.properties b/sonar-project.properties
index ad18a60d6fc7..4362539a94ff 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,3 +1,7 @@
+# Note: Updating this file on feature branches or forks will not reflect changes in the SonarCloud scan results.
+# The SonarCloud scan workflow always uses the latest version from the default branch.
+# This means any changes made to this file in a feature branch will not be considered until they are merged.
+
sonar.projectKey=metamask-extension
sonar.organization=consensys
From 01ea106de1f6bcd2b122d3b9b8c3fc862591f35a Mon Sep 17 00:00:00 2001
From: legobeat <109787230+legobeat@users.noreply.github.com>
Date: Thu, 17 Oct 2024 04:16:11 +0000
Subject: [PATCH 19/51] fix: fall back to bundled chainlist (#23392)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
The list of known chains is fetched at runtime from
`https://chainid.network/chains.json` and cached. There are some issues
with the way this works:
- MetaMask will not have a list of chains until online (if ever)
- When the 24h cache timeout expires, the chainlist becomes unavailable
This PR addresses this by:
- Refactoring out `https://chainid.network/chains.json` into constant
`CHAIN_SPEC_URL`
- Add new optional option `allowStale` to `fetchWithCache`. If set to
`true`, it will falling back to return any entry instead of throwing an
error when a request fail.
- Set `allowStale` to `true` for all requests to `CHAIN_SPEC_URL`
- Seed the fetch cache for `CHAIN_SPEC_URL` with
[`eth-chainlist`](https://www.npmjs.com/package/eth-chainlist), which is
the same data exposed via a published npm package.
- Open for suggestions on if this should be bundled differently - maybe
we want our own equivalent mirror?
While an improvement, this could still be further improved.
- The bundled result could be used immediately in all cases without
waiting for response
- The cached data could be updated asynchronously in the background,
without being prompted by user action
I currently consider these out-of-scope for this PR.
Or put more generally: Decoupling the fetching of the data from its use
would be even better.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/PR?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've clearly explained what problem this PR is solving and how it
is solved.
- [ ] I've linked related issues
- [x] I've included manual testing steps
- [x] I've included screenshots/recordings if applicable
- [ ] I’ve included tests if applicable
- [x] 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.
- [x] I’ve properly set the pull request status:
- [x] In case it's not yet "ready for review", I've set it to "draft".
- [x] In case it's "ready for review", I've changed it from "draft" to
"non-draft".
## **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/metamask-controller.js | 25 +++++++++++++++++++
package.json | 1 +
shared/constants/network.ts | 1 +
shared/lib/fetch-with-cache.ts | 7 ++++++
ui/hooks/useIsOriginalNativeTokenSymbol.js | 4 ++-
.../confirmation/confirmation.js | 4 ++-
.../networks-form/use-safe-chains.ts | 4 ++-
yarn.lock | 8 ++++++
8 files changed, 51 insertions(+), 3 deletions(-)
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 87f7570f2d19..93f8007d4d2f 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -42,6 +42,7 @@ import {
LedgerIframeBridge,
} from '@metamask/eth-ledger-bridge-keyring';
import LatticeKeyring from 'eth-lattice-keyring';
+import { rawChainData } from 'eth-chainlist';
import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring';
import EthQuery from '@metamask/eth-query';
import EthJSQuery from '@metamask/ethjs-query';
@@ -169,6 +170,7 @@ import {
} from '../../shared/constants/swaps';
import {
CHAIN_IDS,
+ CHAIN_SPEC_URL,
NETWORK_TYPES,
NetworkStatus,
MAINNET_DISPLAY_NAME,
@@ -200,6 +202,10 @@ import {
} from '../../shared/constants/metametrics';
import { LOG_EVENT } from '../../shared/constants/logs';
+import {
+ getStorageItem,
+ setStorageItem,
+} from '../../shared/lib/storage-helpers';
import {
getTokenIdParam,
fetchTokenBalance,
@@ -413,6 +419,8 @@ export default class MetamaskController extends EventEmitter {
this.getRequestAccountTabIds = opts.getRequestAccountTabIds;
this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds;
+ this.initializeChainlist();
+
this.controllerMessenger = new ControllerMessenger();
this.loggingController = new LoggingController({
@@ -6304,6 +6312,23 @@ export default class MetamaskController extends EventEmitter {
});
}
+ /**
+ * The chain list is fetched live at runtime, falling back to a cache.
+ * This preseeds the cache at startup with a static list provided at build.
+ */
+ async initializeChainlist() {
+ const cacheKey = `cachedFetch:${CHAIN_SPEC_URL}`;
+ const { cachedResponse } = (await getStorageItem(cacheKey)) || {};
+ if (cachedResponse) {
+ return;
+ }
+ await setStorageItem(cacheKey, {
+ cachedResponse: rawChainData(),
+ // Cached value is immediately invalidated
+ cachedTime: 0,
+ });
+ }
+
/**
* Returns the nonce that will be associated with a transaction once approved
*
diff --git a/package.json b/package.json
index 7efae54424ff..404dbc8e50df 100644
--- a/package.json
+++ b/package.json
@@ -387,6 +387,7 @@
"currency-formatter": "^1.4.2",
"debounce-stream": "^2.0.0",
"deep-freeze-strict": "1.1.1",
+ "eth-chainlist": "~0.0.498",
"eth-ens-namehash": "^2.0.8",
"eth-lattice-keyring": "^0.12.4",
"eth-method-registry": "^4.0.0",
diff --git a/shared/constants/network.ts b/shared/constants/network.ts
index a98417794d81..9ed2e26150a9 100644
--- a/shared/constants/network.ts
+++ b/shared/constants/network.ts
@@ -96,6 +96,7 @@ export const NETWORK_NAMES = {
HOMESTEAD: 'homestead',
};
+export const CHAIN_SPEC_URL = 'https://chainid.network/chains.json';
/**
* An object containing all of the chain ids for networks both built in and
* those that we have added custom code to support our feature set.
diff --git a/shared/lib/fetch-with-cache.ts b/shared/lib/fetch-with-cache.ts
index 969fba9f869f..66610ec925b8 100644
--- a/shared/lib/fetch-with-cache.ts
+++ b/shared/lib/fetch-with-cache.ts
@@ -7,6 +7,7 @@ const fetchWithCache = async ({
fetchOptions = {},
cacheOptions: { cacheRefreshTime = MINUTE * 6, timeout = SECOND * 30 } = {},
functionName = '',
+ allowStale = false,
}: {
url: string;
// TODO: Replace `any` with type
@@ -16,6 +17,7 @@ const fetchWithCache = async ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cacheOptions?: Record;
functionName: string;
+ allowStale?: boolean;
}) => {
if (
fetchOptions.body ||
@@ -49,6 +51,11 @@ const fetchWithCache = async ({
...fetchOptions,
});
if (!response.ok) {
+ const message = `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`;
+ if (allowStale) {
+ console.debug(`${message}. Returning cached result`);
+ return cachedResponse;
+ }
throw new Error(
`Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`,
);
diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.js b/ui/hooks/useIsOriginalNativeTokenSymbol.js
index 9a546dba8305..65811c4d656c 100644
--- a/ui/hooks/useIsOriginalNativeTokenSymbol.js
+++ b/ui/hooks/useIsOriginalNativeTokenSymbol.js
@@ -4,6 +4,7 @@ import fetchWithCache from '../../shared/lib/fetch-with-cache';
import {
CHAIN_ID_TO_CURRENCY_SYMBOL_MAP,
CHAIN_ID_TO_CURRENCY_SYMBOL_MAP_NETWORK_COLLISION,
+ CHAIN_SPEC_URL,
} from '../../shared/constants/network';
import { DAY } from '../../shared/constants/time';
import { useSafeChainsListValidationSelector } from '../selectors';
@@ -78,7 +79,8 @@ export function useIsOriginalNativeTokenSymbol(
}
const safeChainsList = await fetchWithCache({
- url: 'https://chainid.network/chains.json',
+ url: CHAIN_SPEC_URL,
+ allowStale: true,
cacheOptions: { cacheRefreshTime: DAY },
functionName: 'getSafeChainsList',
});
diff --git a/ui/pages/confirmations/confirmation/confirmation.js b/ui/pages/confirmations/confirmation/confirmation.js
index 12b2af503f7f..4bb1f4f7d203 100644
--- a/ui/pages/confirmations/confirmation/confirmation.js
+++ b/ui/pages/confirmations/confirmation/confirmation.js
@@ -14,6 +14,7 @@ import { produce } from 'immer';
import log from 'loglevel';
import { ApprovalType } from '@metamask/controller-utils';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
+import { CHAIN_SPEC_URL } from '../../../../shared/constants/network';
import fetchWithCache from '../../../../shared/lib/fetch-with-cache';
import {
MetaMetricsEventCategory,
@@ -372,7 +373,8 @@ export default function ConfirmationPage({
try {
if (useSafeChainsListValidation) {
const response = await fetchWithCache({
- url: 'https://chainid.network/chains.json',
+ url: CHAIN_SPEC_URL,
+ allowStale: true,
cacheOptions: { cacheRefreshTime: DAY },
functionName: 'getSafeChainsList',
});
diff --git a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts
index 56b237e9fce4..3556c2196b31 100644
--- a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts
+++ b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { useSafeChainsListValidationSelector } from '../../../../selectors';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
+import { CHAIN_SPEC_URL } from '../../../../../shared/constants/network';
import { DAY } from '../../../../../shared/constants/time';
export type SafeChain = {
@@ -25,8 +26,9 @@ export const useSafeChains = () => {
if (useSafeChainsListValidation) {
useEffect(() => {
fetchWithCache({
- url: 'https://chainid.network/chains.json',
+ url: CHAIN_SPEC_URL,
functionName: 'getSafeChainsList',
+ allowStale: true,
cacheOptions: { cacheRefreshTime: DAY },
})
.then((response) => {
diff --git a/yarn.lock b/yarn.lock
index 825c8bf06d8a..4d604200c496 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18431,6 +18431,13 @@ __metadata:
languageName: node
linkType: hard
+"eth-chainlist@npm:~0.0.498":
+ version: 0.0.498
+ resolution: "eth-chainlist@npm:0.0.498"
+ checksum: 10/a414c0e1f0a877f9ab8bf1cf775556308ddbb66618e368666d4dea9a0b949febedf8ca5440cf57419413404e7661f1e3d040802faf532d0e1618c40ecd334cbf
+ languageName: node
+ linkType: hard
+
"eth-eip712-util-browser@npm:^0.0.3":
version: 0.0.3
resolution: "eth-eip712-util-browser@npm:0.0.3"
@@ -26313,6 +26320,7 @@ __metadata:
eslint-plugin-react-hooks: "npm:^4.2.0"
eslint-plugin-storybook: "npm:^0.6.15"
eta: "npm:^3.2.0"
+ eth-chainlist: "npm:~0.0.498"
eth-ens-namehash: "npm:^2.0.8"
eth-lattice-keyring: "npm:^0.12.4"
eth-method-registry: "npm:^4.0.0"
From 935ad43bc68da8044a83f079eea22203f929ced1 Mon Sep 17 00:00:00 2001
From: Priya
Date: Thu, 17 Oct 2024 08:52:01 +0200
Subject: [PATCH 20/51] test: Update test-dapp to verison 8.7.0 (#27816)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Upgrade the test-dapp version from 8.4.0 to 8.7.0
Update failing permit tests due to the upgrade
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27816?quickstart=1)
## **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.
---
package.json | 2 +-
test/e2e/tests/confirmations/signatures/permit.spec.ts | 8 ++++----
yarn.lock | 10 +++++-----
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/package.json b/package.json
index 404dbc8e50df..4951a4106e47 100644
--- a/package.json
+++ b/package.json
@@ -478,7 +478,7 @@
"@metamask/phishing-warning": "^4.0.0",
"@metamask/preferences-controller": "^13.0.2",
"@metamask/test-bundler": "^1.0.0",
- "@metamask/test-dapp": "^8.4.0",
+ "@metamask/test-dapp": "8.7.0",
"@octokit/core": "^3.6.0",
"@open-rpc/meta-schema": "^1.14.6",
"@open-rpc/mock-server": "^1.7.5",
diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts
index c9c4ca9399f4..5c52d1f029ee 100644
--- a/test/e2e/tests/confirmations/signatures/permit.spec.ts
+++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts
@@ -152,22 +152,22 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) {
await driver.waitForSelector({
css: '#signPermitResult',
- text: '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c',
+ text: '0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d730103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea491b',
});
await driver.waitForSelector({
css: '#signPermitResultR',
- text: 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f',
+ text: 'r: 0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d73',
});
await driver.waitForSelector({
css: '#signPermitResultS',
- text: 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee',
+ text: 's: 0x0103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea49',
});
await driver.waitForSelector({
css: '#signPermitResultV',
- text: 'v: 28',
+ text: 'v: 27',
});
await driver.waitForSelector({
css: '#signPermitVerifyResult',
diff --git a/yarn.lock b/yarn.lock
index 4d604200c496..9f547f225d9a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6478,10 +6478,10 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/test-dapp@npm:^8.4.0":
- version: 8.4.0
- resolution: "@metamask/test-dapp@npm:8.4.0"
- checksum: 10/9d9c4df11c2b18c72b52e8743435ed0bd18815dd7a7aed43cf3a2cce1b9ef8926909890d00b4b624446f73b88c15e95bc0190c5437b9dad437a0e345a6b430ba
+"@metamask/test-dapp@npm:8.7.0":
+ version: 8.7.0
+ resolution: "@metamask/test-dapp@npm:8.7.0"
+ checksum: 10/c2559179d3372e5fc8d67a60c1e4056fad9809486eaff6a2aa9c351a2a613eeecc15885a5fd9b71b8f4139058fe168abeac06bd6bdb6d4a47fe0b9b4146923ab
languageName: node
linkType: hard
@@ -26181,7 +26181,7 @@ __metadata:
"@metamask/snaps-sdk": "npm:^6.9.0"
"@metamask/snaps-utils": "npm:^8.4.1"
"@metamask/test-bundler": "npm:^1.0.0"
- "@metamask/test-dapp": "npm:^8.4.0"
+ "@metamask/test-dapp": "npm:8.7.0"
"@metamask/transaction-controller": "npm:^37.2.0"
"@metamask/user-operation-controller": "npm:^13.0.0"
"@metamask/utils": "npm:^9.3.0"
From 2afe52e29850b3afa351c29e34c8f71a887186b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Albert=20Oliv=C3=A9?=
Date: Thu, 17 Oct 2024 11:55:27 +0200
Subject: [PATCH 21/51] feat(logging): add extension request logging and
retrieval (#27655)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
feat: Implement improved API request logging for MMI
- Add API request logging in MMI controller
- Implement logAndStoreApiRequest method in MetaMask controller
- Update UI to use new logging mechanism
- Add types and interfaces for API call log entries
- Fixed MMI e2e tests
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MMI-5436
## **Manual testing steps**
1. Open the extension in your browser.
2. Click on the three dots (menu icon) and select Settings.
3. Go to the Advanced section and search for State Logs.
4. Click Download Logs.
5. Open the downloaded file and look for API request logs to review the
necessary data.
## **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: MetaMask Bot
---
.../controllers/mmi-controller.test.ts | 99 +++++++++++++
app/scripts/controllers/mmi-controller.ts | 24 +++-
app/scripts/metamask-controller.js | 3 +
package.json | 14 +-
.../mmi/pageObjects/mmi-dummyApp-page.ts | 15 +-
.../interactive-replacement-token-modal.tsx | 40 +++---
.../institution-background.test.js | 72 ++++++++++
.../institutional/institution-background.ts | 7 +
yarn.lock | 135 +++++++++---------
9 files changed, 305 insertions(+), 104 deletions(-)
diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts
index dbef190a5573..0c4aa2d5d874 100644
--- a/app/scripts/controllers/mmi-controller.test.ts
+++ b/app/scripts/controllers/mmi-controller.test.ts
@@ -24,6 +24,7 @@ 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 { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk';
jest.mock('@metamask-institutional/portfolio-dashboard', () => ({
handleMmiPortfolio: jest.fn(),
@@ -353,6 +354,33 @@ describe('MMIController', function () {
mmiController.mmiConfigurationController.storeConfiguration,
).toHaveBeenCalled();
});
+
+ it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => {
+ const mockKeyring = {
+ on: jest.fn(),
+ getAccounts: jest.fn().mockResolvedValue([]),
+ getSupportedChains: jest.fn().mockResolvedValue({}),
+ };
+
+ mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring);
+ mmiController.custodyController.getAllCustodyTypes = jest.fn().mockReturnValue(['mock-custody-type']);
+ mmiController.logAndStoreApiRequest = jest.fn();
+
+ await mmiController.onSubmitPassword();
+
+ expect(mockKeyring.on).toHaveBeenCalledWith(
+ API_REQUEST_LOG_EVENT,
+ expect.any(Function)
+ );
+
+ const mockLogData = { someKey: 'someValue' };
+ const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find(
+ call => call[0] === API_REQUEST_LOG_EVENT
+ )[1];
+ apiRequestLogEventHandler(mockLogData);
+
+ expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData);
+ });
});
describe('connectCustodyAddresses', () => {
@@ -408,6 +436,54 @@ describe('MMIController', function () {
).toHaveBeenCalled();
expect(result).toEqual(['0x1']);
});
+
+ it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => {
+ const custodianType = 'mock-custodian-type';
+ const custodianName = 'mock-custodian-name';
+ const accounts = {
+ '0x1': {
+ name: 'Account 1',
+ custodianDetails: {},
+ labels: [],
+ token: 'token',
+ chainId: 1,
+ },
+ };
+ CUSTODIAN_TYPES['MOCK-CUSTODIAN-TYPE'] = {
+ keyringClass: { type: 'mock-keyring-class' },
+ };
+
+ const mockKeyring = {
+ on: jest.fn(),
+ setSelectedAddresses: jest.fn(),
+ addAccounts: jest.fn(),
+ getStatusMap: jest.fn(),
+ };
+
+ mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring);
+ mmiController.keyringController.getAccounts = jest.fn().mockResolvedValue(['0x2']);
+ mmiController.keyringController.addNewAccountForKeyring = jest.fn().mockResolvedValue('0x3');
+ mmiController.custodyController.setAccountDetails = jest.fn();
+ mmiController.accountTrackerController.syncWithAddresses = jest.fn();
+ mmiController.storeCustodianSupportedChains = jest.fn();
+ mmiController.custodyController.storeCustodyStatusMap = jest.fn();
+ mmiController.logAndStoreApiRequest = jest.fn();
+
+ await mmiController.connectCustodyAddresses(custodianType, custodianName, accounts);
+
+ expect(mockKeyring.on).toHaveBeenCalledWith(
+ API_REQUEST_LOG_EVENT,
+ expect.any(Function)
+ );
+
+ const mockLogData = { someKey: 'someValue' };
+ const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find(
+ call => call[0] === API_REQUEST_LOG_EVENT
+ )[1];
+ apiRequestLogEventHandler(mockLogData);
+
+ expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData);
+ });
});
describe('getCustodianAccounts', () => {
@@ -783,4 +859,27 @@ describe('MMIController', function () {
).toHaveBeenCalledWith('/new-account/connect');
});
});
+
+ describe('logAndStoreApiRequest', () => {
+ it('should call custodyController.sanitizeAndLogApiCall with the provided log data', async () => {
+ const mockLogData = { someKey: 'someValue' };
+ const mockSanitizedLogs = { sanitizedKey: 'sanitizedValue' };
+
+ mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockResolvedValue(mockSanitizedLogs);
+
+ const result = await mmiController.logAndStoreApiRequest(mockLogData);
+
+ expect(mmiController.custodyController.sanitizeAndLogApiCall).toHaveBeenCalledWith(mockLogData);
+ expect(result).toEqual(mockSanitizedLogs);
+ });
+
+ it('should handle errors and throw them', async () => {
+ const mockLogData = { someKey: 'someValue' };
+ const mockError = new Error('Sanitize error');
+
+ mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockRejectedValue(mockError);
+
+ await expect(mmiController.logAndStoreApiRequest(mockLogData)).rejects.toThrow('Sanitize error');
+ });
+ });
});
diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts
index 2373484d4a6e..8c5f1ee4b49b 100644
--- a/app/scripts/controllers/mmi-controller.ts
+++ b/app/scripts/controllers/mmi-controller.ts
@@ -13,12 +13,14 @@ import {
import {
REFRESH_TOKEN_CHANGE_EVENT,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
+ API_REQUEST_LOG_EVENT,
} from '@metamask-institutional/sdk';
import { handleMmiPortfolio } from '@metamask-institutional/portfolio-dashboard';
-import { TransactionMeta } from '@metamask/transaction-controller';
-import { KeyringTypes } from '@metamask/keyring-controller';
import { CustodyController } from '@metamask-institutional/custody-controller';
+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 { SignatureController } from '@metamask/signature-controller';
import {
OriginalRequest,
@@ -304,6 +306,10 @@ export default class MMIController extends EventEmitter {
},
);
+ keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => {
+ this.logAndStoreApiRequest(logData);
+ });
+
// store the supported chains for this custodian type
const accounts = await keyring.getAccounts();
addresses = addresses.concat(...accounts);
@@ -419,6 +425,10 @@ export default class MMIController extends EventEmitter {
},
);
+ keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => {
+ this.logAndStoreApiRequest(logData);
+ });
+
if (!keyring) {
throw new Error('Unable to get keyring');
}
@@ -884,4 +894,14 @@ export default class MMIController extends EventEmitter {
this.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE);
return true;
}
+
+ async logAndStoreApiRequest(logData: IApiCallLogEntry) {
+ try {
+ const logs = await this.custodyController.sanitizeAndLogApiCall(logData);
+ return logs;
+ } catch (error) {
+ log.error('Error fetching extension request logs:', error);
+ throw error;
+ }
+ }
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 93f8007d4d2f..176c7aea10e5 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -3776,6 +3776,9 @@ export default class MetamaskController extends EventEmitter {
appStateController.setCustodianDeepLink.bind(appStateController),
setNoteToTraderMessage:
appStateController.setNoteToTraderMessage.bind(appStateController),
+ logAndStoreApiRequest: this.mmiController.logAndStoreApiRequest.bind(
+ this.mmiController,
+ ),
///: END:ONLY_INCLUDE_IF
// snaps
diff --git a/package.json b/package.json
index 4951a4106e47..57dc9224b765 100644
--- a/package.json
+++ b/package.json
@@ -285,15 +285,15 @@
"@lavamoat/lavadome-react": "0.0.17",
"@lavamoat/snow": "^2.0.2",
"@material-ui/core": "^4.11.0",
- "@metamask-institutional/custody-controller": "^0.2.31",
- "@metamask-institutional/custody-keyring": "^2.0.3",
- "@metamask-institutional/extension": "^0.3.27",
- "@metamask-institutional/institutional-features": "^1.3.5",
+ "@metamask-institutional/custody-controller": "^0.3.0",
+ "@metamask-institutional/custody-keyring": "^2.1.0",
+ "@metamask-institutional/extension": "^0.3.28",
+ "@metamask-institutional/institutional-features": "^1.3.6",
"@metamask-institutional/portfolio-dashboard": "^1.4.1",
"@metamask-institutional/rpc-allowlist": "^1.0.3",
- "@metamask-institutional/sdk": "^0.1.30",
- "@metamask-institutional/transaction-update": "^0.2.5",
- "@metamask-institutional/types": "^1.1.0",
+ "@metamask-institutional/sdk": "^0.2.0",
+ "@metamask-institutional/transaction-update": "^0.2.6",
+ "@metamask-institutional/types": "^1.2.0",
"@metamask/abi-utils": "^2.0.2",
"@metamask/account-watcher": "^4.1.1",
"@metamask/accounts-controller": "^18.2.2",
diff --git a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts
index a9a175e82971..cf455dc0a7e0 100644
--- a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts
+++ b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts
@@ -34,10 +34,11 @@ export class DummyAppPage {
this.connectBtn.click(),
]);
await popup1.waitForLoadState();
- // Check which account is selected and select if required
- await popup1.locator('.check-box__indeterminate');
- await popup1.locator('button:has-text("Next")').click();
- await popup1.locator('button:has-text("Confirm")').click();
+ await popup1.getByTestId('edit').nth(1).click();
+ await popup1.getByText('Select all').click();
+ await popup1.getByTestId('Sepolia').click();
+ await popup1.getByTestId('connect-more-chains-button').click();
+ await popup1.getByTestId('confirm-btn').click();
await popup1.close();
}
@@ -60,11 +61,7 @@ export class DummyAppPage {
if (isSign) {
await popup.click('button:has-text("Confirm")');
} else {
- await popup.getByTestId('page-container-footer-next').click();
-
- if (buttonId === 'approveTokens') {
- await popup.getByTestId('page-container-footer-next').click();
- }
+ await popup.getByTestId('confirm-footer-button').click();
await popup
.getByTestId('custody-confirm-link__btn')
diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx
index 4526595fa455..06fe1336ca7e 100644
--- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx
+++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx
@@ -85,30 +85,26 @@ const InteractiveReplacementTokenModal: React.FC = () => {
{t('custodyRefreshTokenModalTitle')}
- {
- // @ts-expect-error: todo: Merge MetaMask Institutional PR 778 to fix this
- custodian.iconUrl ? (
-
-
-
-
-
- ) : (
+ {custodian.iconUrl ? (
+
- {custodian.displayName}
+
- )
- }
+
+ ) : (
+
+ {custodian.displayName}
+
+ )}
({
@@ -173,4 +174,75 @@ describe('Institution Actions', () => {
);
});
});
+
+ describe('#logAndStoreApiRequest', () => {
+ it('should call submitRequestToBackground with correct parameters', async () => {
+ const mockLogData = {
+ id: '123',
+ method: 'GET',
+ request: {
+ url: 'https://api.example.com/data',
+ headers: { 'Content-Type': 'application/json' },
+ },
+ response: {
+ status: 200,
+ body: '{"success": true}',
+ },
+ timestamp: 1234567890,
+ };
+
+ await logAndStoreApiRequest(mockLogData);
+
+ expect(submitRequestToBackground).toHaveBeenCalledWith(
+ 'logAndStoreApiRequest',
+ [mockLogData],
+ );
+ });
+
+ it('should return the result from submitRequestToBackground', async () => {
+ const mockLogData = {
+ id: '456',
+ method: 'POST',
+ request: {
+ url: 'https://api.example.com/submit',
+ headers: { 'Content-Type': 'application/json' },
+ body: '{"data": "test"}',
+ },
+ response: {
+ status: 201,
+ body: '{"id": "789"}',
+ },
+ timestamp: 1234567890,
+ };
+
+ submitRequestToBackground.mockResolvedValue('success');
+
+ const result = await logAndStoreApiRequest(mockLogData);
+
+ expect(result).toBe('success');
+ });
+
+ it('should throw an error if submitRequestToBackground fails', async () => {
+ const mockLogData = {
+ id: '789',
+ method: 'GET',
+ request: {
+ url: 'https://api.example.com/error',
+ headers: { 'Content-Type': 'application/json' },
+ },
+ response: {
+ status: 500,
+ body: '{"error": "Internal Server Error"}',
+ },
+ timestamp: 1234567890,
+ };
+
+ const mockError = new Error('Background request failed');
+ submitRequestToBackground.mockRejectedValue(mockError);
+
+ await expect(logAndStoreApiRequest(mockLogData)).rejects.toThrow(
+ 'Background request failed',
+ );
+ });
+ });
});
diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts
index c1d2cfa062a5..fd42d069b8b7 100644
--- a/ui/store/institutional/institution-background.ts
+++ b/ui/store/institutional/institution-background.ts
@@ -1,6 +1,7 @@
import log from 'loglevel';
import { ThunkAction } from 'redux-thunk';
import { AnyAction } from 'redux';
+import { IApiCallLogEntry } from '@metamask-institutional/types';
import {
forceUpdateMetamaskState,
displayWarning,
@@ -108,6 +109,12 @@ export function setPersonalMessageInProgress(msgId: string) {
};
}
+export async function logAndStoreApiRequest(
+ logData: IApiCallLogEntry,
+): Promise {
+ return await submitRequestToBackground('logAndStoreApiRequest', [logData]);
+}
+
/**
* A factory that contains all MMI actions ready to use
* Example usage:
diff --git a/yarn.lock b/yarn.lock
index 9f547f225d9a..a0f9df5f4706 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4627,60 +4627,60 @@ __metadata:
languageName: node
linkType: hard
-"@metamask-institutional/custody-controller@npm:^0.2.30, @metamask-institutional/custody-controller@npm:^0.2.31":
- version: 0.2.31
- resolution: "@metamask-institutional/custody-controller@npm:0.2.31"
+"@metamask-institutional/custody-controller@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "@metamask-institutional/custody-controller@npm:0.3.0"
dependencies:
"@ethereumjs/util": "npm:^8.0.5"
- "@metamask-institutional/custody-keyring": "npm:^2.0.3"
- "@metamask-institutional/sdk": "npm:^0.1.30"
- "@metamask-institutional/types": "npm:^1.1.0"
+ "@metamask-institutional/custody-keyring": "npm:^2.1.0"
+ "@metamask-institutional/sdk": "npm:^0.2.0"
+ "@metamask-institutional/types": "npm:^1.2.0"
"@metamask/obs-store": "npm:^9.0.0"
- checksum: 10/f856c98db42a21639d9ec5d1c835bc302b5a1b3fb821aae8641f63a9400f8303b8fa578368a2f2d2a1ec0c148c070f809b8c0fa46fa3fd2fa29f80e0ec1da207
+ checksum: 10/572e96d4b23566fb8dbf06ab0117c68c2d1db901deea69eee48d08f41ea3e1dbbbb3090c83cce6ff240ed8061e84df1b61befaf57da764b495eb0978d45fac42
languageName: node
linkType: hard
-"@metamask-institutional/custody-keyring@npm:^2.0.3":
- version: 2.0.3
- resolution: "@metamask-institutional/custody-keyring@npm:2.0.3"
+"@metamask-institutional/custody-keyring@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "@metamask-institutional/custody-keyring@npm:2.1.0"
dependencies:
"@ethereumjs/tx": "npm:^4.1.1"
"@ethereumjs/util": "npm:^8.0.5"
"@metamask-institutional/configuration-client": "npm:^2.0.1"
- "@metamask-institutional/sdk": "npm:^0.1.30"
- "@metamask-institutional/types": "npm:^1.1.0"
+ "@metamask-institutional/sdk": "npm:^0.2.0"
+ "@metamask-institutional/types": "npm:^1.2.0"
"@metamask/obs-store": "npm:^9.0.0"
crypto: "npm:^1.0.1"
lodash.clonedeep: "npm:^4.5.0"
- checksum: 10/987beeeed67fb92a436eb1318f48ec2cc0ceb1ae944b7f5b2e492dcdc28a4298c5a8d25a520022ac52f87a411f7341961100be47a9626fbb1674aed349d98737
+ checksum: 10/78421e38fed4ad88412593a307fc13f220b0e5a83dee76de0032c835a7896bf23bb76030e4bb7d69bfa604db7a31faa6312ac64b05cc135d8afb723fb3660920
languageName: node
linkType: hard
-"@metamask-institutional/extension@npm:^0.3.27":
- version: 0.3.27
- resolution: "@metamask-institutional/extension@npm:0.3.27"
+"@metamask-institutional/extension@npm:^0.3.28":
+ version: 0.3.28
+ resolution: "@metamask-institutional/extension@npm:0.3.28"
dependencies:
"@ethereumjs/util": "npm:^8.0.5"
- "@metamask-institutional/custody-controller": "npm:^0.2.30"
- "@metamask-institutional/custody-keyring": "npm:^2.0.3"
+ "@metamask-institutional/custody-controller": "npm:^0.3.0"
+ "@metamask-institutional/custody-keyring": "npm:^2.1.0"
"@metamask-institutional/portfolio-dashboard": "npm:^1.4.1"
- "@metamask-institutional/sdk": "npm:^0.1.30"
- "@metamask-institutional/transaction-update": "npm:^0.2.5"
- "@metamask-institutional/types": "npm:^1.1.0"
+ "@metamask-institutional/sdk": "npm:^0.2.0"
+ "@metamask-institutional/transaction-update": "npm:^0.2.6"
+ "@metamask-institutional/types": "npm:^1.2.0"
jest-create-mock-instance: "npm:^2.0.0"
jest-fetch-mock: "npm:3.0.3"
lodash.clonedeep: "npm:^4.5.0"
- checksum: 10/dc9eefe8045607cd415b9db4a8df833c9a523e9d06a3a0e49e4c6e85063924db1f117725a91c926f19ce26d0701fc175ea4ad38fb13a8a3b092434bcd7fd7882
+ checksum: 10/a1f73c5281282ab1315ee19dd363330504300c036586ff64c98c176da8ac23046de8e8051956b4e15184faf0720bf324b81c406a1bf85295691c24f191b8f747
languageName: node
linkType: hard
-"@metamask-institutional/institutional-features@npm:^1.3.5":
- version: 1.3.5
- resolution: "@metamask-institutional/institutional-features@npm:1.3.5"
+"@metamask-institutional/institutional-features@npm:^1.3.6":
+ version: 1.3.6
+ resolution: "@metamask-institutional/institutional-features@npm:1.3.6"
dependencies:
- "@metamask-institutional/custody-keyring": "npm:^2.0.3"
+ "@metamask-institutional/custody-keyring": "npm:^2.1.0"
"@metamask/obs-store": "npm:^9.0.0"
- checksum: 10/1a154dbbfc71c9fee43d755d901423e3ea17ad149679225481fdc2d73ae95960e1805a792dbe660dd778703614ea5fd7390314bd7099c8ede510db1d23bc08ab
+ checksum: 10/a6b53f1b0ba8554595498153cbc0d32bb1a2d8374ad6ff9b617fea4e10872120000d14d9916b48ff9bafbac5da954ada99dca5f88f3ba21d4fbb80590804444c
languageName: node
linkType: hard
@@ -4698,17 +4698,17 @@ __metadata:
languageName: node
linkType: hard
-"@metamask-institutional/sdk@npm:^0.1.30":
- version: 0.1.30
- resolution: "@metamask-institutional/sdk@npm:0.1.30"
+"@metamask-institutional/sdk@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "@metamask-institutional/sdk@npm:0.2.0"
dependencies:
"@metamask-institutional/simplecache": "npm:^1.1.0"
- "@metamask-institutional/types": "npm:^1.1.0"
+ "@metamask-institutional/types": "npm:^1.2.0"
"@types/jsonwebtoken": "npm:^9.0.1"
- "@types/node": "npm:^20.11.17"
+ "@types/node": "npm:^20.14.9"
bignumber.js: "npm:^9.1.1"
jsonwebtoken: "npm:^9.0.0"
- checksum: 10/3f36925fa9399a0ea06e2a64ea89accfb34f0a17581ab69652b4f325a948db10e88faebcca4f7c2d9f5f1f1c7f98bd8f970b7a489218dfd1be8cebc669a2f67e
+ checksum: 10/59f8b5eff176746ef3c9c406edda340ab04b37df1799d9b56e26fcede95441461d73d4be8b33f1dc3153cddea6baa876eba1232ca538da8f732a29801531a2f8
languageName: node
linkType: hard
@@ -4719,36 +4719,36 @@ __metadata:
languageName: node
linkType: hard
-"@metamask-institutional/transaction-update@npm:^0.2.5":
- version: 0.2.5
- resolution: "@metamask-institutional/transaction-update@npm:0.2.5"
+"@metamask-institutional/transaction-update@npm:^0.2.6":
+ version: 0.2.6
+ resolution: "@metamask-institutional/transaction-update@npm:0.2.6"
dependencies:
"@ethereumjs/util": "npm:^8.0.5"
- "@metamask-institutional/custody-keyring": "npm:^2.0.3"
- "@metamask-institutional/sdk": "npm:^0.1.30"
- "@metamask-institutional/types": "npm:^1.1.0"
- "@metamask-institutional/websocket-client": "npm:^0.2.5"
+ "@metamask-institutional/custody-keyring": "npm:^2.1.0"
+ "@metamask-institutional/sdk": "npm:^0.2.0"
+ "@metamask-institutional/types": "npm:^1.2.0"
+ "@metamask-institutional/websocket-client": "npm:^0.2.6"
"@metamask/obs-store": "npm:^9.0.0"
- checksum: 10/9dbcf7c38a03becf61ab013f78df225da1f6de12976f328e7809c0edda5ab9e1aeee2b4d5b9430c15d5dc9f7040fa703c560c58073d601110895388c1c15d7a8
+ checksum: 10/815c6faaaed9af25ed21d1339790e82622bef81f3c578269afde908dc95d36cc64a549c58164e24f20d9941e8c05e883d02c8886b741e50e3cf83960a8cb00d2
languageName: node
linkType: hard
-"@metamask-institutional/types@npm:^1.1.0":
- version: 1.1.0
- resolution: "@metamask-institutional/types@npm:1.1.0"
- checksum: 10/76f3c8529e4fe549bcabe60c39a66dd1a526aa7ea16fe7949e960a884d2c9e5e2e65db4d1123e23eeaae46f88b10aafe365cc693f5f632ef1a8e407373fe2fdf
+"@metamask-institutional/types@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@metamask-institutional/types@npm:1.2.0"
+ checksum: 10/3e28224c12f1ad955f114de919dbf4abbef19bd19cca3a4544898061d79518a94baa14121ebf6e5c6972dd6b1d1ec8071ebc50a77480ad944c26a2be53af5290
languageName: node
linkType: hard
-"@metamask-institutional/websocket-client@npm:^0.2.5":
- version: 0.2.5
- resolution: "@metamask-institutional/websocket-client@npm:0.2.5"
+"@metamask-institutional/websocket-client@npm:^0.2.6":
+ version: 0.2.6
+ resolution: "@metamask-institutional/websocket-client@npm:0.2.6"
dependencies:
- "@metamask-institutional/custody-keyring": "npm:^2.0.3"
- "@metamask-institutional/sdk": "npm:^0.1.30"
- "@metamask-institutional/types": "npm:^1.1.0"
+ "@metamask-institutional/custody-keyring": "npm:^2.1.0"
+ "@metamask-institutional/sdk": "npm:^0.2.0"
+ "@metamask-institutional/types": "npm:^1.2.0"
mock-socket: "npm:^9.2.1"
- checksum: 10/4743ccbb3a92a5b7ddccfd9f72741910bb93cc769023c8b9ee7944bb82f79938e45b10af5f7754b2898dc218c0e3874cb38aa628f96685fc69d956900723755d
+ checksum: 10/ba59b6d776fdc9d681ac0a294cd3eab961ba9d06d1ebd6a59fbe379cf640c421fdaaf53f6b6ab187ea3f1993b251292deb3c9d1fff8b6717fbd14f2512105190
languageName: node
linkType: hard
@@ -10699,12 +10699,12 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.11.17":
- version: 20.12.7
- resolution: "@types/node@npm:20.12.7"
+"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.14.9":
+ version: 20.16.11
+ resolution: "@types/node@npm:20.16.11"
dependencies:
- undici-types: "npm:~5.26.4"
- checksum: 10/b4a28a3b593a9bdca5650880b6a9acef46911d58cf7cfa57268f048e9a7157a7c3196421b96cea576850ddb732e3b54bc982c8eb5e1e5ef0635d4424c2fce801
+ undici-types: "npm:~6.19.2"
+ checksum: 10/6d2f92b7b320c32ba0c2bc54d21651bd21690998a2e27f00d15019d4db3e0ec30fce85332efed5e37d4cda078ff93ea86ee3e92b76b7a25a9b92a52a039b60b2
languageName: node
linkType: hard
@@ -26096,15 +26096,15 @@ __metadata:
"@lgbot/madge": "npm:^6.2.0"
"@lydell/node-pty": "npm:^1.0.1"
"@material-ui/core": "npm:^4.11.0"
- "@metamask-institutional/custody-controller": "npm:^0.2.31"
- "@metamask-institutional/custody-keyring": "npm:^2.0.3"
- "@metamask-institutional/extension": "npm:^0.3.27"
- "@metamask-institutional/institutional-features": "npm:^1.3.5"
+ "@metamask-institutional/custody-controller": "npm:^0.3.0"
+ "@metamask-institutional/custody-keyring": "npm:^2.1.0"
+ "@metamask-institutional/extension": "npm:^0.3.28"
+ "@metamask-institutional/institutional-features": "npm:^1.3.6"
"@metamask-institutional/portfolio-dashboard": "npm:^1.4.1"
"@metamask-institutional/rpc-allowlist": "npm:^1.0.3"
- "@metamask-institutional/sdk": "npm:^0.1.30"
- "@metamask-institutional/transaction-update": "npm:^0.2.5"
- "@metamask-institutional/types": "npm:^1.1.0"
+ "@metamask-institutional/sdk": "npm:^0.2.0"
+ "@metamask-institutional/transaction-update": "npm:^0.2.6"
+ "@metamask-institutional/types": "npm:^1.2.0"
"@metamask/abi-utils": "npm:^2.0.2"
"@metamask/account-watcher": "npm:^4.1.1"
"@metamask/accounts-controller": "npm:^18.2.2"
@@ -35488,6 +35488,13 @@ __metadata:
languageName: node
linkType: hard
+"undici-types@npm:~6.19.2":
+ version: 6.19.8
+ resolution: "undici-types@npm:6.19.8"
+ checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70
+ languageName: node
+ linkType: hard
+
"undici@npm:5.28.4":
version: 5.28.4
resolution: "undici@npm:5.28.4"
From bbba7c5c8e82150a08e2d1a07539112e5655212d Mon Sep 17 00:00:00 2001
From: seaona <54408225+seaona@users.noreply.github.com>
Date: Thu, 17 Oct 2024 12:55:06 +0200
Subject: [PATCH 22/51] fix: flaky tests `Add existing token using search
renders the balance for the chosen token` (#27853)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR fixes a couple of race conditions on the test `Add existing
token using search renders the balance for the chosen token` and the
rest of the specs in the Add token test file.
The changes have also uncovered a production bug in mmi build (see
[here](https://github.com/MetaMask/metamask-extension/issues/27854)). So
now, this PR is blocked until the fix for mmi is done.
[Edit] The fix has now been merged here, so the PR is ready
https://github.com/MetaMask/metamask-extension/pull/27855
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27853?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27703
## **Manual testing steps**
1. Check ci
## **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: sahar-fehri
---
test/e2e/tests/tokens/add-hide-token.spec.js | 55 +++++++++++---------
1 file changed, 30 insertions(+), 25 deletions(-)
diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js
index 535948ba1c9b..5eb60d3db17b 100644
--- a/test/e2e/tests/tokens/add-hide-token.spec.js
+++ b/test/e2e/tests/tokens/add-hide-token.spec.js
@@ -8,6 +8,7 @@ const {
clickNestedButton,
} = require('../../helpers');
const FixtureBuilder = require('../../fixture-builder');
+const { SMART_CONTRACTS } = require('../../seeder/smart-contracts');
const { CHAIN_IDS } = require('../../../../shared/constants/network');
describe('Add hide token', function () {
@@ -126,16 +127,16 @@ describe('Add existing token using search', function () {
tag: 'p',
});
await driver.clickElement({ text: 'Next', tag: 'button' });
- await driver.clickElement(
+ await driver.clickElementAndWaitToDisappear(
'[data-testid="import-tokens-modal-import-button"]',
);
await driver.clickElement(
'[data-testid="account-overview__asset-tab"]',
);
- const [, tkn] = await driver.findElements(
- '[data-testid="multichain-token-list-button"]',
- );
- await tkn.click();
+ await driver.clickElement({
+ tag: 'span',
+ text: 'Basic Attention Token',
+ });
await driver.waitForSelector({
css: '[data-testid="multichain-token-list-item-value"]',
@@ -147,6 +148,8 @@ describe('Add existing token using search', function () {
});
describe('Add token using wallet_watchAsset', function () {
+ const smartContract = SMART_CONTRACTS.HST;
+
it('opens a notification that adds a token when wallet_watchAsset is executed, then approves', async function () {
await withFixtures(
{
@@ -155,9 +158,13 @@ describe('Add token using wallet_watchAsset', function () {
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions: defaultGanacheOptions,
+ smartContract,
title: this.test.fullTitle(),
},
- async ({ driver }) => {
+ async ({ driver, contractRegistry }) => {
+ const contractAddress = await contractRegistry.getContractAddress(
+ smartContract,
+ );
await unlockWallet(driver);
await driver.openNewPage('http://127.0.0.1:8080/');
@@ -168,7 +175,7 @@ describe('Add token using wallet_watchAsset', function () {
params: {
type: 'ERC20',
options: {
- address: '0x86002be4cdd922de1ccb831582bf99284b99ac12',
+ address: '${contractAddress}',
symbol: 'TST',
decimals: 4
},
@@ -176,19 +183,16 @@ describe('Add token using wallet_watchAsset', function () {
})
`);
- const windowHandles = await driver.waitUntilXWindowHandles(3);
-
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.Dialog,
- windowHandles,
- );
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- await driver.clickElement({
+ await driver.clickElementAndWaitForWindowToClose({
tag: 'button',
text: 'Add token',
});
- await driver.switchToWindowWithTitle('MetaMask', windowHandles);
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
await driver.waitForSelector({
css: '[data-testid="multichain-token-list-item-value"]',
@@ -206,9 +210,13 @@ describe('Add token using wallet_watchAsset', function () {
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions: defaultGanacheOptions,
+ smartContract,
title: this.test.fullTitle(),
},
- async ({ driver }) => {
+ async ({ driver, contractRegistry }) => {
+ const contractAddress = await contractRegistry.getContractAddress(
+ smartContract,
+ );
await unlockWallet(driver);
await driver.openNewPage('http://127.0.0.1:8080/');
@@ -219,7 +227,7 @@ describe('Add token using wallet_watchAsset', function () {
params: {
type: 'ERC20',
options: {
- address: '0x86002be4cdd922de1ccb831582bf99284b99ac12',
+ address: '${contractAddress}',
symbol: 'TST',
decimals: 4
},
@@ -227,19 +235,16 @@ describe('Add token using wallet_watchAsset', function () {
})
`);
- const windowHandles = await driver.waitUntilXWindowHandles(3);
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.Dialog,
- windowHandles,
- );
-
- await driver.clickElement({
+ await driver.clickElementAndWaitForWindowToClose({
tag: 'button',
text: 'Cancel',
});
- await driver.switchToWindowWithTitle('MetaMask', windowHandles);
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
const assetListItems = await driver.findElements(
'.multichain-token-list-item',
From 043ea3fc29a4b3d943b69bf1e66b80662d8228e0 Mon Sep 17 00:00:00 2001
From: Charly Chevalier
Date: Thu, 17 Oct 2024 13:45:55 +0200
Subject: [PATCH 23/51] chore: bump `@metamask/eth-snap-keyring` to version
4.4.0 (#27864)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Bumping the Snap bridge. This new version will now sanitize the redirect
URL passed by a Snap.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27864?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/accounts-planning/issues/585
## **Manual testing steps**
1. `yarn start:flask`
2. Use the SSK Snap with the following branch:
`test/keyring-snap-bridge-70`
3. Run the SSK dapp + Snap using `yarn start`, go to
http://localhost:8000/
4. Install the Snap
5. Make sure that "Use Synchronous Approval" on the SSK dapp is
**disabled**
6. Create an SSK account
7. Go to https://metamask.github.io/test-dapp/
8. Connect your SSK account
9. Use the personal sign test
10. You should see a sanitized URL `https://ioi.com?fake=1` on the Snap
dialog
## **Screenshots/Recordings**
### **Before**
![Screenshot 2024-10-11 at 11 19
52](https://github.com/user-attachments/assets/60661c21-18cd-4570-b642-a47650258556)
### **After**
![Screenshot 2024-10-11 at 12 22
59](https://github.com/user-attachments/assets/819d3ef5-2b09-4b43-8118-0870ee695bff)
## **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: MetaMask Bot
---
package.json | 2 +-
yarn.lock | 22 +++++++++++-----------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/package.json b/package.json
index 57dc9224b765..fca1780d3300 100644
--- a/package.json
+++ b/package.json
@@ -314,7 +314,7 @@
"@metamask/eth-ledger-bridge-keyring": "^3.0.1",
"@metamask/eth-query": "^4.0.0",
"@metamask/eth-sig-util": "^7.0.1",
- "@metamask/eth-snap-keyring": "^4.3.6",
+ "@metamask/eth-snap-keyring": "^4.4.0",
"@metamask/eth-token-tracker": "^8.0.0",
"@metamask/eth-trezor-keyring": "^3.1.3",
"@metamask/etherscan-link": "^3.0.0",
diff --git a/yarn.lock b/yarn.lock
index a0f9df5f4706..3c34b05fee52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5382,22 +5382,22 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6":
- version: 4.3.6
- resolution: "@metamask/eth-snap-keyring@npm:4.3.6"
+"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6, @metamask/eth-snap-keyring@npm:^4.4.0":
+ version: 4.4.0
+ resolution: "@metamask/eth-snap-keyring@npm:4.4.0"
dependencies:
"@ethereumjs/tx": "npm:^4.2.0"
"@metamask/eth-sig-util": "npm:^7.0.3"
- "@metamask/snaps-controllers": "npm:^9.7.0"
- "@metamask/snaps-sdk": "npm:^6.5.1"
- "@metamask/snaps-utils": "npm:^7.8.1"
+ "@metamask/snaps-controllers": "npm:^9.10.0"
+ "@metamask/snaps-sdk": "npm:^6.7.0"
+ "@metamask/snaps-utils": "npm:^8.3.0"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^9.2.1"
"@types/uuid": "npm:^9.0.8"
uuid: "npm:^9.0.1"
peerDependencies:
"@metamask/keyring-api": ^8.1.3
- checksum: 10/378dce125ba9e38b9ba7d9b7124383b4fd8d2782207dc69e1ae9e262beb83f22044eae5200986d4c353de29e5283c289e56b3acb88c8971a63f9365bdde3d5b4
+ checksum: 10/fd9926ba3706506bd9a16d1c2501e7c6cd7b7e3e7ea332bc7f28e0fca1f67f4514da51e6f9f4541a7354a2363d04c09c445f61b98fdc366432e1def9c2f27d07
languageName: node
linkType: hard
@@ -6282,7 +6282,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snaps-controllers@npm:^9.11.1, @metamask/snaps-controllers@npm:^9.7.0":
+"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.11.1":
version: 9.11.1
resolution: "@metamask/snaps-controllers@npm:9.11.1"
dependencies:
@@ -6379,7 +6379,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1":
+"@metamask/snaps-utils@npm:^7.4.0":
version: 7.8.1
resolution: "@metamask/snaps-utils@npm:7.8.1"
dependencies:
@@ -6410,7 +6410,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1":
+"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1":
version: 8.4.1
resolution: "@metamask/snaps-utils@npm:8.4.1"
dependencies:
@@ -26134,7 +26134,7 @@ __metadata:
"@metamask/eth-ledger-bridge-keyring": "npm:^3.0.1"
"@metamask/eth-query": "npm:^4.0.0"
"@metamask/eth-sig-util": "npm:^7.0.1"
- "@metamask/eth-snap-keyring": "npm:^4.3.6"
+ "@metamask/eth-snap-keyring": "npm:^4.4.0"
"@metamask/eth-token-tracker": "npm:^8.0.0"
"@metamask/eth-trezor-keyring": "npm:^3.1.3"
"@metamask/etherscan-link": "npm:^3.0.0"
From f58d598d221c4ea75c15f649ca542ddf6f16d911 Mon Sep 17 00:00:00 2001
From: seaona <54408225+seaona@users.noreply.github.com>
Date: Thu, 17 Oct 2024 14:12:29 +0200
Subject: [PATCH 24/51] fix: flaky test `Vault Decryptor Page is able to
decrypt the vault pasting the text in the vault-decryptor webapp` (#27921)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
The problem is that we are clicking an element when it's moving, making
the click take no effect.
In the onboarding steps, there is a carousel, where you can move from
one page to the other. The issue is that the classical methods
findElement or clickElement, can take place without having the correct
page fully visible (while it's moving), making the click, take no
effect.
To avoid our clicks taking no effect, we need to add a new method to
wait until an element is not moving. That's the condition we need before
clicking that element.
Note. adding this by default to the clickElement can be an overkill as
the check will delay all instances of clickElement. Since this only
happens in the onboarding, I decided to **not** add it by default in the
clickEelemtn for this reason. Only in the onboarding flow, we need to
make sure we wait for elements not moving.
![Screenshot from 2024-10-17
08-48-13](https://github.com/user-attachments/assets/3b4df3b3-8d3b-49b7-8de7-34082410cfdd)
![image](https://github.com/user-attachments/assets/dbd1d928-3c32-42c4-8e2a-006f1345a54e)
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27921?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27922
## **Manual testing steps**
1. Check ci run here
https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/106117/workflows/e0bb3426-1c21-481b-8c0b-4a417f149172/jobs/3963202
## **Screenshots/Recordings**
https://github.com/user-attachments/assets/1d00a2b6-3b03-482f-a6c1-c14fb7e5c318
## **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.
---
test/e2e/helpers.js | 23 +++++++++--
test/e2e/tests/onboarding/onboarding.spec.js | 41 +++++++++++--------
.../tests/privacy/basic-functionality.spec.js | 30 +++++++++++---
test/e2e/webdriver/driver.js | 40 ++++++++++++++++++
4 files changed, 108 insertions(+), 26 deletions(-)
diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js
index 564f99f2cde6..c857838f0810 100644
--- a/test/e2e/helpers.js
+++ b/test/e2e/helpers.js
@@ -535,7 +535,10 @@ const onboardingRevealAndConfirmSRP = async (driver) => {
await driver.clickElement('[data-testid="confirm-recovery-phrase"]');
- await driver.clickElement({ text: 'Confirm', tag: 'button' });
+ await driver.clickElementAndWaitToDisappear({
+ tag: 'button',
+ text: 'Confirm',
+ });
};
/**
@@ -566,7 +569,7 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => {
await driver.findElement({ text: 'Congratulations!', tag: 'h2' });
// opt-out from third party API on general section
- await driver.clickElement({
+ await driver.clickElementAndWaitToDisappear({
text: 'Manage default privacy settings',
tag: 'button',
});
@@ -575,7 +578,10 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => {
'[data-testid="basic-functionality-toggle"] .toggle-button',
);
await driver.clickElement('[id="basic-configuration-checkbox"]');
- await driver.clickElement({ text: 'Turn off', tag: 'button' });
+ await driver.clickElementAndWaitToDisappear({
+ tag: 'button',
+ text: 'Turn off',
+ });
// opt-out from third party API on assets section
await driver.clickElement('[data-testid="category-back-button"]');
@@ -588,10 +594,19 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => {
).map((toggle) => toggle.click()),
);
await driver.clickElement('[data-testid="category-back-button"]');
+
+ // Wait until the onboarding carousel has stopped moving
+ // otherwise the click has no effect.
+ await driver.waitForElementToStopMoving(
+ '[data-testid="privacy-settings-back-button"]',
+ );
await driver.clickElement('[data-testid="privacy-settings-back-button"]');
// complete onboarding
- await driver.clickElement({ text: 'Done', tag: 'button' });
+ await driver.clickElementAndWaitToDisappear({
+ tag: 'button',
+ text: 'Done',
+ });
await onboardingPinExtension(driver);
};
diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js
index 8d6b00de07ed..b5e273b7e978 100644
--- a/test/e2e/tests/onboarding/onboarding.spec.js
+++ b/test/e2e/tests/onboarding/onboarding.spec.js
@@ -328,28 +328,24 @@ describe('MetaMask onboarding @no-mmi', function () {
tag: 'button',
});
- await driver.clickElement({ text: 'Save', tag: 'button' });
-
- await driver.delay(largeDelayMs);
- await driver.waitForSelector('[data-testid="category-back-button"]');
- const generalBackButton = await driver.waitForSelector(
- '[data-testid="category-back-button"]',
- );
- await generalBackButton.click();
+ await driver.clickElementAndWaitToDisappear({
+ tag: 'button',
+ text: 'Save',
+ });
- await driver.delay(largeDelayMs);
+ await driver.clickElement('[data-testid="category-back-button"]');
- await driver.waitForSelector(
+ // Wait until the onboarding carousel has stopped moving
+ // otherwise the click has no effect.
+ await driver.waitForElementToStopMoving(
'[data-testid="privacy-settings-back-button"]',
);
- const defaultSettingsBackButton = await driver.findElement(
+
+ await driver.clickElement(
'[data-testid="privacy-settings-back-button"]',
);
- await defaultSettingsBackButton.click();
-
- await driver.delay(largeDelayMs);
- await driver.clickElement({
+ await driver.clickElementAndWaitToDisappear({
text: 'Done',
tag: 'button',
});
@@ -359,9 +355,14 @@ describe('MetaMask onboarding @no-mmi', function () {
tag: 'button',
});
- await driver.delay(largeDelayMs);
+ // Wait until the onboarding carousel has stopped moving
+ // otherwise the click has no effect.
+ await driver.waitForElementToStopMoving({
+ text: 'Done',
+ tag: 'button',
+ });
- await driver.clickElement({
+ await driver.clickElementAndWaitToDisappear({
text: 'Done',
tag: 'button',
});
@@ -412,6 +413,12 @@ describe('MetaMask onboarding @no-mmi', function () {
await driver.clickElement('[id="basic-configuration-checkbox"]');
await driver.clickElement({ text: 'Turn off', tag: 'button' });
await driver.clickElement('[data-testid="category-back-button"]');
+
+ // Wait until the onboarding carousel has stopped moving
+ // otherwise the click has no effect.
+ await driver.waitForElementToStopMoving(
+ '[data-testid="privacy-settings-back-button"]',
+ );
await driver.clickElement(
'[data-testid="privacy-settings-back-button"]',
);
diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js
index b4fc0e138104..6ae14ca660be 100644
--- a/test/e2e/tests/privacy/basic-functionality.spec.js
+++ b/test/e2e/tests/privacy/basic-functionality.spec.js
@@ -81,15 +81,35 @@ describe('MetaMask onboarding @no-mmi', function () {
'[data-testid="currency-rate-check-toggle"] .toggle-button',
);
await driver.clickElement('[data-testid="category-back-button"]');
- await driver.delay(regularDelayMs);
+
+ // Wait until the onboarding carousel has stopped moving
+ // otherwise the click has no effect.
+ await driver.waitForElementToStopMoving(
+ '[data-testid="privacy-settings-back-button"]',
+ );
await driver.clickElement(
'[data-testid="privacy-settings-back-button"]',
);
- await driver.delay(regularDelayMs);
- await driver.clickElement({ text: 'Done', tag: 'button' });
- await driver.clickElement('[data-testid="pin-extension-next"]');
- await driver.clickElement({ text: 'Done', tag: 'button' });
+ await driver.clickElementAndWaitToDisappear({
+ text: 'Done',
+ tag: 'button',
+ });
+ await driver.clickElement({
+ text: 'Next',
+ tag: 'button',
+ });
+
+ // Wait until the onboarding carousel has stopped moving
+ // otherwise the click has no effect.
+ await driver.waitForElementToStopMoving({
+ text: 'Done',
+ tag: 'button',
+ });
+ await driver.clickElementAndWaitToDisappear({
+ text: 'Done',
+ tag: 'button',
+ });
await driver.clickElement('[data-testid="network-display"]');
diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js
index b0648f122fb9..813d00d5e0e8 100644
--- a/test/e2e/webdriver/driver.js
+++ b/test/e2e/webdriver/driver.js
@@ -597,6 +597,46 @@ class Driver {
}
}
+ /**
+ * Checks if an element is moving by comparing its position at two different times.
+ *
+ * @param {string | object} rawLocator - Element locator.
+ * @returns {Promise} Promise that resolves to a boolean indicating if the element is moving.
+ */
+ async isElementMoving(rawLocator) {
+ const element = await this.findElement(rawLocator);
+ const initialPosition = await element.getRect();
+
+ await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for a short period
+
+ const newPosition = await element.getRect();
+
+ return (
+ initialPosition.x !== newPosition.x || initialPosition.y !== newPosition.y
+ );
+ }
+
+ /**
+ * Waits until an element stops moving within a specified timeout period.
+ *
+ * @param {string | object} rawLocator - Element locator.
+ * @param {number} timeout - The maximum time to wait for the element to stop moving.
+ * @returns {Promise} Promise that resolves when the element stops moving.
+ * @throws {Error} Throws an error if the element does not stop moving within the timeout period.
+ */
+ async waitForElementToStopMoving(rawLocator, timeout = 5000) {
+ const startTime = Date.now();
+
+ while (Date.now() - startTime < timeout) {
+ if (!(await this.isElementMoving(rawLocator))) {
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 500)); // Check every 500ms
+ }
+
+ throw new Error('Element did not stop moving within the timeout period');
+ }
+
/** @param {string} title - The title of the window or tab the screenshot is being taken in */
async takeScreenshot(title) {
const filepathBase = `${artifactDir(title)}/test-screenshot`;
From dc48117984e59f2bcf507dda48675bf9c4da9419 Mon Sep 17 00:00:00 2001
From: Pedro Figueiredo
Date: Thu, 17 Oct 2024 13:57:29 +0100
Subject: [PATCH 25/51] feat: Add transaction flow and details sections
(#27654)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Adds two new sections for the wallet initiated ERC20 token transfer
redesigned confirmation.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27654?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3220
## **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/en/messages.json | 3 +
.../legacy/watch-asset-confirmation.ts | 20 ++
.../redesign}/confirmation.ts | 4 +-
...proval-for-all-transaction-confirmation.ts | 6 +-
.../redesign/token-transfer-confirmation.ts | 45 +++
.../redesign/transaction-confirmation.ts | 25 ++
.../pages/send/send-token-page.ts | 16 +
test/e2e/page-objects/pages/test-dapp.ts | 15 +-
.../pages/transaction-confirmation.ts | 5 -
...55-revoke-set-approval-for-all-redesign.ts | 2 +-
...1155-set-approval-for-all-redesign.spec.ts | 2 +-
.../erc20-token-send-redesign.spec.ts | 115 +++++++
...21-revoke-set-approval-for-all-redesign.ts | 2 +-
...c721-set-approval-for-all-redesign.spec.ts | 2 +-
.../confirm/info/approve/approve.tsx | 8 +-
.../base-transaction-info.tsx | 8 +-
.../info/hooks/use-token-values.test.ts | 148 +++++----
.../confirm/info/hooks/use-token-values.ts | 87 +++--
.../set-approval-for-all-info.tsx | 8 +-
.../advanced-details.test.tsx.snap | 23 +-
.../advanced-details.test.tsx | 45 ++-
.../advanced-details/advanced-details.tsx | 15 +-
.../__snapshots__/send-heading.test.tsx.snap | 49 ++-
.../send-heading/send-heading.stories.tsx | 4 +-
.../info/shared/send-heading/send-heading.tsx | 17 +-
.../token-details-section.test.tsx.snap | 118 +++++++
.../token-transfer.test.tsx.snap | 309 +++++++++++++++++-
.../transaction-flow-section.test.tsx.snap | 53 +++
.../token-details-section.test.tsx | 26 ++
.../token-transfer/token-details-section.tsx | 76 +++++
.../token-transfer/token-transfer.stories.tsx | 18 +-
.../token-transfer/token-transfer.test.tsx | 8 +
.../info/token-transfer/token-transfer.tsx | 14 +-
.../transaction-flow-section.test.tsx | 48 +++
.../transaction-flow-section.tsx | 61 ++++
.../alerts/useConfirmationOriginAlerts.ts | 2 +-
36 files changed, 1200 insertions(+), 207 deletions(-)
create mode 100644 test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts
rename test/e2e/page-objects/pages/{ => confirmations/redesign}/confirmation.ts (85%)
rename test/e2e/page-objects/pages/{ => confirmations/redesign}/set-approval-for-all-transaction-confirmation.ts (89%)
create mode 100644 test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts
create mode 100644 test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts
delete mode 100644 test/e2e/page-objects/pages/transaction-confirmation.ts
create mode 100644 test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts
create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap
create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap
create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx
create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx
create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx
create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index bb10d6f579a0..6834ba7169c7 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -6179,6 +6179,9 @@
"transactionFee": {
"message": "Transaction fee"
},
+ "transactionFlowNetwork": {
+ "message": "Network"
+ },
"transactionHistoryBaseFee": {
"message": "Base fee (GWEI)"
},
diff --git a/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts b/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts
new file mode 100644
index 000000000000..23f1b010de87
--- /dev/null
+++ b/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts
@@ -0,0 +1,20 @@
+import { Driver } from '../../../../webdriver/driver';
+import { RawLocator } from '../../../common';
+
+class WatchAssetConfirmation {
+ private driver: Driver;
+
+ private footerConfirmButton: RawLocator;
+
+ constructor(driver: Driver) {
+ this.driver = driver;
+
+ this.footerConfirmButton = '[data-testid="page-container-footer-next"]';
+ }
+
+ async clickFooterConfirmButton() {
+ await this.driver.clickElement(this.footerConfirmButton);
+ }
+}
+
+export default WatchAssetConfirmation;
diff --git a/test/e2e/page-objects/pages/confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts
similarity index 85%
rename from test/e2e/page-objects/pages/confirmation.ts
rename to test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts
index 3ec372cb3163..f8fc66c3fc65 100644
--- a/test/e2e/page-objects/pages/confirmation.ts
+++ b/test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts
@@ -1,5 +1,5 @@
-import { Driver } from '../../webdriver/driver';
-import { RawLocator } from '../common';
+import { Driver } from '../../../../webdriver/driver';
+import { RawLocator } from '../../../common';
class Confirmation {
protected driver: Driver;
diff --git a/test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts
similarity index 89%
rename from test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts
rename to test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts
index 5259e0a51dcd..a1aadeff3376 100644
--- a/test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts
+++ b/test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts
@@ -1,6 +1,6 @@
-import { tEn } from '../../../lib/i18n-helpers';
-import { Driver } from '../../webdriver/driver';
-import { RawLocator } from '../common';
+import { tEn } from '../../../../../lib/i18n-helpers';
+import { Driver } from '../../../../webdriver/driver';
+import { RawLocator } from '../../../common';
import TransactionConfirmation from './transaction-confirmation';
class SetApprovalForAllTransactionConfirmation extends TransactionConfirmation {
diff --git a/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts
new file mode 100644
index 000000000000..837c7aa24e21
--- /dev/null
+++ b/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts
@@ -0,0 +1,45 @@
+import { tEn } from '../../../../../lib/i18n-helpers';
+import { Driver } from '../../../../webdriver/driver';
+import { RawLocator } from '../../../common';
+import TransactionConfirmation from './transaction-confirmation';
+
+class TokenTransferTransactionConfirmation extends TransactionConfirmation {
+ private networkParagraph: RawLocator;
+
+ private interactingWithParagraph: RawLocator;
+
+ private networkFeeParagraph: RawLocator;
+
+ constructor(driver: Driver) {
+ super(driver);
+
+ this.driver = driver;
+
+ this.networkParagraph = {
+ css: 'p',
+ text: tEn('transactionFlowNetwork') as string,
+ };
+ this.interactingWithParagraph = {
+ css: 'p',
+ text: tEn('interactingWith') as string,
+ };
+ this.networkFeeParagraph = {
+ css: 'p',
+ text: tEn('networkFee') as string,
+ };
+ }
+
+ async check_networkParagraph() {
+ await this.driver.waitForSelector(this.networkParagraph);
+ }
+
+ async check_interactingWithParagraph() {
+ await this.driver.waitForSelector(this.interactingWithParagraph);
+ }
+
+ async check_networkFeeParagraph() {
+ await this.driver.waitForSelector(this.networkFeeParagraph);
+ }
+}
+
+export default TokenTransferTransactionConfirmation;
diff --git a/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts
new file mode 100644
index 000000000000..661feef33197
--- /dev/null
+++ b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts
@@ -0,0 +1,25 @@
+import { tEn } from '../../../../../lib/i18n-helpers';
+import { Driver } from '../../../../webdriver/driver';
+import { RawLocator } from '../../../common';
+import Confirmation from './confirmation';
+
+class TransactionConfirmation extends Confirmation {
+ private walletInitiatedHeadingTitle: RawLocator;
+
+ constructor(driver: Driver) {
+ super(driver);
+
+ this.driver = driver;
+
+ this.walletInitiatedHeadingTitle = {
+ css: 'h3',
+ text: tEn('review') as string,
+ };
+ }
+
+ async check_walletInitiatedHeadingTitle() {
+ await this.driver.waitForSelector(this.walletInitiatedHeadingTitle);
+ }
+}
+
+export default TransactionConfirmation;
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 65727b106783..728afbfdd4df 100644
--- a/test/e2e/page-objects/pages/send/send-token-page.ts
+++ b/test/e2e/page-objects/pages/send/send-token-page.ts
@@ -1,5 +1,6 @@
import { strict as assert } from 'assert';
import { Driver } from '../../../webdriver/driver';
+import { RawLocator } from '../../common';
class SendTokenPage {
private driver: Driver;
@@ -18,6 +19,10 @@ class SendTokenPage {
private ensResolvedAddress: string;
+ private assetPickerButton: RawLocator;
+
+ private tokenListButton: RawLocator;
+
constructor(driver: Driver) {
this.driver = driver;
this.inputAmount = '[data-testid="currency-input"]';
@@ -32,6 +37,8 @@ class SendTokenPage {
text: 'Continue',
tag: 'button',
};
+ this.assetPickerButton = '[data-testid="asset-picker-button"]';
+ this.tokenListButton = '[data-testid="multichain-token-list-button"]';
}
async check_pageIsLoaded(): Promise {
@@ -125,6 +132,15 @@ class SendTokenPage {
`ENS domain '${ensDomain}' resolved to address '${address}' and can be used as recipient on send token screen.`,
);
}
+
+ async click_assetPickerButton() {
+ await this.driver.clickElement(this.assetPickerButton);
+ }
+
+ async click_secondTokenListButton() {
+ const elements = await this.driver.findElements(this.tokenListButton);
+ await elements[1].click();
+ }
}
export default SendTokenPage;
diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts
index ffb1f9033bdb..c0f71f1b3280 100644
--- a/test/e2e/page-objects/pages/test-dapp.ts
+++ b/test/e2e/page-objects/pages/test-dapp.ts
@@ -1,5 +1,6 @@
-import { Driver } from '../../webdriver/driver';
import { WINDOW_TITLES } from '../../helpers';
+import { Driver } from '../../webdriver/driver';
+import { RawLocator } from '../common';
const DAPP_HOST_ADDRESS = '127.0.0.1:8080';
const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`;
@@ -83,8 +84,16 @@ class TestDapp {
private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult';
+ private erc20WatchAssetButton: RawLocator;
+
constructor(driver: Driver) {
this.driver = driver;
+
+ this.erc721SetApprovalForAllButton = '#setApprovalForAllButton';
+ this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button';
+ this.erc721RevokeSetApprovalForAllButton = '#revokeButton';
+ this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button';
+ this.erc20WatchAssetButton = '#watchAssets';
}
async check_pageIsLoaded(): Promise {
@@ -143,6 +152,10 @@ class TestDapp {
await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton);
}
+ public async clickERC20WatchAssetButton() {
+ await this.driver.clickElement(this.erc20WatchAssetButton);
+ }
+
/**
* Verify the failed personal sign signature.
*
diff --git a/test/e2e/page-objects/pages/transaction-confirmation.ts b/test/e2e/page-objects/pages/transaction-confirmation.ts
deleted file mode 100644
index 7ae98d74d4c8..000000000000
--- a/test/e2e/page-objects/pages/transaction-confirmation.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import Confirmation from './confirmation';
-
-class TransactionConfirmation extends Confirmation {}
-
-export default TransactionConfirmation;
diff --git a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts
index 3e75adb34db8..1f9d05cd26a8 100644
--- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts
+++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts
@@ -3,7 +3,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller';
import { DAPP_URL } from '../../../constants';
import { unlockWallet, WINDOW_TITLES } from '../../../helpers';
import { Mockttp } from '../../../mock-e2e';
-import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation';
+import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation';
import TestDapp from '../../../page-objects/pages/test-dapp';
import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry';
import { Driver } from '../../../webdriver/driver';
diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts
index 0e1134737c87..c9c9fdbd5eda 100644
--- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts
+++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts
@@ -2,7 +2,7 @@
import { TransactionEnvelopeType } from '@metamask/transaction-controller';
import { DAPP_URL, unlockWallet, WINDOW_TITLES } from '../../../helpers';
import { Mockttp } from '../../../mock-e2e';
-import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation';
+import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation';
import TestDapp from '../../../page-objects/pages/test-dapp';
import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry';
import { Driver } from '../../../webdriver/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
new file mode 100644
index 000000000000..83892b1ca6e1
--- /dev/null
+++ b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts
@@ -0,0 +1,115 @@
+/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
+import { TransactionEnvelopeType } from '@metamask/transaction-controller';
+import { DAPP_URL } from '../../../constants';
+import { unlockWallet, WINDOW_TITLES } from '../../../helpers';
+import { Mockttp } from '../../../mock-e2e';
+import WatchAssetConfirmation from '../../../page-objects/pages/confirmations/legacy/watch-asset-confirmation';
+import TokenTransferTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/token-transfer-confirmation';
+import HomePage from '../../../page-objects/pages/homepage';
+import SendTokenPage from '../../../page-objects/pages/send/send-token-page';
+import TestDapp from '../../../page-objects/pages/test-dapp';
+import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry';
+import { Driver } from '../../../webdriver/driver';
+import { withRedesignConfirmationFixtures } from '../helpers';
+import { TestSuiteArguments } from './shared';
+
+const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts');
+
+describe('Confirmation Redesign ERC20 Token Send @no-mmi', function () {
+ it('Sends a type 0 transaction (Legacy)', async function () {
+ await withRedesignConfirmationFixtures(
+ this.test?.fullTitle(),
+ TransactionEnvelopeType.legacy,
+ async ({ driver, contractRegistry }: TestSuiteArguments) => {
+ await createTransactionAndAssertDetails(driver, contractRegistry);
+ },
+ mocks,
+ SMART_CONTRACTS.HST,
+ );
+ });
+
+ it('Sends a type 2 transaction (EIP1559)', async function () {
+ await withRedesignConfirmationFixtures(
+ this.test?.fullTitle(),
+ TransactionEnvelopeType.feeMarket,
+ async ({ driver, contractRegistry }: TestSuiteArguments) => {
+ await createTransactionAndAssertDetails(driver, contractRegistry);
+ },
+ mocks,
+ SMART_CONTRACTS.HST,
+ );
+ });
+});
+
+async function mocks(server: Mockttp) {
+ return [await mockedSourcifyTokenSend(server)];
+}
+
+export async function mockedSourcifyTokenSend(mockServer: Mockttp) {
+ return await mockServer
+ .forGet('https://www.4byte.directory/api/v1/signatures/')
+ .withQuery({ hex_signature: '0xa9059cbb' })
+ .always()
+ .thenCallback(() => ({
+ statusCode: 200,
+ json: {
+ count: 1,
+ next: null,
+ previous: null,
+ results: [
+ {
+ bytes_signature: '©\u0005»',
+ created_at: '2016-07-09T03:58:28.234977Z',
+ hex_signature: '0xa9059cbb',
+ id: 145,
+ text_signature: 'transfer(address,uint256)',
+ },
+ ],
+ },
+ }));
+}
+
+async function createTransactionAndAssertDetails(
+ driver: Driver,
+ contractRegistry?: GanacheContractAddressRegistry,
+) {
+ await unlockWallet(driver);
+
+ const contractAddress = await (
+ contractRegistry as GanacheContractAddressRegistry
+ ).getContractAddress(SMART_CONTRACTS.HST);
+
+ const testDapp = new TestDapp(driver);
+
+ await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL });
+
+ await testDapp.clickERC20WatchAssetButton();
+
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+
+ const watchAssetConfirmation = new WatchAssetConfirmation(driver);
+ await watchAssetConfirmation.clickFooterConfirmButton();
+
+ await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView);
+
+ const homePage = new HomePage(driver);
+ await homePage.startSendFlow();
+
+ const sendToPage = new SendTokenPage(driver);
+ await sendToPage.check_pageIsLoaded();
+ await sendToPage.fillRecipient('0x2f318C334780961FB129D2a6c30D0763d9a5C970');
+ await sendToPage.fillAmount('1');
+
+ await sendToPage.click_assetPickerButton();
+ await sendToPage.click_secondTokenListButton();
+ await sendToPage.goToNextScreen();
+
+ const tokenTransferTransactionConfirmation =
+ new TokenTransferTransactionConfirmation(driver);
+ await tokenTransferTransactionConfirmation.check_walletInitiatedHeadingTitle();
+ await tokenTransferTransactionConfirmation.check_networkParagraph();
+ await tokenTransferTransactionConfirmation.check_interactingWithParagraph();
+ await tokenTransferTransactionConfirmation.check_networkFeeParagraph();
+
+ await tokenTransferTransactionConfirmation.clickFooterConfirmButton();
+}
diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts
index 138695904e55..b0f1291a47d9 100644
--- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts
+++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts
@@ -3,7 +3,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller';
import { DAPP_URL } from '../../../constants';
import { unlockWallet, WINDOW_TITLES } from '../../../helpers';
import { Mockttp } from '../../../mock-e2e';
-import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation';
+import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation';
import TestDapp from '../../../page-objects/pages/test-dapp';
import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry';
import { Driver } from '../../../webdriver/driver';
diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts
index 589670212be1..9e481ee9c75f 100644
--- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts
+++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts
@@ -2,7 +2,7 @@
import { TransactionEnvelopeType } from '@metamask/transaction-controller';
import { DAPP_URL, unlockWallet, WINDOW_TITLES } from '../../../helpers';
import { Mockttp } from '../../../mock-e2e';
-import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation';
+import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation';
import TestDapp from '../../../page-objects/pages/test-dapp';
import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry';
import { Driver } from '../../../webdriver/driver';
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx
index eabf8639ccfb..fed03a75e17e 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx
@@ -3,10 +3,8 @@ import {
TransactionType,
} from '@metamask/transaction-controller';
import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
import { useConfirmContext } from '../../../../context/confirm';
import { useAssetDetails } from '../../../../hooks/useAssetDetails';
-import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences';
import { AdvancedDetails } from '../shared/advanced-details/advanced-details';
import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader';
import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section';
@@ -24,10 +22,6 @@ const ApproveInfo = () => {
currentConfirmation: TransactionMeta;
};
- const showAdvancedDetails = useSelector(
- selectConfirmationAdvancedDetailsOpen,
- );
-
const { isNFT } = useIsNFT(transactionMeta);
const [isOpenEditSpendingCapModal, setIsOpenEditSpendingCapModal] =
@@ -70,7 +64,7 @@ const ApproveInfo = () => {
/>
)}
- {showAdvancedDetails && }
+
{
const { currentConfirmation: transactionMeta } =
useConfirmContext();
- const showAdvancedDetails = useSelector(
- selectConfirmationAdvancedDetailsOpen,
- );
-
if (!transactionMeta?.txParams) {
return null;
}
@@ -33,7 +27,7 @@ const BaseTransactionInfo = () => {
- {showAdvancedDetails && }
+
>
);
};
diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts
index 7ac4aa5b5c92..1ed5e9c249ff 100644
--- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts
+++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts
@@ -1,120 +1,126 @@
import { TransactionMeta } from '@metamask/transaction-controller';
+import { Numeric } from '../../../../../../../shared/modules/Numeric';
import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer';
import mockState from '../../../../../../../test/data/mock-state.json';
-import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers';
-// import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate';
-import { Numeric } from '../../../../../../../shared/modules/Numeric';
+import { renderHookWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers';
import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate';
-import { useTokenTracker } from '../../../../../../hooks/useTokenTracker';
+import { useAssetDetails } from '../../../../hooks/useAssetDetails';
import { useTokenValues } from './use-token-values';
+import { useDecodedTransactionData } from './useDecodedTransactionData';
+
+jest.mock('../../../../hooks/useAssetDetails', () => ({
+ ...jest.requireActual('../../../../hooks/useAssetDetails'),
+ useAssetDetails: jest.fn(),
+}));
+
+jest.mock('./useDecodedTransactionData', () => ({
+ ...jest.requireActual('./useDecodedTransactionData'),
+ useDecodedTransactionData: jest.fn(),
+}));
jest.mock(
'../../../../../../components/app/currency-input/hooks/useTokenExchangeRate',
() => jest.fn(),
);
-jest.mock('../../../../../../hooks/useTokenTracker', () => ({
- ...jest.requireActual('../../../../../../hooks/useTokenTracker'),
- useTokenTracker: jest.fn(),
-}));
-
describe('useTokenValues', () => {
+ const useAssetDetailsMock = jest.mocked(useAssetDetails);
+ const useDecodedTransactionDataMock = jest.mocked(useDecodedTransactionData);
const useTokenExchangeRateMock = jest.mocked(useTokenExchangeRate);
- const useTokenTrackerMock = jest.mocked(useTokenTracker);
- const TEST_SELECTED_TOKEN = {
- address: 'address',
- decimals: 18,
- symbol: 'symbol',
- iconUrl: 'iconUrl',
- image: 'image',
- };
-
- it('returns native and fiat balances', async () => {
- (useTokenTrackerMock as jest.Mock).mockResolvedValue({
- tokensWithBalances: [
- {
- address: '0x076146c765189d51be3160a2140cf80bfc73ad68',
- balance: '1000000000000000000',
- decimals: 18,
- },
- ],
- });
-
- (useTokenExchangeRateMock as jest.Mock).mockResolvedValue(
- new Numeric(1, 10),
- );
-
- const transactionMeta = genUnapprovedTokenTransferConfirmation(
- {},
- ) as TransactionMeta;
-
- const { result, waitForNextUpdate } = renderHookWithProvider(
- () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN),
- mockState,
- );
-
- await waitForNextUpdate();
-
- expect(result.current).toEqual({
- fiatDisplayValue: '$1.00',
- tokenBalance: '1',
- });
+ beforeEach(() => {
+ jest.resetAllMocks();
});
- it('returns undefined native and fiat balances if no token with balances is returned', async () => {
- (useTokenTrackerMock as jest.Mock).mockResolvedValue({
- tokensWithBalances: [],
- });
-
+ it('returns native and fiat balances', async () => {
+ (useAssetDetailsMock as jest.Mock).mockImplementation(() => ({
+ decimals: '10',
+ }));
+ (useDecodedTransactionDataMock as jest.Mock).mockImplementation(() => ({
+ pending: false,
+ value: {
+ data: [
+ {
+ name: 'transfer',
+ params: [
+ {
+ type: 'address',
+ value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4',
+ },
+ {
+ type: 'uint256',
+ value: 70000000000,
+ },
+ ],
+ },
+ ],
+ source: 'FourByte',
+ },
+ }));
(useTokenExchangeRateMock as jest.Mock).mockResolvedValue(
- new Numeric(1, 10),
+ new Numeric(0.91, 10),
);
const transactionMeta = genUnapprovedTokenTransferConfirmation(
{},
) as TransactionMeta;
- const { result, waitForNextUpdate } = renderHookWithProvider(
- () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN),
+ const { result, waitForNextUpdate } = renderHookWithConfirmContextProvider(
+ () => useTokenValues(transactionMeta),
mockState,
);
await waitForNextUpdate();
expect(result.current).toEqual({
- fiatDisplayValue: undefined,
- tokenBalance: undefined,
+ decodedTransferValue: 7,
+ fiatDisplayValue: '$6.37',
+ pending: false,
});
});
it('returns undefined fiat balance if no token rate is returned', async () => {
- (useTokenTrackerMock as jest.Mock).mockResolvedValue({
- tokensWithBalances: [
- {
- address: '0x076146c765189d51be3160a2140cf80bfc73ad68',
- balance: '1000000000000000000',
- decimals: 18,
- },
- ],
- });
-
+ (useAssetDetailsMock as jest.Mock).mockImplementation(() => ({
+ decimals: '10',
+ }));
+ (useDecodedTransactionDataMock as jest.Mock).mockImplementation(() => ({
+ pending: false,
+ value: {
+ data: [
+ {
+ name: 'transfer',
+ params: [
+ {
+ type: 'address',
+ value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4',
+ },
+ {
+ type: 'uint256',
+ value: 70000000000,
+ },
+ ],
+ },
+ ],
+ source: 'FourByte',
+ },
+ }));
(useTokenExchangeRateMock as jest.Mock).mockResolvedValue(null);
const transactionMeta = genUnapprovedTokenTransferConfirmation(
{},
) as TransactionMeta;
- const { result, waitForNextUpdate } = renderHookWithProvider(
- () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN),
+ const { result, waitForNextUpdate } = renderHookWithConfirmContextProvider(
+ () => useTokenValues(transactionMeta),
mockState,
);
await waitForNextUpdate();
expect(result.current).toEqual({
+ decodedTransferValue: 7,
fiatDisplayValue: null,
- tokenBalance: '1',
+ pending: false,
});
});
});
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 9515a45515bf..139a1e8116b9 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
@@ -1,38 +1,44 @@
import { TransactionMeta } from '@metamask/transaction-controller';
+import { isHexString } from '@metamask/utils';
+import { BigNumber } from 'bignumber.js';
+import { isBoolean } from 'lodash';
import { useMemo, useState } from 'react';
-import { calcTokenAmount } from '../../../../../../../shared/lib/transactions-controller-utils';
-import { toChecksumHexAddress } from '../../../../../../../shared/modules/hexstring-utils';
import { Numeric } from '../../../../../../../shared/modules/Numeric';
import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate';
import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter';
-import { useTokenTracker } from '../../../../../../hooks/useTokenTracker';
-import { SelectedToken } from '../shared/selected-token';
+import { useAssetDetails } from '../../../../hooks/useAssetDetails';
+import { useDecodedTransactionData } from './useDecodedTransactionData';
-export const useTokenValues = (
- transactionMeta: TransactionMeta,
- selectedToken: SelectedToken,
-) => {
- const [tokensWithBalances, setTokensWithBalances] = useState<
- { balance: string; address: string; decimals: number; string: string }[]
- >([]);
+export const useTokenValues = (transactionMeta: TransactionMeta) => {
+ const { decimals } = useAssetDetails(
+ transactionMeta.txParams.to,
+ transactionMeta.txParams.from,
+ transactionMeta.txParams.data,
+ );
- const fetchTokenBalances = async () => {
- const result: {
- tokensWithBalances: {
- balance: string;
- address: string;
- decimals: number;
- string: string;
- }[];
- } = await useTokenTracker({
- tokens: [selectedToken],
- address: undefined,
- });
+ const decodedResponse = useDecodedTransactionData();
+ const { value, pending } = decodedResponse;
- setTokensWithBalances(result.tokensWithBalances);
- };
+ const decodedTransferValue = useMemo(() => {
+ if (!value || !decimals) {
+ return 0;
+ }
- fetchTokenBalances();
+ const paramIndex = value.data[0].params.findIndex(
+ (param) =>
+ param.value !== undefined &&
+ !isHexString(param.value) &&
+ param.value.length === undefined &&
+ !isBoolean(param.value),
+ );
+ if (paramIndex === -1) {
+ return 0;
+ }
+
+ return new BigNumber(value.data[0].params[paramIndex].value.toString())
+ .dividedBy(new BigNumber(10).pow(Number(decimals)))
+ .toNumber();
+ }, [value, decimals]);
const [exchangeRate, setExchangeRate] = useState();
const fetchExchangeRate = async () => {
@@ -40,38 +46,19 @@ export const useTokenValues = (
setExchangeRate(result);
};
-
fetchExchangeRate();
- const tokenBalance = useMemo(() => {
- const tokenWithBalance = tokensWithBalances.find(
- (token: {
- balance: string;
- address: string;
- decimals: number;
- string: string;
- }) =>
- toChecksumHexAddress(token.address) ===
- toChecksumHexAddress(transactionMeta?.txParams?.to as string),
- );
-
- if (!tokenWithBalance) {
- return undefined;
- }
-
- return calcTokenAmount(tokenWithBalance.balance, tokenWithBalance.decimals);
- }, [tokensWithBalances]);
-
const fiatValue =
- exchangeRate && tokenBalance && exchangeRate.times(tokenBalance).toNumber();
-
+ exchangeRate &&
+ decodedTransferValue &&
+ exchangeRate.times(decodedTransferValue, 10).toNumber();
const fiatFormatter = useFiatFormatter();
-
const fiatDisplayValue =
fiatValue && fiatFormatter(fiatValue, { shorten: true });
return {
+ decodedTransferValue,
fiatDisplayValue,
- tokenBalance: tokenBalance && String(tokenBalance.toNumber()),
+ pending,
};
};
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx
index 92df913783a1..6902a6da9b1f 100644
--- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx
+++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx
@@ -1,8 +1,6 @@
import { TransactionMeta } from '@metamask/transaction-controller';
import React from 'react';
-import { useSelector } from 'react-redux';
import { useConfirmContext } from '../../../../context/confirm';
-import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences';
import { ApproveDetails } from '../approve/approve-details/approve-details';
import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData';
import { AdvancedDetails } from '../shared/advanced-details/advanced-details';
@@ -16,10 +14,6 @@ const SetApprovalForAllInfo = () => {
const { currentConfirmation: transactionMeta } =
useConfirmContext();
- const showAdvancedDetails = useSelector(
- selectConfirmationAdvancedDetailsOpen,
- );
-
const decodedResponse = useDecodedTransactionData();
const { value, pending } = decodedResponse;
@@ -45,7 +39,7 @@ const SetApprovalForAllInfo = () => {
)}
- {showAdvancedDetails && }
+
>
);
};
diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap
index f66db615defe..3a93bae1e26d 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap
@@ -1,6 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[` does not render component for advanced transaction details 1`] = `
+exports[` does not render component when the state property is false 1`] = ``;
+
+exports[` renders component when the prop override is passed 1`] = `
does not render component for advanced transaction
does not render component for advanced transaction
`;
-exports[`
renders component for advanced transaction details 1`] = `
+exports[`
renders component when the state property is true 1`] = `
renders component for advanced transaction details
renders component for advanced transaction details
class="mm-box mm-text mm-text--body-md mm-box--color-inherit"
style="white-space: pre-wrap;"
>
- 12
+ undefined
-
diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx
index b965e2015895..60512441e77d 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx
@@ -9,8 +9,18 @@ import { AdvancedDetails } from './advanced-details';
describe('', () => {
const middleware = [thunk];
- it('does not render component for advanced transaction details', () => {
- const state = mockState;
+ it('does not render component when the state property is false', () => {
+ const state = {
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ preferences: {
+ ...mockState.metamask.preferences,
+ showConfirmationAdvancedDetails: false,
+ },
+ },
+ };
+
const mockStore = configureMockStore(middleware)(state);
const { container } = renderWithConfirmContextProvider(
,
@@ -20,16 +30,18 @@ describe('', () => {
expect(container).toMatchSnapshot();
});
- it('renders component for advanced transaction details', () => {
+ it('renders component when the state property is true', () => {
const state = {
...mockState,
metamask: {
...mockState.metamask,
- useNonceField: true,
- nextNonce: 1,
- customNonceValue: '12',
+ preferences: {
+ ...mockState.metamask.preferences,
+ showConfirmationAdvancedDetails: true,
+ },
},
};
+
const mockStore = configureMockStore(middleware)(state);
const { container } = renderWithConfirmContextProvider(
,
@@ -38,4 +50,25 @@ describe('', () => {
expect(container).toMatchSnapshot();
});
+
+ it('renders component when the prop override is passed', () => {
+ const state = {
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ preferences: {
+ ...mockState.metamask.preferences,
+ showConfirmationAdvancedDetails: false,
+ },
+ },
+ };
+
+ const mockStore = configureMockStore(middleware)(state);
+ const { container } = renderWithConfirmContextProvider(
+ ,
+ mockStore,
+ );
+
+ expect(container).toMatchSnapshot();
+ });
});
diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx
index 7e0cee721bb8..ebb0f69d75c1 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx
@@ -16,6 +16,7 @@ import {
showModal,
updateCustomNonce,
} from '../../../../../../../store/actions';
+import { selectConfirmationAdvancedDetailsOpen } from '../../../../../selectors/preferences';
import { TransactionData } from '../transaction-data/transaction-data';
const NonceDetails = () => {
@@ -65,7 +66,19 @@ const NonceDetails = () => {
);
};
-export const AdvancedDetails: React.FC = () => {
+export const AdvancedDetails = ({
+ overrideVisibility = false,
+}: {
+ overrideVisibility?: boolean;
+}) => {
+ const showAdvancedDetails = useSelector(
+ selectConfirmationAdvancedDetailsOpen,
+ );
+
+ if (!overrideVisibility && !showAdvancedDetails) {
+ return null;
+ }
+
return (
<>
diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap
index e4222b56cbc5..677a5a357155 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap
@@ -3,18 +3,47 @@
exports[` renders component 1`] = `
-
- ?
-
-
- Unknown
-
+
+
+
+
+
+
+
+
`;
diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx
index f4bfb484c107..8944a84b770e 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx
@@ -13,7 +13,9 @@ const Story = {
component: SendHeading,
decorators: [
(story: () => Meta) => (
- {story()}
+
+ {story()}
+
),
],
};
diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx
index d571c61ee93e..2806c33936c0 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx
@@ -22,6 +22,7 @@ import { MultichainState } from '../../../../../../../selectors/multichain';
import { useConfirmContext } from '../../../../../context/confirm';
import { useTokenImage } from '../../hooks/use-token-image';
import { useTokenValues } from '../../hooks/use-token-values';
+import { ConfirmLoader } from '../confirm-loader/confirm-loader';
const SendHeading = () => {
const t = useI18nContext();
@@ -31,10 +32,8 @@ const SendHeading = () => {
getWatchedToken(transactionMeta)(state),
);
const { tokenImage } = useTokenImage(transactionMeta, selectedToken);
- const { tokenBalance, fiatDisplayValue } = useTokenValues(
- transactionMeta,
- selectedToken,
- );
+ const { decodedTransferValue, fiatDisplayValue, pending } =
+ useTokenValues(transactionMeta);
const TokenImage = (
{
variant={TextVariant.headingLg}
color={TextColor.inherit}
marginTop={3}
- >{`${tokenBalance || ''} ${selectedToken?.symbol || t('unknown')}`}
+ >{`${decodedTransferValue || ''} ${
+ selectedToken?.symbol || t('unknown')
+ }`}
{fiatDisplayValue && (
{fiatDisplayValue}
@@ -67,13 +68,17 @@ const SendHeading = () => {
>
);
+ if (pending) {
+ return ;
+ }
+
return (
{TokenImage}
{TokenValue}
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap
new file mode 100644
index 000000000000..c545cea1f66d
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TokenDetailsSection renders correctly 1`] = `
+
+
+
+
+
+
+
+
+
+ 0x07614...3ad68
+
+
+
+
+
+
+`;
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap
index 63b44d50173d..c9813ea1470e 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap
@@ -3,18 +3,313 @@
exports[`TokenTransferInfo renders correctly 1`] = `
+
+
-
- Unknown
-
+
+
+
+
+
+ 0x07614...3ad68
+
+
+
+
+
+
+
+
+
+
+ 0.0001 ETH
+
+
+ $0.04
+
+
+
+
+
+
+
+
+
+ 🦊 Market
+
+
+
+ ~
+ 0 sec
+
+
+
+
+
`;
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap
new file mode 100644
index 000000000000..23cddb2b59b2
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders correctly 1`] = `
+
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
+
+
+
+
+ 0x6B175...71d0F
+
+
+
+
+
+
+`;
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx
new file mode 100644
index 000000000000..4188ea62bc84
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import configureMockStore from 'redux-mock-store';
+import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper';
+import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers';
+import { TokenDetailsSection } from './token-details-section';
+
+jest.mock(
+ '../../../../../../components/app/alert-system/contexts/alertMetricsContext',
+ () => ({
+ useAlertMetrics: jest.fn(() => ({
+ trackAlertMetrics: jest.fn(),
+ })),
+ }),
+);
+
+describe('TokenDetailsSection', () => {
+ it('renders correctly', () => {
+ const state = getMockTokenTransferConfirmState({});
+ const mockStore = configureMockStore([])(state);
+ const { container } = renderWithConfirmContextProvider(
+ ,
+ mockStore,
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx
new file mode 100644
index 000000000000..48a5f2dad74c
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx
@@ -0,0 +1,76 @@
+import { TransactionMeta } from '@metamask/transaction-controller';
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../shared/constants/network';
+import {
+ ConfirmInfoRow,
+ ConfirmInfoRowAddress,
+} from '../../../../../../components/app/confirm/info/row';
+import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section';
+import {
+ AvatarNetwork,
+ AvatarNetworkSize,
+ Box,
+ Text,
+} from '../../../../../../components/component-library';
+import {
+ AlignItems,
+ BlockSize,
+ BorderColor,
+ Display,
+ FlexWrap,
+ TextColor,
+ TextVariant,
+} from '../../../../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../../../../hooks/useI18nContext';
+import { getNetworkConfigurationsByChainId } from '../../../../../../selectors';
+import { useConfirmContext } from '../../../../context/confirm';
+
+export const TokenDetailsSection = () => {
+ const t = useI18nContext();
+ const { currentConfirmation: transactionMeta } =
+ useConfirmContext();
+
+ const { chainId } = transactionMeta;
+ const networkConfigurations = useSelector(getNetworkConfigurationsByChainId);
+ const networkName = networkConfigurations[chainId].name;
+
+ const networkRow = (
+
+
+
+
+ {networkName}
+
+
+
+ );
+
+ const tokenRow = (
+
+
+
+ );
+
+ return (
+
+ {networkRow}
+ {tokenRow}
+
+ );
+};
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx
index 384a8f161e9b..1cb5f3b40ab2 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx
@@ -1,6 +1,13 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper';
+import { Box } from '../../../../../../components/component-library';
+import {
+ AlignItems,
+ Display,
+ FlexDirection,
+ JustifyContent,
+} from '../../../../../../helpers/constants/design-system';
import configureStore from '../../../../../../store/store';
import { ConfirmContextProvider } from '../../../../context/confirm';
import TokenTransferInfo from './token-transfer';
@@ -13,7 +20,16 @@ const Story = {
decorators: [
(story: () => any) => (
- {story()}
+
+
+ {story()}
+
+
),
],
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx
index 186505ee7740..01efc5db0005 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx
@@ -13,6 +13,14 @@ jest.mock(
}),
);
+jest.mock('../../../../../../store/actions', () => ({
+ ...jest.requireActual('../../../../../../store/actions'),
+ getGasFeeTimeEstimate: jest.fn().mockResolvedValue({
+ lowerTimeBound: 0,
+ upperTimeBound: 60000,
+ }),
+}));
+
describe('TokenTransferInfo', () => {
it('renders correctly', () => {
const state = getMockTokenTransferConfirmState({});
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx
index 6fe5ecf166b2..9c0dfe81f536 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx
@@ -1,8 +1,20 @@
import React from 'react';
+import { AdvancedDetails } from '../shared/advanced-details/advanced-details';
+import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section';
import SendHeading from '../shared/send-heading/send-heading';
+import { TokenDetailsSection } from './token-details-section';
+import { TransactionFlowSection } from './transaction-flow-section';
const TokenTransferInfo = () => {
- return ;
+ return (
+ <>
+
+
+
+
+
+ >
+ );
};
export default TokenTransferInfo;
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx
new file mode 100644
index 000000000000..c23d3645abd3
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx
@@ -0,0 +1,48 @@
+import { TransactionType } from '@metamask/transaction-controller';
+import React from 'react';
+import configureMockStore from 'redux-mock-store';
+import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper';
+import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers';
+import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData';
+import { TransactionFlowSection } from './transaction-flow-section';
+
+jest.mock('../hooks/useDecodedTransactionData', () => ({
+ ...jest.requireActual('../hooks/useDecodedTransactionData'),
+ useDecodedTransactionData: jest.fn(),
+}));
+
+describe('', () => {
+ const useDecodedTransactionDataMock = jest.fn().mockImplementation(() => ({
+ pending: false,
+ value: {
+ data: [
+ {
+ name: TransactionType.tokenMethodTransfer,
+ params: [
+ {
+ name: 'dst',
+ type: 'address',
+ value: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ },
+ { name: 'wad', type: 'uint256', value: 0 },
+ ],
+ },
+ ],
+ source: 'Sourcify',
+ },
+ }));
+
+ (useDecodedTransactionData as jest.Mock).mockImplementation(
+ useDecodedTransactionDataMock,
+ );
+
+ it('renders correctly', () => {
+ const state = getMockTokenTransferConfirmState({});
+ const mockStore = configureMockStore([])(state);
+ const { container } = renderWithConfirmContextProvider(
+ ,
+ mockStore,
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
new file mode 100644
index 000000000000..de0e928c10f8
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
@@ -0,0 +1,61 @@
+import { NameType } from '@metamask/name-controller';
+import { TransactionMeta } from '@metamask/transaction-controller';
+import React from 'react';
+import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section';
+import Name from '../../../../../../components/app/name';
+import {
+ Box,
+ Icon,
+ IconName,
+ IconSize,
+} from '../../../../../../components/component-library';
+import {
+ AlignItems,
+ Display,
+ FlexDirection,
+ IconColor,
+ JustifyContent,
+} from '../../../../../../helpers/constants/design-system';
+import { useConfirmContext } from '../../../../context/confirm';
+import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData';
+import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader';
+
+export const TransactionFlowSection = () => {
+ const { currentConfirmation: transactionMeta } =
+ useConfirmContext();
+
+ const { value, pending } = useDecodedTransactionData();
+
+ const recipientAddress = value?.data[0].params.find(
+ (param) => param.type === 'address',
+ )?.value;
+
+ if (pending) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {recipientAddress && (
+
+ )}
+
+
+ );
+};
diff --git a/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts b/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts
index f84ba991f071..2f2af8ddf0de 100644
--- a/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts
+++ b/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts
@@ -20,7 +20,7 @@ const useConfirmationOriginAlerts = (): Alert[] => {
: (currentConfirmation as TransactionMeta)?.origin;
const originUndefinedOrValid =
- origin === undefined || isValidASCIIURL(origin);
+ origin === undefined || origin === 'metamask' || isValidASCIIURL(origin);
return useMemo((): Alert[] => {
if (originUndefinedOrValid) {
From 93fbaaf3604c1455339a1f59d794f905ff76d163 Mon Sep 17 00:00:00 2001
From: Derek Brans
Date: Thu, 17 Oct 2024 09:04:31 -0400
Subject: [PATCH 26/51] feat(TXL-435): turn smart transactions on by default
for new users (#27885)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR:
* enables smart transactions by default for new users
* prevents the smart transaction opt-in modal from appearing
To enable stx by default, we needed to replace the
`getSmartTransactionsOptInStatus` selector with two distinct selectors:
- `getSmartTransactionsOptInStatusForMetrics`
- `getSmartTransactionsPreferenceEnabled`
Previously, the `getSmartTransactionsOptInStatus` selector was doing
double duty for the user's opt-in status and deciding whether the
preference was enabled. Since the feature was disabled by default, and a
user that has not opted-in or out of smart transactions has an opt-in
status of null, this did not pose a problem.
However, since we decided to enable smart transactions by default, we
needed a separate selector for checking the preference for deciding to
enable stx.
### Why not keep the `getSmartTransactionsOptInStatus` selector as-is
and just add a new one?
We considered keeping the `getSmartTransactionsOptInStatus` selector and
adding a new one. However, by renaming it to
`getSmartTransactionsOptInStatusForMetrics`, we avoid any confusion and
make it clear that it is used for metrics collection and not for user
preference handling.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27885?quickstart=1)
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/TXL-435
## **Manual testing steps**
1. add an account with funds on mainnet
2. opt-in modal does not display
3. transfer 0ETH to yourself uses smart transaction
4. preferences toggle starts enabled
5. set preferences toggle off
6. transfer 0ETH to yourself uses regular transaction
## **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/metamask-controller.js | 4 +-
package.json | 1 +
shared/modules/selectors/index.test.ts | 466 +++++++++++-------
.../modules/selectors/smart-transactions.ts | 70 ++-
ui/ducks/swaps/swaps.js | 19 +-
.../confirm-transaction-base.component.js | 9 +-
.../confirm-transaction-base.container.js | 7 +-
ui/pages/home/home.container.js | 7 +-
ui/pages/routes/routes.container.js | 2 -
.../advanced-tab/advanced-tab.component.js | 10 +-
.../advanced-tab.component.test.js | 6 +-
.../advanced-tab/advanced-tab.container.js | 10 +-
.../smart-transactions-opt-in-modal.test.tsx | 14 +-
.../smart-transactions-opt-in-modal.tsx | 6 +-
.../awaiting-signatures.js | 4 +-
ui/pages/swaps/awaiting-swap/awaiting-swap.js | 4 +-
ui/pages/swaps/index.js | 4 +-
.../loading-swaps-quotes.js | 4 +-
.../prepare-swap-page/prepare-swap-page.js | 8 +-
.../swaps/prepare-swap-page/review-quote.js | 19 +-
.../smart-transaction-status.js | 7 +-
ui/store/actions.ts | 9 +-
yarn.lock | 1 +
23 files changed, 426 insertions(+), 265 deletions(-)
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 176c7aea10e5..142ae80d09c1 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -221,9 +221,9 @@ import {
getIsSmartTransaction,
isHardwareWallet,
getFeatureFlagsByChainId,
- getSmartTransactionsOptInStatus,
getCurrentChainSupportsSmartTransactions,
getHardwareWalletType,
+ getSmartTransactionsPreferenceEnabled,
} from '../../shared/modules/selectors';
import { createCaipStream } from '../../shared/modules/caip-stream';
import { BaseUrl } from '../../shared/constants/urls';
@@ -1904,7 +1904,7 @@ export default class MetamaskController extends EventEmitter {
isResubmitEnabled: () => {
const state = this._getMetaMaskState();
return !(
- getSmartTransactionsOptInStatus(state) &&
+ getSmartTransactionsPreferenceEnabled(state) &&
getCurrentChainSupportsSmartTransactions(state)
);
},
diff --git a/package.json b/package.json
index fca1780d3300..5709ea91a51c 100644
--- a/package.json
+++ b/package.json
@@ -460,6 +460,7 @@
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.2",
"@babel/register": "^7.22.15",
+ "@jest/globals": "^29.7.0",
"@lavamoat/allow-scripts": "^3.0.4",
"@lavamoat/lavadome-core": "0.0.10",
"@lavamoat/lavapack": "^6.1.0",
diff --git a/shared/modules/selectors/index.test.ts b/shared/modules/selectors/index.test.ts
index f1dc4fee5ec2..9f0b1b201a5c 100644
--- a/shared/modules/selectors/index.test.ts
+++ b/shared/modules/selectors/index.test.ts
@@ -1,12 +1,16 @@
+// Mocha type definitions are conflicting with Jest
+import { it as jestIt } from '@jest/globals';
+
import { createSwapsMockStore } from '../../../test/jest';
import { CHAIN_IDS } from '../../constants/network';
import { mockNetworkState } from '../../../test/stub/networks';
import {
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
getCurrentChainSupportsSmartTransactions,
getSmartTransactionsEnabled,
getIsSmartTransaction,
getIsSmartTransactionsOptInModalAvailable,
+ getSmartTransactionsPreferenceEnabled,
} from '.';
describe('Selectors', () => {
@@ -65,115 +69,190 @@ describe('Selectors', () => {
};
};
- describe('getSmartTransactionsOptInStatus', () => {
- it('should return the smart transactions opt-in status', () => {
- const state = createMockState();
- const result = getSmartTransactionsOptInStatus(state);
- expect(result).toBe(true);
- });
- });
+ describe('getSmartTransactionsOptInStatusForMetrics and getSmartTransactionsPreferenceEnabled', () => {
+ const createMockOptInStatusState = (status: boolean | null) => {
+ return {
+ metamask: {
+ preferences: {
+ smartTransactionsOptInStatus: status,
+ },
+ },
+ };
+ };
+ describe('getSmartTransactionsOptInStatusForMetrics', () => {
+ jestIt('should return the smart transactions opt-in status', () => {
+ const state = createMockState();
+ const result = getSmartTransactionsOptInStatusForMetrics(state);
+ expect(result).toBe(true);
+ });
- describe('getCurrentChainSupportsSmartTransactions', () => {
- it('should return true if the chain ID is allowed for smart transactions', () => {
- const state = createMockState();
- const result = getCurrentChainSupportsSmartTransactions(state);
- expect(result).toBe(true);
+ jestIt.each([
+ { status: true, expected: true },
+ { status: false, expected: false },
+ { status: null, expected: null },
+ ])(
+ 'should return $expected if the smart transactions opt-in status is $status',
+ ({ status, expected }) => {
+ const state = createMockOptInStatusState(status);
+ const result = getSmartTransactionsOptInStatusForMetrics(state);
+ expect(result).toBe(expected);
+ },
+ );
});
- it('should return false if the chain ID is not allowed for smart transactions', () => {
- const state = createMockState();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
+ describe('getSmartTransactionsPreferenceEnabled', () => {
+ jestIt(
+ 'should return the smart transactions preference enabled status',
+ () => {
+ const state = createMockState();
+ const result = getSmartTransactionsPreferenceEnabled(state);
+ expect(result).toBe(true);
},
- };
- const result = getCurrentChainSupportsSmartTransactions(newState);
- expect(result).toBe(false);
+ );
+
+ jestIt.each([
+ { status: true, expected: true },
+ { status: false, expected: false },
+ { status: null, expected: true },
+ ])(
+ 'should return $expected if the smart transactions opt-in status is $status',
+ ({ status, expected }) => {
+ const state = createMockOptInStatusState(status);
+ const result = getSmartTransactionsPreferenceEnabled(state);
+ expect(result).toBe(expected);
+ },
+ );
});
});
+ describe('getCurrentChainSupportsSmartTransactions', () => {
+ jestIt(
+ 'should return true if the chain ID is allowed for smart transactions',
+ () => {
+ const state = createMockState();
+ const result = getCurrentChainSupportsSmartTransactions(state);
+ expect(result).toBe(true);
+ },
+ );
+
+ jestIt(
+ 'should return false if the chain ID is not allowed for smart transactions',
+ () => {
+ const state = createMockState();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
+ },
+ };
+ const result = getCurrentChainSupportsSmartTransactions(newState);
+ expect(result).toBe(false);
+ },
+ );
+ });
+
describe('getSmartTransactionsEnabled', () => {
- it('returns true if feature flag is enabled, not a HW and is Ethereum network', () => {
- const state = createSwapsMockStore();
- expect(getSmartTransactionsEnabled(state)).toBe(true);
- });
+ jestIt(
+ 'returns true if feature flag is enabled, not a HW and is Ethereum network',
+ () => {
+ const state = createSwapsMockStore();
+ expect(getSmartTransactionsEnabled(state)).toBe(true);
+ },
+ );
- it('returns false if feature flag is disabled, not a HW and is Ethereum network', () => {
- const state = createSwapsMockStore();
- state.metamask.swapsState.swapsFeatureFlags.smartTransactions.extensionActive =
- false;
- expect(getSmartTransactionsEnabled(state)).toBe(false);
- });
+ jestIt(
+ 'returns false if feature flag is disabled, not a HW and is Ethereum network',
+ () => {
+ const state = createSwapsMockStore();
+ state.metamask.swapsState.swapsFeatureFlags.smartTransactions.extensionActive =
+ false;
+ expect(getSmartTransactionsEnabled(state)).toBe(false);
+ },
+ );
- it('returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network', () => {
- const state = createSwapsMockStore();
- state.metamask.smartTransactionsState.liveness = false;
- expect(getSmartTransactionsEnabled(state)).toBe(false);
- });
+ jestIt(
+ 'returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network',
+ () => {
+ const state = createSwapsMockStore();
+ state.metamask.smartTransactionsState.liveness = false;
+ expect(getSmartTransactionsEnabled(state)).toBe(false);
+ },
+ );
- it('returns true if feature flag is enabled, is a HW and is Ethereum network', () => {
- const state = createSwapsMockStore();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- internalAccounts: {
- ...state.metamask.internalAccounts,
- selectedAccount: 'account2',
- accounts: {
- account2: {
- metadata: {
- keyring: {
- type: 'Trezor Hardware',
+ jestIt(
+ 'returns true if feature flag is enabled, is a HW and is Ethereum network',
+ () => {
+ const state = createSwapsMockStore();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ internalAccounts: {
+ ...state.metamask.internalAccounts,
+ selectedAccount: 'account2',
+ accounts: {
+ account2: {
+ metadata: {
+ keyring: {
+ type: 'Trezor Hardware',
+ },
},
},
},
},
},
- },
- };
- expect(getSmartTransactionsEnabled(newState)).toBe(true);
- });
+ };
+ expect(getSmartTransactionsEnabled(newState)).toBe(true);
+ },
+ );
- it('returns false if feature flag is enabled, not a HW and is Polygon network', () => {
- const state = createSwapsMockStore();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
- },
- };
- expect(getSmartTransactionsEnabled(newState)).toBe(false);
- });
+ jestIt(
+ 'returns false if feature flag is enabled, not a HW and is Polygon network',
+ () => {
+ const state = createSwapsMockStore();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
+ },
+ };
+ expect(getSmartTransactionsEnabled(newState)).toBe(false);
+ },
+ );
- it('returns false if feature flag is enabled, not a HW and is BSC network', () => {
- const state = createSwapsMockStore();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- ...mockNetworkState({ chainId: CHAIN_IDS.BSC }),
- },
- };
- expect(getSmartTransactionsEnabled(newState)).toBe(false);
- });
+ jestIt(
+ 'returns false if feature flag is enabled, not a HW and is BSC network',
+ () => {
+ const state = createSwapsMockStore();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ ...mockNetworkState({ chainId: CHAIN_IDS.BSC }),
+ },
+ };
+ expect(getSmartTransactionsEnabled(newState)).toBe(false);
+ },
+ );
- it('returns false if feature flag is enabled, not a HW and is Linea network', () => {
- const state = createSwapsMockStore();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }),
- },
- };
- expect(getSmartTransactionsEnabled(newState)).toBe(false);
- });
+ jestIt(
+ 'returns false if feature flag is enabled, not a HW and is Linea network',
+ () => {
+ const state = createSwapsMockStore();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }),
+ },
+ };
+ expect(getSmartTransactionsEnabled(newState)).toBe(false);
+ },
+ );
- it('returns false if a snap account is used', () => {
+ jestIt('returns false if a snap account is used', () => {
const state = createSwapsMockStore();
state.metamask.internalAccounts.selectedAccount =
'36eb02e0-7925-47f0-859f-076608f09b69';
@@ -182,13 +261,16 @@ describe('Selectors', () => {
});
describe('getIsSmartTransaction', () => {
- it('should return true if smart transactions are opt-in and enabled', () => {
- const state = createMockState();
- const result = getIsSmartTransaction(state);
- expect(result).toBe(true);
- });
+ jestIt(
+ 'should return true if smart transactions are opt-in and enabled',
+ () => {
+ const state = createMockState();
+ const result = getIsSmartTransaction(state);
+ expect(result).toBe(true);
+ },
+ );
- it('should return false if smart transactions are not opt-in', () => {
+ jestIt('should return false if smart transactions are not opt-in', () => {
const state = createMockState();
const newState = {
...state,
@@ -204,7 +286,7 @@ describe('Selectors', () => {
expect(result).toBe(false);
});
- it('should return false if smart transactions are not enabled', () => {
+ jestIt('should return false if smart transactions are not enabled', () => {
const state = createMockState();
const newState = {
...state,
@@ -236,103 +318,121 @@ describe('Selectors', () => {
});
describe('getIsSmartTransactionsOptInModalAvailable', () => {
- it('returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance', () => {
- const state = createMockState();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- preferences: {
- ...state.metamask.preferences,
- smartTransactionsOptInStatus: null,
+ jestIt(
+ 'returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance',
+ () => {
+ const state = createMockState();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ preferences: {
+ ...state.metamask.preferences,
+ smartTransactionsOptInStatus: null,
+ },
},
- },
- };
- expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true);
- });
+ };
+ expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true);
+ },
+ );
- it('returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance', () => {
- const state = createMockState();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- preferences: {
- ...state.metamask.preferences,
- smartTransactionsOptInStatus: null,
+ jestIt(
+ 'returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance',
+ () => {
+ const state = createMockState();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ preferences: {
+ ...state.metamask.preferences,
+ smartTransactionsOptInStatus: null,
+ },
+ ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
},
- ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
- },
- };
- expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
- });
+ };
+ expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
+ },
+ );
- it('returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance', () => {
- const state = createMockState();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- preferences: {
- ...state.metamask.preferences,
- smartTransactionsOptInStatus: null,
+ jestIt(
+ 'returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance',
+ () => {
+ const state = createMockState();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ preferences: {
+ ...state.metamask.preferences,
+ smartTransactionsOptInStatus: null,
+ },
+ ...mockNetworkState({
+ chainId: CHAIN_IDS.MAINNET,
+ rpcUrl: 'https://mainnet.quiknode.pro/',
+ }),
},
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- rpcUrl: 'https://mainnet.quiknode.pro/',
- }),
- },
- };
- expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
- });
+ };
+ expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
+ },
+ );
- it('returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance', () => {
- const state = createMockState();
- expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false);
- });
+ jestIt(
+ 'returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance',
+ () => {
+ const state = createMockState();
+ expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false);
+ },
+ );
- it('returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)', () => {
- const state = createMockState();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- preferences: {
- ...state.metamask.preferences,
- smartTransactionsOptInStatus: null,
- },
- accounts: {
- ...state.metamask.accounts,
- '0x123': {
- address: '0x123',
- balance: '0x0',
+ jestIt(
+ 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)',
+ () => {
+ const state = createMockState();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ preferences: {
+ ...state.metamask.preferences,
+ smartTransactionsOptInStatus: null,
+ },
+ accounts: {
+ ...state.metamask.accounts,
+ '0x123': {
+ address: '0x123',
+ balance: '0x0',
+ },
},
},
- },
- };
- expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
- });
+ };
+ expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
+ },
+ );
- it('returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)', () => {
- const state = createMockState();
- const newState = {
- ...state,
- metamask: {
- ...state.metamask,
- preferences: {
- ...state.metamask.preferences,
- smartTransactionsOptInStatus: null,
- },
- accounts: {
- ...state.metamask.accounts,
- '0x123': {
- address: '0x123',
- balance: '0x00',
+ jestIt(
+ 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)',
+ () => {
+ const state = createMockState();
+ const newState = {
+ ...state,
+ metamask: {
+ ...state.metamask,
+ preferences: {
+ ...state.metamask.preferences,
+ smartTransactionsOptInStatus: null,
+ },
+ accounts: {
+ ...state.metamask.accounts,
+ '0x123': {
+ address: '0x123',
+ balance: '0x00',
+ },
},
},
- },
- };
- expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
- });
+ };
+ expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false);
+ },
+ );
});
});
diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts
index 1c3147632381..a02fe63692b3 100644
--- a/shared/modules/selectors/smart-transactions.ts
+++ b/shared/modules/selectors/smart-transactions.ts
@@ -1,3 +1,4 @@
+import { createSelector } from 'reselect';
import {
getAllowedSmartTransactionsChainIds,
SKIP_STX_RPC_URL_CHECK_CHAIN_IDS,
@@ -7,6 +8,7 @@ import {
getCurrentNetwork,
accountSupportsSmartTx,
getSelectedAccount,
+ getPreferences,
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
} from '../../../ui/selectors/selectors'; // TODO: Migrate shared selectors to this file.
@@ -56,11 +58,60 @@ type SmartTransactionsMetaMaskState = {
};
};
-export const getSmartTransactionsOptInStatus = (
- state: SmartTransactionsMetaMaskState,
-): boolean | null => {
- return state.metamask.preferences?.smartTransactionsOptInStatus ?? null;
-};
+/**
+ * Returns the user's explicit opt-in status for the smart transactions feature.
+ * This should only be used for reading the user's internal opt-in status, and
+ * not for determining if the smart transactions user preference is enabled.
+ *
+ * To determine if the smart transactions user preference is enabled, use
+ * getSmartTransactionsPreferenceEnabled instead.
+ *
+ * @param state - The state object.
+ * @returns true if the user has explicitly opted in, false if they have opted out,
+ * or null if they have not explicitly opted in or out.
+ */
+export const getSmartTransactionsOptInStatusInternal = createSelector(
+ getPreferences,
+ (preferences: {
+ smartTransactionsOptInStatus?: boolean | null;
+ }): boolean | null => {
+ return preferences?.smartTransactionsOptInStatus ?? null;
+ },
+);
+
+/**
+ * Returns the user's explicit opt-in status for the smart transactions feature.
+ * This should only be used for metrics collection, and not for determining if the
+ * smart transactions user preference is enabled.
+ *
+ * To determine if the smart transactions user preference is enabled, use
+ * getSmartTransactionsPreferenceEnabled instead.
+ *
+ * @param state - The state object.
+ * @returns true if the user has explicitly opted in, false if they have opted out,
+ * or null if they have not explicitly opted in or out.
+ */
+export const getSmartTransactionsOptInStatusForMetrics = createSelector(
+ getSmartTransactionsOptInStatusInternal,
+ (optInStatus: boolean | null): boolean | null => optInStatus,
+);
+
+/**
+ * Returns the user's preference for the smart transactions feature.
+ * Defaults to `true` if the user has not set a preference.
+ *
+ * @param state
+ * @returns
+ */
+export const getSmartTransactionsPreferenceEnabled = createSelector(
+ getSmartTransactionsOptInStatusInternal,
+ (optInStatus: boolean | null): boolean => {
+ // In the absence of an explicit opt-in or opt-out,
+ // the Smart Transactions toggle is enabled.
+ const DEFAULT_SMART_TRANSACTIONS_ENABLED = true;
+ return optInStatus ?? DEFAULT_SMART_TRANSACTIONS_ENABLED;
+ },
+);
export const getCurrentChainSupportsSmartTransactions = (
state: SmartTransactionsMetaMaskState,
@@ -105,7 +156,7 @@ export const getIsSmartTransactionsOptInModalAvailable = (
return (
getCurrentChainSupportsSmartTransactions(state) &&
getIsAllowedRpcUrlForSmartTransactions(state) &&
- getSmartTransactionsOptInStatus(state) === null &&
+ getSmartTransactionsOptInStatusInternal(state) === null &&
hasNonZeroBalance(state)
);
};
@@ -132,7 +183,10 @@ export const getSmartTransactionsEnabled = (
export const getIsSmartTransaction = (
state: SmartTransactionsMetaMaskState,
): boolean => {
- const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state);
+ const smartTransactionsPreferenceEnabled =
+ getSmartTransactionsPreferenceEnabled(state);
const smartTransactionsEnabled = getSmartTransactionsEnabled(state);
- return Boolean(smartTransactionsOptInStatus && smartTransactionsEnabled);
+ return Boolean(
+ smartTransactionsPreferenceEnabled && smartTransactionsEnabled,
+ );
};
diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js
index 91ed081eb719..8dd7336d7a62 100644
--- a/ui/ducks/swaps/swaps.js
+++ b/ui/ducks/swaps/swaps.js
@@ -69,8 +69,9 @@ import {
getSelectedInternalAccount,
} from '../../selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
+ getSmartTransactionsPreferenceEnabled,
} from '../../../shared/modules/selectors';
import {
MetaMetricsEventCategory,
@@ -746,7 +747,6 @@ export const fetchQuotesAndSetQuoteState = (
const hardwareWalletType = getHardwareWalletType(state);
const networkAndAccountSupports1559 =
checkNetworkAndAccountSupports1559(state);
- const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state);
const smartTransactionsEnabled = getSmartTransactionsEnabled(state);
const currentSmartTransactionsEnabled =
getCurrentSmartTransactionsEnabled(state);
@@ -764,7 +764,7 @@ export const fetchQuotesAndSetQuoteState = (
hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
- stx_user_opt_in: smartTransactionsOptInStatus,
+ stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state),
anonymizedData: true,
},
});
@@ -784,7 +784,8 @@ export const fetchQuotesAndSetQuoteState = (
balanceError,
sourceDecimals: fromTokenDecimals,
enableGasIncludedQuotes:
- currentSmartTransactionsEnabled && smartTransactionsOptInStatus,
+ currentSmartTransactionsEnabled &&
+ getSmartTransactionsPreferenceEnabled(state),
},
{
sourceTokenInfo,
@@ -819,7 +820,7 @@ export const fetchQuotesAndSetQuoteState = (
hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
- stx_user_opt_in: smartTransactionsOptInStatus,
+ stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state),
},
});
dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR));
@@ -856,7 +857,7 @@ export const fetchQuotesAndSetQuoteState = (
hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
- stx_user_opt_in: smartTransactionsOptInStatus,
+ stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state),
anonymizedData: true,
},
});
@@ -910,7 +911,6 @@ export const signAndSendSwapsSmartTransaction = ({
usedQuote.destinationAmount,
destinationTokenInfo.decimals || 18,
).toPrecision(8);
- const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state);
const smartTransactionsEnabled = getSmartTransactionsEnabled(state);
const currentSmartTransactionsEnabled =
getCurrentSmartTransactionsEnabled(state);
@@ -937,7 +937,7 @@ export const signAndSendSwapsSmartTransaction = ({
hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
- stx_user_opt_in: smartTransactionsOptInStatus,
+ stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state),
gas_included: usedQuote.isGasIncludedTrade,
...additionalTrackingParams,
};
@@ -1180,7 +1180,6 @@ export const signAndSendTransactions = (
numberOfDecimals: 6,
});
- const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state);
const smartTransactionsEnabled = getSmartTransactionsEnabled(state);
const currentSmartTransactionsEnabled =
getCurrentSmartTransactionsEnabled(state);
@@ -1212,7 +1211,7 @@ export const signAndSendTransactions = (
hardware_wallet_type: getHardwareWalletType(state),
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
- stx_user_opt_in: smartTransactionsOptInStatus,
+ stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state),
...additionalTrackingParams,
};
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 b4d2d6a8def5..1ae7eaaeb33d 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
@@ -178,7 +178,7 @@ export default class ConfirmTransactionBase extends Component {
isUserOpContractDeployError: PropTypes.bool,
useMaxValue: PropTypes.bool,
maxValue: PropTypes.string,
- smartTransactionsOptInStatus: PropTypes.bool,
+ smartTransactionsPreferenceEnabled: PropTypes.bool,
currentChainSupportsSmartTransactions: PropTypes.bool,
selectedNetworkClientId: PropTypes.string,
isSmartTransactionsEnabled: PropTypes.bool,
@@ -1019,7 +1019,7 @@ export default class ConfirmTransactionBase extends Component {
txData: { origin, chainId: txChainId } = {},
getNextNonce,
tryReverseResolveAddress,
- smartTransactionsOptInStatus,
+ smartTransactionsPreferenceEnabled,
currentChainSupportsSmartTransactions,
setSwapsFeatureFlags,
fetchSmartTransactionsLiveness,
@@ -1071,7 +1071,10 @@ export default class ConfirmTransactionBase extends Component {
window.addEventListener('beforeunload', this._beforeUnloadForGasPolling);
- if (smartTransactionsOptInStatus && currentChainSupportsSmartTransactions) {
+ if (
+ smartTransactionsPreferenceEnabled &&
+ currentChainSupportsSmartTransactions
+ ) {
// TODO: Fetching swaps feature flags, which include feature flags for smart transactions, is only a short-term solution.
// Long-term, we want to have a new proxy service specifically for feature flags.
Promise.all([
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 5d92a1af9c56..e06090f48e75 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
@@ -59,10 +59,10 @@ import {
} from '../../../selectors';
import {
getCurrentChainSupportsSmartTransactions,
- getSmartTransactionsOptInStatus,
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
getSmartTransactionsEnabled,
///: END:ONLY_INCLUDE_IF
+ getSmartTransactionsPreferenceEnabled,
} from '../../../../shared/modules/selectors';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import {
@@ -185,7 +185,8 @@ const mapStateToProps = (state, ownProps) => {
data,
} = (transaction && transaction.txParams) || txParams;
const accounts = getMetaMaskAccounts(state);
- const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state);
+ const smartTransactionsPreferenceEnabled =
+ getSmartTransactionsPreferenceEnabled(state);
const currentChainSupportsSmartTransactions =
getCurrentChainSupportsSmartTransactions(state);
@@ -364,7 +365,7 @@ const mapStateToProps = (state, ownProps) => {
isUserOpContractDeployError,
useMaxValue,
maxValue,
- smartTransactionsOptInStatus,
+ smartTransactionsPreferenceEnabled,
currentChainSupportsSmartTransactions,
hasPriorityApprovalRequest,
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js
index 42bdfc685779..dfeb1a5e7cdb 100644
--- a/ui/pages/home/home.container.js
+++ b/ui/pages/home/home.container.js
@@ -51,7 +51,6 @@ import {
getAccountType,
///: END:ONLY_INCLUDE_IF
} from '../../selectors';
-import { getIsSmartTransactionsOptInModalAvailable } from '../../../shared/modules/selectors';
import {
closeNotificationPopup,
@@ -223,8 +222,10 @@ const mapStateToProps = (state) => {
custodianDeepLink: getCustodianDeepLink(state),
accountType: getAccountType(state),
///: END:ONLY_INCLUDE_IF
- isSmartTransactionsOptInModalAvailable:
- getIsSmartTransactionsOptInModalAvailable(state),
+
+ // Set to false to prevent the opt-in modal from showing.
+ // TODO(dbrans): Remove opt-in modal once default opt-in is stable.
+ isSmartTransactionsOptInModalAvailable: false,
showMultiRpcModal: state.metamask.preferences.showMultiRpcModal,
};
};
diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js
index 419daf561778..2c26f0daa0b5 100644
--- a/ui/pages/routes/routes.container.js
+++ b/ui/pages/routes/routes.container.js
@@ -30,7 +30,6 @@ import {
getNftDetectionEnablementToast,
getCurrentNetwork,
} from '../../selectors';
-import { getSmartTransactionsOptInStatus } from '../../../shared/modules/selectors';
import {
lockMetamask,
hideImportNftsModal,
@@ -118,7 +117,6 @@ function mapStateToProps(state) {
allAccountsOnNetworkAreEmpty: getAllAccountsOnNetworkAreEmpty(state),
isTestNet: getIsTestnet(state),
showExtensionInFullSizeView: getShowExtensionInFullSizeView(state),
- smartTransactionsOptInStatus: getSmartTransactionsOptInStatus(state),
currentChainId: getCurrentChainId(state),
shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state),
forgottenPassword: state.metamask.forgottenPassword,
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js
index 5c7f4a659a6d..50aea4e0dc60 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.component.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js
@@ -46,12 +46,12 @@ export default class AdvancedTab extends PureComponent {
sendHexData: PropTypes.bool,
showFiatInTestnets: PropTypes.bool,
showTestNetworks: PropTypes.bool,
- smartTransactionsOptInStatus: PropTypes.bool,
+ smartTransactionsEnabled: PropTypes.bool,
autoLockTimeLimit: PropTypes.number,
setAutoLockTimeLimit: PropTypes.func.isRequired,
setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
setShowTestNetworks: PropTypes.func.isRequired,
- setSmartTransactionsOptInStatus: PropTypes.func.isRequired,
+ setSmartTransactionsEnabled: PropTypes.func.isRequired,
setDismissSeedBackUpReminder: PropTypes.func.isRequired,
dismissSeedBackUpReminder: PropTypes.bool.isRequired,
backupUserData: PropTypes.func.isRequired,
@@ -199,7 +199,7 @@ export default class AdvancedTab extends PureComponent {
renderToggleStxOptIn() {
const { t } = this.context;
- const { smartTransactionsOptInStatus, setSmartTransactionsOptInStatus } =
+ const { smartTransactionsEnabled, setSmartTransactionsEnabled } =
this.props;
const learMoreLink = (
@@ -237,10 +237,10 @@ export default class AdvancedTab extends PureComponent {
{
const newValue = !oldValue;
- setSmartTransactionsOptInStatus(newValue);
+ setSmartTransactionsEnabled(newValue);
}}
offLabel={t('off')}
onLabel={t('on')}
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js
index 6be53fb4e21a..2c64b79e4f4d 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js
@@ -9,7 +9,7 @@ import AdvancedTab from '.';
const mockSetAutoLockTimeLimit = jest.fn().mockReturnValue({ type: 'TYPE' });
const mockSetShowTestNetworks = jest.fn();
const mockSetShowFiatConversionOnTestnetsPreference = jest.fn();
-const mockSetStxOptIn = jest.fn();
+const mockSetStxPrefEnabled = jest.fn();
jest.mock('../../../store/actions.ts', () => {
return {
@@ -17,7 +17,7 @@ jest.mock('../../../store/actions.ts', () => {
setShowTestNetworks: () => mockSetShowTestNetworks,
setShowFiatConversionOnTestnetsPreference: () =>
mockSetShowFiatConversionOnTestnetsPreference,
- setSmartTransactionsOptInStatus: () => mockSetStxOptIn,
+ setSmartTransactionsPreferenceEnabled: () => mockSetStxPrefEnabled,
};
});
@@ -102,7 +102,7 @@ describe('AdvancedTab Component', () => {
const { queryByTestId } = renderWithProvider(, mockStore);
const toggleButton = queryByTestId('settings-page-stx-opt-in-toggle');
fireEvent.click(toggleButton);
- expect(mockSetStxOptIn).toHaveBeenCalled();
+ expect(mockSetStxPrefEnabled).toHaveBeenCalled();
});
});
});
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js
index 2a11b6751dae..f2ad894d1e8b 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.container.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js
@@ -12,10 +12,11 @@ import {
setShowExtensionInFullSizeView,
setShowFiatConversionOnTestnetsPreference,
setShowTestNetworks,
- setSmartTransactionsOptInStatus,
+ setSmartTransactionsPreferenceEnabled,
setUseNonceField,
showModal,
} from '../../../store/actions';
+import { getSmartTransactionsPreferenceEnabled } from '../../../../shared/modules/selectors';
import AdvancedTab from './advanced-tab.component';
export const mapStateToProps = (state) => {
@@ -32,7 +33,6 @@ export const mapStateToProps = (state) => {
showFiatInTestnets,
showTestNetworks,
showExtensionInFullSizeView,
- smartTransactionsOptInStatus,
autoLockTimeLimit = DEFAULT_AUTO_LOCK_TIME_LIMIT,
} = getPreferences(state);
@@ -42,7 +42,7 @@ export const mapStateToProps = (state) => {
showFiatInTestnets,
showTestNetworks,
showExtensionInFullSizeView,
- smartTransactionsOptInStatus,
+ smartTransactionsEnabled: getSmartTransactionsPreferenceEnabled(state),
autoLockTimeLimit,
useNonceField,
dismissSeedBackUpReminder,
@@ -67,8 +67,8 @@ export const mapDispatchToProps = (dispatch) => {
setShowExtensionInFullSizeView: (value) => {
return dispatch(setShowExtensionInFullSizeView(value));
},
- setSmartTransactionsOptInStatus: (value) => {
- return dispatch(setSmartTransactionsOptInStatus(value));
+ setSmartTransactionsEnabled: (value) => {
+ return dispatch(setSmartTransactionsPreferenceEnabled(value));
},
setAutoLockTimeLimit: (value) => {
return dispatch(setAutoLockTimeLimit(value));
diff --git a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx
index 15546c3aa09d..ab491ea05ea5 100644
--- a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx
+++ b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx
@@ -7,7 +7,7 @@ import {
renderWithProvider,
createSwapsMockStore,
} from '../../../../test/jest';
-import { setSmartTransactionsOptInStatus } from '../../../store/actions';
+import { setSmartTransactionsPreferenceEnabled } from '../../../store/actions';
import SmartTransactionsOptInModal from './smart-transactions-opt-in-modal';
const middleware = [thunk];
@@ -35,8 +35,8 @@ describe('SmartTransactionsOptInModal', () => {
});
it('calls setSmartTransactionsOptInStatus with false when the "No thanks" link is clicked', () => {
- (setSmartTransactionsOptInStatus as jest.Mock).mockImplementationOnce(() =>
- jest.fn(),
+ (setSmartTransactionsPreferenceEnabled as jest.Mock).mockImplementationOnce(
+ () => jest.fn(),
);
const store = configureMockStore(middleware)(createSwapsMockStore());
const { getByText } = renderWithProvider(
@@ -48,12 +48,12 @@ describe('SmartTransactionsOptInModal', () => {
);
const noThanksLink = getByText('No thanks');
fireEvent.click(noThanksLink);
- expect(setSmartTransactionsOptInStatus).toHaveBeenCalledWith(false);
+ expect(setSmartTransactionsPreferenceEnabled).toHaveBeenCalledWith(false);
});
it('calls setSmartTransactionsOptInStatus with true when the "Enable" button is clicked', () => {
- (setSmartTransactionsOptInStatus as jest.Mock).mockImplementationOnce(() =>
- jest.fn(),
+ (setSmartTransactionsPreferenceEnabled as jest.Mock).mockImplementationOnce(
+ () => jest.fn(),
);
const store = configureMockStore(middleware)(createSwapsMockStore());
const { getByText } = renderWithProvider(
@@ -65,6 +65,6 @@ describe('SmartTransactionsOptInModal', () => {
);
const enableButton = getByText('Enable');
fireEvent.click(enableButton);
- expect(setSmartTransactionsOptInStatus).toHaveBeenCalledWith(true);
+ expect(setSmartTransactionsPreferenceEnabled).toHaveBeenCalledWith(true);
});
});
diff --git a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx
index 78055de42831..2269018e2239 100644
--- a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx
+++ b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx
@@ -28,7 +28,7 @@ import {
Icon,
IconName,
} from '../../../components/component-library';
-import { setSmartTransactionsOptInStatus } from '../../../store/actions';
+import { setSmartTransactionsPreferenceEnabled } from '../../../store/actions';
import { SMART_TRANSACTIONS_LEARN_MORE_URL } from '../../../../shared/constants/smartTransactions';
export type SmartTransactionsOptInModalProps = {
@@ -166,12 +166,12 @@ export default function SmartTransactionsOptInModal({
const dispatch = useDispatch();
const handleEnableButtonClick = useCallback(() => {
- dispatch(setSmartTransactionsOptInStatus(true));
+ dispatch(setSmartTransactionsPreferenceEnabled(true));
}, [dispatch]);
const handleNoThanksLinkClick = useCallback(() => {
// Set the Smart Transactions opt-in status to false, so the opt-in modal is not shown again.
- dispatch(setSmartTransactionsOptInStatus(false));
+ dispatch(setSmartTransactionsPreferenceEnabled(false));
}, [dispatch]);
useEffect(() => {
diff --git a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js
index a1b4179beefb..56db1578ce9a 100644
--- a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js
+++ b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js
@@ -15,8 +15,8 @@ import {
getHardwareWalletType,
} from '../../../selectors/selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
} from '../../../../shared/modules/selectors';
import {
DEFAULT_ROUTE,
@@ -47,7 +47,7 @@ export default function AwaitingSignatures() {
const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType);
const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
);
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js
index b5c2589fbd28..7c410ca03ce5 100644
--- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js
+++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js
@@ -23,8 +23,8 @@ import {
getFullTxData,
} from '../../../selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
} from '../../../../shared/modules/selectors';
import {
@@ -120,7 +120,7 @@ export default function AwaitingSwap({
const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType);
const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
);
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
diff --git a/ui/pages/swaps/index.js b/ui/pages/swaps/index.js
index cf079acc9623..e16166297545 100644
--- a/ui/pages/swaps/index.js
+++ b/ui/pages/swaps/index.js
@@ -46,8 +46,8 @@ import {
} from '../../ducks/swaps/swaps';
import { getCurrentNetworkTransactions } from '../../selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
} from '../../../shared/modules/selectors';
import {
AWAITING_SIGNATURES_ROUTE,
@@ -133,7 +133,7 @@ export default function Swap() {
const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp);
const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp);
const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
);
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
diff --git a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js
index e98d275f8aa8..ebbb6f652496 100644
--- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js
+++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js
@@ -16,8 +16,8 @@ import {
getHardwareWalletType,
} from '../../../selectors/selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
} from '../../../../shared/modules/selectors';
import { I18nContext } from '../../../contexts/i18n';
import { MetaMetricsContext } from '../../../contexts/metametrics';
@@ -51,7 +51,7 @@ export default function LoadingSwapsQuotes({
const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType);
const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
);
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
index 72050df4aca9..8a701289bebd 100644
--- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
+++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
@@ -69,8 +69,9 @@ import {
getDataCollectionForMarketing,
} from '../../../selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsPreferenceEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
} from '../../../../shared/modules/selectors';
import {
getValueFromWeiHex,
@@ -212,14 +213,15 @@ export default function PrepareSwapPage({
const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType);
const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
);
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled,
);
const isSmartTransaction =
- currentSmartTransactionsEnabled && smartTransactionsOptInStatus;
+ useSelector(getSmartTransactionsPreferenceEnabled) &&
+ currentSmartTransactionsEnabled;
const currentCurrency = useSelector(getCurrentCurrency);
const fetchingQuotes = useSelector(getFetchingQuotes);
const loadingComplete = !fetchingQuotes && areQuotesPresent;
diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js
index 4c47437b1bd8..13d11a93cd1f 100644
--- a/ui/pages/swaps/prepare-swap-page/review-quote.js
+++ b/ui/pages/swaps/prepare-swap-page/review-quote.js
@@ -58,8 +58,9 @@ import {
getUSDConversionRate,
} from '../../../selectors';
import {
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
getSmartTransactionsEnabled,
+ getSmartTransactionsPreferenceEnabled,
} from '../../../../shared/modules/selectors';
import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask';
import {
@@ -240,7 +241,10 @@ export default function ReviewQuote({ setReceiveToAmount }) {
const nativeCurrencySymbol = useSelector(getNativeCurrency);
const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp);
const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
+ getSmartTransactionsOptInStatusForMetrics,
+ );
+ const smartTransactionsPreferenceEnabled = useSelector(
+ getSmartTransactionsPreferenceEnabled,
);
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const swapsSTXLoading = useSelector(getSwapsSTXLoading);
@@ -280,7 +284,8 @@ export default function ReviewQuote({ setReceiveToAmount }) {
const unsignedTransaction = usedQuote.trade;
const { isGasIncludedTrade } = usedQuote;
const isSmartTransaction =
- currentSmartTransactionsEnabled && smartTransactionsOptInStatus;
+ useSelector(getSmartTransactionsPreferenceEnabled) &&
+ currentSmartTransactionsEnabled;
const [slippageErrorKey] = useState(() => {
const slippage = Number(fetchParams?.slippage);
@@ -377,7 +382,7 @@ export default function ReviewQuote({ setReceiveToAmount }) {
chainId,
smartTransactionEstimatedGas:
smartTransactionsEnabled &&
- smartTransactionsOptInStatus &&
+ smartTransactionsPreferenceEnabled &&
smartTransactionFees?.tradeTxFees,
nativeCurrencySymbol,
multiLayerL1ApprovalFeeTotal,
@@ -394,7 +399,7 @@ export default function ReviewQuote({ setReceiveToAmount }) {
smartTransactionFees?.tradeTxFees,
nativeCurrencySymbol,
smartTransactionsEnabled,
- smartTransactionsOptInStatus,
+ smartTransactionsPreferenceEnabled,
multiLayerL1ApprovalFeeTotal,
]);
@@ -887,7 +892,7 @@ export default function ReviewQuote({ setReceiveToAmount }) {
(currentSmartTransactionsEnabled &&
(currentSmartTransactionsError || smartTransactionsError)) ||
(currentSmartTransactionsEnabled &&
- smartTransactionsOptInStatus &&
+ smartTransactionsPreferenceEnabled &&
!smartTransactionFees?.tradeTxFees),
);
@@ -1136,7 +1141,7 @@ export default function ReviewQuote({ setReceiveToAmount }) {
initialAggId={usedQuote.aggregator}
onQuoteDetailsIsOpened={trackQuoteDetailsOpened}
hideEstimatedGasFee={
- smartTransactionsEnabled && smartTransactionsOptInStatus
+ smartTransactionsEnabled && smartTransactionsPreferenceEnabled
}
/>
)
diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js
index d6e7f4d653cf..7b8d5910c218 100644
--- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js
+++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js
@@ -20,8 +20,8 @@ import {
getRpcPrefsForCurrentProvider,
} from '../../../selectors';
import {
- getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled,
+ getSmartTransactionsOptInStatusForMetrics,
} from '../../../../shared/modules/selectors';
import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps';
import {
@@ -78,9 +78,6 @@ export default function SmartTransactionStatusPage() {
getCurrentSmartTransactions,
isEqual,
);
- const smartTransactionsOptInStatus = useSelector(
- getSmartTransactionsOptInStatus,
- );
const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual);
const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual);
@@ -128,7 +125,7 @@ export default function SmartTransactionStatusPage() {
hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
- stx_user_opt_in: smartTransactionsOptInStatus,
+ stx_user_opt_in: useSelector(getSmartTransactionsOptInStatusForMetrics),
};
let destinationValue;
diff --git a/ui/store/actions.ts b/ui/store/actions.ts
index 43c7fb189822..3433a798a9d9 100644
--- a/ui/store/actions.ts
+++ b/ui/store/actions.ts
@@ -103,7 +103,7 @@ import {
} from '../../shared/constants/metametrics';
import { parseSmartTransactionsError } from '../pages/swaps/swaps.util';
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
-import { getSmartTransactionsOptInStatus } from '../../shared/modules/selectors';
+import { getSmartTransactionsOptInStatusInternal } from '../../shared/modules/selectors';
import { NOTIFICATIONS_EXPIRATION_DELAY } from '../helpers/constants/notifications';
import {
fetchLocale,
@@ -3090,13 +3090,12 @@ export function setTokenSortConfig(value: SortCriteria) {
return setPreference('tokenSortConfig', value, false);
}
-export function setSmartTransactionsOptInStatus(
+export function setSmartTransactionsPreferenceEnabled(
value: boolean,
): ThunkAction {
return async (dispatch, getState) => {
- const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(
- getState(),
- );
+ const smartTransactionsOptInStatus =
+ getSmartTransactionsOptInStatusInternal(getState());
trackMetaMetricsEvent({
category: MetaMetricsEventCategory.Settings,
event: MetaMetricsEventName.SettingsUpdated,
diff --git a/yarn.lock b/yarn.lock
index 3c34b05fee52..19e1f3bf1a8b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -26086,6 +26086,7 @@ __metadata:
"@ethersproject/providers": "npm:^5.7.2"
"@ethersproject/wallet": "npm:^5.7.0"
"@fortawesome/fontawesome-free": "npm:^5.13.0"
+ "@jest/globals": "npm:^29.7.0"
"@keystonehq/bc-ur-registry-eth": "npm:^0.19.1"
"@keystonehq/metamask-airgapped-keyring": "npm:^0.13.1"
"@lavamoat/allow-scripts": "npm:^3.0.4"
From bf475ee2cee9f63dee60492e39b39e64d2636fe1 Mon Sep 17 00:00:00 2001
From: Kanthesha Devaramane
Date: Thu, 17 Oct 2024 16:28:26 +0100
Subject: [PATCH 27/51] feat: convert AlertController to typescript (#27764)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
As a prerequisite for migrating AlertController to BaseController v2,
and to support the TypeScript migration effort, we want to convert
AlertController to TypeScript.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27764?quickstart=1)
## **Related issues**
Fixes: #25921
## **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.
---
.eslintrc.js | 1 +
.../controllers/alert-controller.test.ts | 258 ++++++++++++++++++
app/scripts/controllers/alert-controller.ts | 203 ++++++++++++++
app/scripts/controllers/alert.js | 136 ---------
app/scripts/metamask-controller.js | 4 +-
.../files-to-convert.json | 1 -
6 files changed, 464 insertions(+), 139 deletions(-)
create mode 100644 app/scripts/controllers/alert-controller.test.ts
create mode 100644 app/scripts/controllers/alert-controller.ts
delete mode 100644 app/scripts/controllers/alert.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 258556239ac3..97d52b6637cc 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -309,6 +309,7 @@ module.exports = {
'**/__snapshots__/*.snap',
'app/scripts/controllers/app-state.test.js',
'app/scripts/controllers/mmi-controller.test.ts',
+ 'app/scripts/controllers/alert-controller.test.ts',
'app/scripts/metamask-controller.actions.test.js',
'app/scripts/detect-multiple-instances.test.js',
'app/scripts/controllers/bridge.test.ts',
diff --git a/app/scripts/controllers/alert-controller.test.ts b/app/scripts/controllers/alert-controller.test.ts
new file mode 100644
index 000000000000..a8aee606e02d
--- /dev/null
+++ b/app/scripts/controllers/alert-controller.test.ts
@@ -0,0 +1,258 @@
+/**
+ * @jest-environment node
+ */
+import { ControllerMessenger } from '@metamask/base-controller';
+import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller';
+import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers';
+import { EthAccountType } from '@metamask/keyring-api';
+import {
+ AlertControllerActions,
+ AlertControllerEvents,
+ AlertController,
+ AllowedActions,
+ AllowedEvents,
+ AlertControllerState,
+} from './alert-controller';
+
+const EMPTY_ACCOUNT = {
+ id: '',
+ address: '',
+ options: {},
+ methods: [],
+ type: EthAccountType.Eoa,
+ metadata: {
+ name: '',
+ keyring: {
+ type: '',
+ },
+ importTime: 0,
+ },
+};
+describe('AlertController', () => {
+ let controllerMessenger: ControllerMessenger<
+ AlertControllerActions | AllowedActions,
+ | AlertControllerEvents
+ | KeyringControllerStateChangeEvent
+ | SnapControllerStateChangeEvent
+ | AllowedEvents
+ >;
+ let alertController: AlertController;
+
+ beforeEach(() => {
+ controllerMessenger = new ControllerMessenger<
+ AllowedActions,
+ AllowedEvents
+ >();
+ controllerMessenger.registerActionHandler(
+ 'AccountsController:getSelectedAccount',
+ () => EMPTY_ACCOUNT,
+ );
+
+ const alertMessenger = controllerMessenger.getRestricted({
+ name: 'AlertController',
+ allowedActions: [`AccountsController:getSelectedAccount`],
+ allowedEvents: [`AccountsController:selectedAccountChange`],
+ });
+
+ alertController = new AlertController({
+ state: {
+ unconnectedAccountAlertShownOrigins: {
+ testUnconnectedOrigin: false,
+ },
+ web3ShimUsageOrigins: {
+ testWeb3ShimUsageOrigin: 0,
+ },
+ },
+ controllerMessenger: alertMessenger,
+ });
+ });
+
+ describe('default state', () => {
+ it('should be same as AlertControllerState initialized', () => {
+ expect(alertController.store.getState()).toStrictEqual({
+ alertEnabledness: {
+ unconnectedAccount: true,
+ web3ShimUsage: true,
+ },
+ unconnectedAccountAlertShownOrigins: {
+ testUnconnectedOrigin: false,
+ },
+ web3ShimUsageOrigins: {
+ testWeb3ShimUsageOrigin: 0,
+ },
+ });
+ });
+ });
+
+ describe('alertEnabledness', () => {
+ it('should default unconnectedAccount of alertEnabledness to true', () => {
+ expect(
+ alertController.store.getState().alertEnabledness.unconnectedAccount,
+ ).toStrictEqual(true);
+ });
+
+ it('should set unconnectedAccount of alertEnabledness to false', () => {
+ alertController.setAlertEnabledness('unconnectedAccount', false);
+ expect(
+ alertController.store.getState().alertEnabledness.unconnectedAccount,
+ ).toStrictEqual(false);
+ expect(
+ controllerMessenger.call('AlertController:getState').alertEnabledness
+ .unconnectedAccount,
+ ).toStrictEqual(false);
+ });
+ });
+
+ describe('unconnectedAccountAlertShownOrigins', () => {
+ it('should default unconnectedAccountAlertShownOrigins', () => {
+ expect(
+ alertController.store.getState().unconnectedAccountAlertShownOrigins,
+ ).toStrictEqual({
+ testUnconnectedOrigin: false,
+ });
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .unconnectedAccountAlertShownOrigins,
+ ).toStrictEqual({
+ testUnconnectedOrigin: false,
+ });
+ });
+
+ it('should set unconnectedAccountAlertShownOrigins', () => {
+ alertController.setUnconnectedAccountAlertShown('testUnconnectedOrigin');
+ expect(
+ alertController.store.getState().unconnectedAccountAlertShownOrigins,
+ ).toStrictEqual({
+ testUnconnectedOrigin: true,
+ });
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .unconnectedAccountAlertShownOrigins,
+ ).toStrictEqual({
+ testUnconnectedOrigin: true,
+ });
+ });
+ });
+
+ describe('web3ShimUsageOrigins', () => {
+ it('should default web3ShimUsageOrigins', () => {
+ expect(
+ alertController.store.getState().web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 0,
+ });
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 0,
+ });
+ });
+
+ it('should set origin of web3ShimUsageOrigins to recorded', () => {
+ alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin');
+ expect(
+ alertController.store.getState().web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 1,
+ });
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 1,
+ });
+ });
+ it('should set origin of web3ShimUsageOrigins to dismissed', () => {
+ alertController.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin');
+ expect(
+ alertController.store.getState().web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 2,
+ });
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 2,
+ });
+ });
+ });
+
+ describe('selectedAccount change', () => {
+ it('should set unconnectedAccountAlertShownOrigins to {}', () => {
+ controllerMessenger.publish('AccountsController:selectedAccountChange', {
+ id: '',
+ address: '0x1234567',
+ options: {},
+ methods: [],
+ type: 'eip155:eoa',
+ metadata: {
+ name: '',
+ keyring: {
+ type: '',
+ },
+ importTime: 0,
+ },
+ });
+ expect(
+ alertController.store.getState().unconnectedAccountAlertShownOrigins,
+ ).toStrictEqual({});
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .unconnectedAccountAlertShownOrigins,
+ ).toStrictEqual({});
+ });
+ });
+
+ describe('AlertController:getState', () => {
+ it('should return the current state of the property', () => {
+ const defaultWeb3ShimUsageOrigins = {
+ testWeb3ShimUsageOrigin: 0,
+ };
+ expect(
+ alertController.store.getState().web3ShimUsageOrigins,
+ ).toStrictEqual(defaultWeb3ShimUsageOrigins);
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .web3ShimUsageOrigins,
+ ).toStrictEqual(defaultWeb3ShimUsageOrigins);
+ });
+ });
+
+ describe('AlertController:stateChange', () => {
+ it('state will be published when there is state change', () => {
+ expect(
+ alertController.store.getState().web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 0,
+ });
+
+ controllerMessenger.subscribe(
+ 'AlertController:stateChange',
+ (state: Partial) => {
+ expect(state.web3ShimUsageOrigins).toStrictEqual({
+ testWeb3ShimUsageOrigin: 1,
+ });
+ },
+ );
+
+ alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin');
+
+ expect(
+ alertController.store.getState().web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 1,
+ });
+ expect(
+ alertController.getWeb3ShimUsageState('testWeb3ShimUsageOrigin'),
+ ).toStrictEqual(1);
+ expect(
+ controllerMessenger.call('AlertController:getState')
+ .web3ShimUsageOrigins,
+ ).toStrictEqual({
+ testWeb3ShimUsageOrigin: 1,
+ });
+ });
+ });
+});
diff --git a/app/scripts/controllers/alert-controller.ts b/app/scripts/controllers/alert-controller.ts
new file mode 100644
index 000000000000..9e1882035e02
--- /dev/null
+++ b/app/scripts/controllers/alert-controller.ts
@@ -0,0 +1,203 @@
+import { ObservableStore } from '@metamask/obs-store';
+import {
+ AccountsControllerGetSelectedAccountAction,
+ AccountsControllerSelectedAccountChangeEvent,
+} from '@metamask/accounts-controller';
+import { RestrictedControllerMessenger } from '@metamask/base-controller';
+import {
+ TOGGLEABLE_ALERT_TYPES,
+ Web3ShimUsageAlertStates,
+} from '../../../shared/constants/alerts';
+
+const controllerName = 'AlertController';
+
+/**
+ * Returns the state of the {@link AlertController}.
+ */
+export type AlertControllerGetStateAction = {
+ type: 'AlertController:getState';
+ handler: () => AlertControllerState;
+};
+
+/**
+ * Actions exposed by the {@link AlertController}.
+ */
+export type AlertControllerActions = AlertControllerGetStateAction;
+
+/**
+ * Event emitted when the state of the {@link AlertController} changes.
+ */
+export type AlertControllerStateChangeEvent = {
+ type: 'AlertController:stateChange';
+ payload: [AlertControllerState, []];
+};
+
+/**
+ * Events emitted by {@link AlertController}.
+ */
+export type AlertControllerEvents = AlertControllerStateChangeEvent;
+
+/**
+ * Actions that this controller is allowed to call.
+ */
+export type AllowedActions = AccountsControllerGetSelectedAccountAction;
+
+/**
+ * Events that this controller is allowed to subscribe.
+ */
+export type AllowedEvents = AccountsControllerSelectedAccountChangeEvent;
+
+export type AlertControllerMessenger = RestrictedControllerMessenger<
+ typeof controllerName,
+ AlertControllerActions | AllowedActions,
+ AlertControllerEvents | AllowedEvents,
+ AllowedActions['type'],
+ AllowedEvents['type']
+>;
+
+/**
+ * The alert controller state type
+ *
+ * @property alertEnabledness - A map of alerts IDs to booleans, where
+ * `true` indicates that the alert is enabled and shown, and `false` the opposite.
+ * @property unconnectedAccountAlertShownOrigins - A map of origin
+ * strings to booleans indicating whether the "switch to connected" alert has
+ * been shown (`true`) or otherwise (`false`).
+ */
+export type AlertControllerState = {
+ alertEnabledness: Record;
+ unconnectedAccountAlertShownOrigins: Record;
+ web3ShimUsageOrigins?: Record;
+};
+
+/**
+ * The alert controller options
+ *
+ * @property state - The initial controller state
+ * @property controllerMessenger - The controller messenger
+ */
+type AlertControllerOptions = {
+ state?: Partial;
+ controllerMessenger: AlertControllerMessenger;
+};
+
+const defaultState: AlertControllerState = {
+ alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce(
+ (alertEnabledness: Record, alertType: string) => {
+ alertEnabledness[alertType] = true;
+ return alertEnabledness;
+ },
+ {},
+ ),
+ unconnectedAccountAlertShownOrigins: {},
+ web3ShimUsageOrigins: {},
+};
+
+/**
+ * Controller responsible for maintaining alert-related state.
+ */
+export class AlertController {
+ store: ObservableStore;
+
+ readonly #controllerMessenger: AlertControllerMessenger;
+
+ #selectedAddress: string;
+
+ constructor(opts: AlertControllerOptions) {
+ const state: AlertControllerState = {
+ ...defaultState,
+ ...opts.state,
+ };
+
+ this.store = new ObservableStore(state);
+ this.#controllerMessenger = opts.controllerMessenger;
+ this.#controllerMessenger.registerActionHandler(
+ 'AlertController:getState',
+ () => this.store.getState(),
+ );
+ this.store.subscribe((alertState: AlertControllerState) => {
+ this.#controllerMessenger.publish(
+ 'AlertController:stateChange',
+ alertState,
+ [],
+ );
+ });
+
+ this.#selectedAddress = this.#controllerMessenger.call(
+ 'AccountsController:getSelectedAccount',
+ ).address;
+
+ this.#controllerMessenger.subscribe(
+ 'AccountsController:selectedAccountChange',
+ (account: { address: string }) => {
+ const currentState = this.store.getState();
+ if (
+ currentState.unconnectedAccountAlertShownOrigins &&
+ this.#selectedAddress !== account.address
+ ) {
+ this.#selectedAddress = account.address;
+ this.store.updateState({ unconnectedAccountAlertShownOrigins: {} });
+ }
+ },
+ );
+ }
+
+ setAlertEnabledness(alertId: string, enabledness: boolean): void {
+ const { alertEnabledness } = this.store.getState();
+ alertEnabledness[alertId] = enabledness;
+ this.store.updateState({ alertEnabledness });
+ }
+
+ /**
+ * Sets the "switch to connected" alert as shown for the given origin
+ *
+ * @param origin - The origin the alert has been shown for
+ */
+ setUnconnectedAccountAlertShown(origin: string): void {
+ const { unconnectedAccountAlertShownOrigins } = this.store.getState();
+ unconnectedAccountAlertShownOrigins[origin] = true;
+ this.store.updateState({ unconnectedAccountAlertShownOrigins });
+ }
+
+ /**
+ * Gets the web3 shim usage state for the given origin.
+ *
+ * @param origin - The origin to get the web3 shim usage state for.
+ * @returns The web3 shim usage state for the given
+ * origin, or undefined.
+ */
+ getWeb3ShimUsageState(origin: string): number | undefined {
+ return this.store.getState().web3ShimUsageOrigins?.[origin];
+ }
+
+ /**
+ * Sets the web3 shim usage state for the given origin to RECORDED.
+ *
+ * @param origin - The origin the that used the web3 shim.
+ */
+ setWeb3ShimUsageRecorded(origin: string): void {
+ this.#setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.recorded);
+ }
+
+ /**
+ * Sets the web3 shim usage state for the given origin to DISMISSED.
+ *
+ * @param origin - The origin that the web3 shim notification was
+ * dismissed for.
+ */
+ setWeb3ShimUsageAlertDismissed(origin: string): void {
+ this.#setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.dismissed);
+ }
+
+ /**
+ * @param origin - The origin to set the state for.
+ * @param value - The state value to set.
+ */
+ #setWeb3ShimUsageState(origin: string, value: number): void {
+ const { web3ShimUsageOrigins } = this.store.getState();
+ if (web3ShimUsageOrigins) {
+ web3ShimUsageOrigins[origin] = value;
+ this.store.updateState({ web3ShimUsageOrigins });
+ }
+ }
+}
diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js
deleted file mode 100644
index d13e4cb2fbbf..000000000000
--- a/app/scripts/controllers/alert.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import { ObservableStore } from '@metamask/obs-store';
-import {
- TOGGLEABLE_ALERT_TYPES,
- Web3ShimUsageAlertStates,
-} from '../../../shared/constants/alerts';
-
-/**
- * @typedef {object} AlertControllerInitState
- * @property {object} alertEnabledness - A map of alerts IDs to booleans, where
- * `true` indicates that the alert is enabled and shown, and `false` the opposite.
- * @property {object} unconnectedAccountAlertShownOrigins - A map of origin
- * strings to booleans indicating whether the "switch to connected" alert has
- * been shown (`true`) or otherwise (`false`).
- */
-
-/**
- * @typedef {object} AlertControllerOptions
- * @property {AlertControllerInitState} initState - The initial controller state
- */
-
-const defaultState = {
- alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce(
- (alertEnabledness, alertType) => {
- alertEnabledness[alertType] = true;
- return alertEnabledness;
- },
- {},
- ),
- unconnectedAccountAlertShownOrigins: {},
- web3ShimUsageOrigins: {},
-};
-
-/**
- * Controller responsible for maintaining alert-related state.
- */
-export default class AlertController {
- /**
- * @param {AlertControllerOptions} [opts] - Controller configuration parameters
- */
- constructor(opts = {}) {
- const { initState = {}, controllerMessenger } = opts;
- const state = {
- ...defaultState,
- alertEnabledness: {
- ...defaultState.alertEnabledness,
- ...initState.alertEnabledness,
- },
- };
-
- this.store = new ObservableStore(state);
- this.controllerMessenger = controllerMessenger;
-
- this.selectedAddress = this.controllerMessenger.call(
- 'AccountsController:getSelectedAccount',
- );
-
- this.controllerMessenger.subscribe(
- 'AccountsController:selectedAccountChange',
- (account) => {
- const currentState = this.store.getState();
- if (
- currentState.unconnectedAccountAlertShownOrigins &&
- this.selectedAddress !== account.address
- ) {
- this.selectedAddress = account.address;
- this.store.updateState({ unconnectedAccountAlertShownOrigins: {} });
- }
- },
- );
- }
-
- setAlertEnabledness(alertId, enabledness) {
- let { alertEnabledness } = this.store.getState();
- alertEnabledness = { ...alertEnabledness };
- alertEnabledness[alertId] = enabledness;
- this.store.updateState({ alertEnabledness });
- }
-
- /**
- * Sets the "switch to connected" alert as shown for the given origin
- *
- * @param {string} origin - The origin the alert has been shown for
- */
- setUnconnectedAccountAlertShown(origin) {
- let { unconnectedAccountAlertShownOrigins } = this.store.getState();
- unconnectedAccountAlertShownOrigins = {
- ...unconnectedAccountAlertShownOrigins,
- };
- unconnectedAccountAlertShownOrigins[origin] = true;
- this.store.updateState({ unconnectedAccountAlertShownOrigins });
- }
-
- /**
- * Gets the web3 shim usage state for the given origin.
- *
- * @param {string} origin - The origin to get the web3 shim usage state for.
- * @returns {undefined | 1 | 2} The web3 shim usage state for the given
- * origin, or undefined.
- */
- getWeb3ShimUsageState(origin) {
- return this.store.getState().web3ShimUsageOrigins[origin];
- }
-
- /**
- * Sets the web3 shim usage state for the given origin to RECORDED.
- *
- * @param {string} origin - The origin the that used the web3 shim.
- */
- setWeb3ShimUsageRecorded(origin) {
- this._setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.recorded);
- }
-
- /**
- * Sets the web3 shim usage state for the given origin to DISMISSED.
- *
- * @param {string} origin - The origin that the web3 shim notification was
- * dismissed for.
- */
- setWeb3ShimUsageAlertDismissed(origin) {
- this._setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.dismissed);
- }
-
- /**
- * @private
- * @param {string} origin - The origin to set the state for.
- * @param {number} value - The state value to set.
- */
- _setWeb3ShimUsageState(origin, value) {
- let { web3ShimUsageOrigins } = this.store.getState();
- web3ShimUsageOrigins = {
- ...web3ShimUsageOrigins,
- };
- web3ShimUsageOrigins[origin] = value;
- this.store.updateState({ web3ShimUsageOrigins });
- }
-}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 142ae80d09c1..f7832f5893ec 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -298,7 +298,7 @@ import createOnboardingMiddleware from './lib/createOnboardingMiddleware';
import { isStreamWritable, setupMultiplex } from './lib/stream-utils';
import { PreferencesController } from './controllers/preferences-controller';
import AppStateController from './controllers/app-state';
-import AlertController from './controllers/alert';
+import { AlertController } from './controllers/alert-controller';
import OnboardingController from './controllers/onboarding';
import Backup from './lib/backup';
import DecryptMessageController from './controllers/decrypt-message';
@@ -1789,7 +1789,7 @@ export default class MetamaskController extends EventEmitter {
});
this.alertController = new AlertController({
- initState: initState.AlertController,
+ state: initState.AlertController,
controllerMessenger: this.controllerMessenger.getRestricted({
name: 'AlertController',
allowedEvents: ['AccountsController:selectedAccountChange'],
diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json
index 17d68bd0e500..e21cc6b03a0c 100644
--- a/development/ts-migration-dashboard/files-to-convert.json
+++ b/development/ts-migration-dashboard/files-to-convert.json
@@ -4,7 +4,6 @@
"app/scripts/constants/contracts.js",
"app/scripts/constants/on-ramp.js",
"app/scripts/contentscript.js",
- "app/scripts/controllers/alert.js",
"app/scripts/controllers/app-state.js",
"app/scripts/controllers/cached-balances.js",
"app/scripts/controllers/cached-balances.test.js",
From 327a2601569e40dcad1a99e76b5313a640d1dfd8 Mon Sep 17 00:00:00 2001
From: sahar-fehri
Date: Thu, 17 Oct 2024 17:36:21 +0200
Subject: [PATCH 28/51] =?UTF-8?q?fix:=20fix=20currency=20display=20when=20?=
=?UTF-8?q?tokenToFiatConversion=20rate=20is=20not=20avai=E2=80=A6=20(#278?=
=?UTF-8?q?93)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
When the fiat conversion for a token is not available; we should show
the token balance.
The default behavior is working as expected; but when a user switches to
a token where fiat conversions are available and clicks on the swap icon
then goes back to the token with no conversions it will show USD when it
should default to token value.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27893?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27805
## **Manual testing steps**
1. click on send
2. select account to send to
3. select token to send, preferably one with poor pricing data (ETH:
0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009)
4. confirm that balance is undefined
5. select a token that has pricing data
6. use arrows on the right of the amount field to swap USD amount to
token amount
7. reselect token to send in prior step
8. confirm that token balance now shows.
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/1b97544b-adcd-424b-9da4-e360b7b94968
### **After**
https://github.com/user-attachments/assets/09cc9428-450a-47a8-a111-414c5996149c
## **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.
---
.../swappable-currency-input/swappable-currency-input.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx b/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx
index ff587fe6c1c1..6d9820b67c8c 100644
--- a/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx
+++ b/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx
@@ -19,6 +19,7 @@ import {
import CurrencyInput from '../../../app/currency-input';
import { getIsFiatPrimary } from '../utils';
import { NFTInput } from '../nft-input/nft-input';
+import useTokenExchangeRate from '../../../app/currency-input/hooks/useTokenExchangeRate';
import SwapIcon from './swap-icon';
type BaseProps = {
@@ -81,12 +82,17 @@ export function SwappableCurrencyInput({
const t = useI18nContext();
const isFiatPrimary = useSelector(getIsFiatPrimary);
+ const tokenToFiatConversionRate = useTokenExchangeRate(
+ asset?.details?.address,
+ );
const isSetToMax = useSelector(getSendMaxModeState);
const TokenComponent = (
(
From fac442247f0180eb05973611e5d35db2fb1d3c21 Mon Sep 17 00:00:00 2001
From: Jyoti Puri
Date: Thu, 17 Oct 2024 21:50:30 +0530
Subject: [PATCH 29/51] feat: NFT permit simulations (#27825)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Add simulation section to NFT permit
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27394
## **Manual testing steps**
1. Submit NFT permit signature request
2. Check simulation section on the confirmation page
## **Screenshots/Recordings**
## **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
- [ ] 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.
---
.../permit-simulation.test.tsx.snap | 117 ++++++++++++++++++
.../permit-simulation.test.tsx | 21 +++-
.../permit-simulation/permit-simulation.tsx | 11 +-
.../__snapshots__/value-display.test.tsx.snap | 46 +++++++
.../value-display/value-display.test.tsx | 17 +++
.../value-display/value-display.tsx | 35 +++---
6 files changed, 230 insertions(+), 17 deletions(-)
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap
index 7c5553495eb0..f35e2218cbfb 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap
@@ -127,3 +127,120 @@ exports[`PermitSimulation renders component correctly 1`] = `
`;
+
+exports[`PermitSimulation renders correctly for NFT permit 1`] = `
+
+
+
+
+
+
+ Estimated changes
+
+
+
+
+
+
+ You're giving someone else permission to withdraw NFTs from your account.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0xC3644...1FE88
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx
index 1be34109a637..e89efb3c0dc1 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx
@@ -4,7 +4,10 @@ import { act } from 'react-dom/test-utils';
import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../test/data/confirmations/helper';
import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers';
-import { permitSignatureMsg } from '../../../../../../../../test/data/confirmations/typed_sign';
+import {
+ permitNFTSignatureMsg,
+ permitSignatureMsg,
+} from '../../../../../../../../test/data/confirmations/typed_sign';
import PermitSimulation from './permit-simulation';
jest.mock('../../../../../../../store/actions', () => {
@@ -28,4 +31,20 @@ describe('PermitSimulation', () => {
expect(container).toMatchSnapshot();
});
});
+
+ it('renders correctly for NFT permit', async () => {
+ const state = getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg);
+ const mockStore = configureMockStore([])(state);
+
+ await act(async () => {
+ const { container, findByText } = renderWithConfirmContextProvider(
+
,
+ mockStore,
+ );
+
+ expect(await findByText('Withdraw')).toBeInTheDocument();
+ expect(await findByText('#3606393')).toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+ });
});
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
index 231997d18547..44131ec18fbf 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
@@ -45,8 +45,10 @@ const PermitSimulation: React.FC
`;
+
+exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = `
+
+
+
+
+
+
+
+
+ 0xA0b86...6eB48
+
+
+
+
+
+
+
+`;
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx
index f6af7357502d..da86d497aac1 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx
@@ -29,4 +29,21 @@ describe('PermitSimulationValueDisplay', () => {
expect(container).toMatchSnapshot();
});
});
+
+ it('renders component correctly for NFT token', async () => {
+ const mockStore = configureMockStore([])(mockState);
+
+ await act(async () => {
+ const { container, findByText } = renderWithProvider(
+ ,
+ mockStore,
+ );
+
+ expect(await findByText('#4321')).toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+ });
});
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx
index 633191cd2638..360559493596 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx
@@ -41,21 +41,26 @@ type PermitSimulationValueDisplayParams = {
tokenContract: Hex | string;
/** The token amount */
- value: number | string;
+ value?: number | string;
+
+ /** The tokenId for NFT */
+ tokenId?: string;
};
const PermitSimulationValueDisplay: React.FC<
PermitSimulationValueDisplayParams
-> = ({ primaryType, tokenContract, value }) => {
+> = ({ primaryType, tokenContract, value, tokenId }) => {
const exchangeRate = useTokenExchangeRate(tokenContract);
- const { value: tokenDecimals } = useAsyncResult(
- async () => await fetchErc20Decimals(tokenContract),
- [tokenContract],
- );
+ const { value: tokenDecimals } = useAsyncResult(async () => {
+ if (tokenId) {
+ return undefined;
+ }
+ return await fetchErc20Decimals(tokenContract);
+ }, [tokenContract]);
const fiatValue = useMemo(() => {
- if (exchangeRate && value) {
+ if (exchangeRate && value && !tokenId) {
const tokenAmount = calcTokenAmount(value, tokenDecimals);
return exchangeRate.times(tokenAmount).toNumber();
}
@@ -63,7 +68,7 @@ const PermitSimulationValueDisplay: React.FC<
}, [exchangeRate, tokenDecimals, value]);
const { tokenValue, tokenValueMaxPrecision } = useMemo(() => {
- if (!value) {
+ if (!value || tokenId) {
return { tokenValue: null, tokenValueMaxPrecision: null };
}
@@ -107,12 +112,14 @@ const PermitSimulationValueDisplay: React.FC<
style={{ paddingTop: '1px', paddingBottom: '1px' }}
textAlign={TextAlign.Center}
>
- {shortenString(tokenValue || '', {
- truncatedCharLimit: 15,
- truncatedStartChars: 15,
- truncatedEndChars: 0,
- skipCharacterInEnd: true,
- })}
+ {tokenValue !== null &&
+ shortenString(tokenValue || '', {
+ truncatedCharLimit: 15,
+ truncatedStartChars: 15,
+ truncatedEndChars: 0,
+ skipCharacterInEnd: true,
+ })}
+ {tokenId && `#${tokenId}`}
From 654dff7f918bb32a5051443609b424b2559080b2 Mon Sep 17 00:00:00 2001
From: sahar-fehri
Date: Thu, 17 Oct 2024 19:43:39 +0200
Subject: [PATCH 30/51] fix: add APE network icon (#27841)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Adds APE network icon
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27841?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/26874
## **Manual testing steps**
1. Click on network picker and click on add custom network
2. ADD name "APE"; RPC: https://curtis.rpc.caldera.xyz/http and chainId
33111
3. Click on add network
4. You should see network icon
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/89abe1e2-49e4-46ba-ada5-953fa99aa9c0
### **After**
https://github.com/user-attachments/assets/d47f9b55-f5c1-4d07-a025-34c8008eef5f
## **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/images/ape.svg | 1658 +++++++++++++++++++++++++++++++++++
shared/constants/network.ts | 3 +
2 files changed, 1661 insertions(+)
create mode 100644 app/images/ape.svg
diff --git a/app/images/ape.svg b/app/images/ape.svg
new file mode 100644
index 000000000000..495a73676a5e
--- /dev/null
+++ b/app/images/ape.svg
@@ -0,0 +1,1658 @@
+
\ No newline at end of file
diff --git a/shared/constants/network.ts b/shared/constants/network.ts
index 9ed2e26150a9..e911ce1aabf5 100644
--- a/shared/constants/network.ts
+++ b/shared/constants/network.ts
@@ -146,6 +146,7 @@ export const CHAIN_IDS = {
CHZ: '0x15b38',
NUMBERS: '0x290b',
SEI: '0x531',
+ APE_TESTNET: '0x8157',
BERACHAIN: '0x138d5',
METACHAIN_ONE: '0x1b6e6',
ARBITRUM_SEPOLIA: '0x66eee',
@@ -448,6 +449,7 @@ export const NUMBERS_MAINNET_IMAGE_URL = './images/numbers-mainnet.svg';
export const NUMBERS_TOKEN_IMAGE_URL = './images/numbers-token.png';
export const SEI_IMAGE_URL = './images/sei.svg';
export const NEAR_IMAGE_URL = './images/near.svg';
+export const APE_TESTNET_IMAGE_URL = './images/ape.svg';
export const INFURA_PROVIDER_TYPES = [
NETWORK_TYPES.MAINNET,
@@ -780,6 +782,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = {
[CHAINLIST_CHAIN_IDS_MAP.ZKATANA]: ZKATANA_MAINNET_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.ZORA_MAINNET]: ZORA_MAINNET_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.FILECOIN]: FILECOIN_MAINNET_IMAGE_URL,
+ [CHAINLIST_CHAIN_IDS_MAP.APE_TESTNET]: APE_TESTNET_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.BASE]: BASE_TOKEN_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.NUMBERS]: NUMBERS_MAINNET_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.SEI]: SEI_IMAGE_URL,
From 9716e949259d7c89cb99f688aef670b8d7af4e73 Mon Sep 17 00:00:00 2001
From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com>
Date: Thu, 17 Oct 2024 19:54:23 +0200
Subject: [PATCH 31/51] fix: bump `@metamask/ppom-validator` from `0.34.0` to
`0.35.1` (#27939)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR bumps `@metamask/ppom-validator ` from `0.34.0` to `0.35.1`
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27939?quickstart=1)
## **Related issues**
Fixes: #27909
## **Manual testing steps**
## **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.
---------
Co-authored-by: MetaMask Bot
---
lavamoat/browserify/beta/policy.json | 67 +--------------------------
lavamoat/browserify/flask/policy.json | 67 +--------------------------
lavamoat/browserify/main/policy.json | 67 +--------------------------
lavamoat/browserify/mmi/policy.json | 67 +--------------------------
package.json | 2 +-
yarn.lock | 35 ++++----------
6 files changed, 19 insertions(+), 286 deletions(-)
diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json
index 880b542673ea..27b06f2ba5b8 100644
--- a/lavamoat/browserify/beta/policy.json
+++ b/lavamoat/browserify/beta/policy.json
@@ -2028,78 +2028,15 @@
"crypto": true
},
"packages": {
+ "@metamask/base-controller": true,
+ "@metamask/controller-utils": true,
"@metamask/eth-query>json-rpc-random-id": true,
- "@metamask/ppom-validator>@metamask/base-controller": true,
- "@metamask/ppom-validator>@metamask/controller-utils": true,
- "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
"await-semaphore": true,
"browserify>buffer": true
}
},
- "@metamask/ppom-validator>@metamask/base-controller": {
- "globals": {
- "setTimeout": true
- },
- "packages": {
- "immer": true
- }
- },
- "@metamask/ppom-validator>@metamask/controller-utils": {
- "globals": {
- "URL": true,
- "console.error": true,
- "fetch": true,
- "setTimeout": true
- },
- "packages": {
- "@ethereumjs/tx>@ethereumjs/util": true,
- "@metamask/controller-utils>@spruceid/siwe-parser": true,
- "@metamask/ethjs>@metamask/ethjs-unit": true,
- "@metamask/ppom-validator>@metamask/utils": true,
- "bn.js": true,
- "browserify>buffer": true,
- "eslint>fast-deep-equal": true,
- "eth-ens-namehash": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors": {
- "packages": {
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
- "@metamask/rpc-errors>fast-safe-stringify": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
- "@metamask/ppom-validator>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
"@metamask/ppom-validator>crypto-js": {
"globals": {
"crypto": true,
diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json
index 880b542673ea..27b06f2ba5b8 100644
--- a/lavamoat/browserify/flask/policy.json
+++ b/lavamoat/browserify/flask/policy.json
@@ -2028,78 +2028,15 @@
"crypto": true
},
"packages": {
+ "@metamask/base-controller": true,
+ "@metamask/controller-utils": true,
"@metamask/eth-query>json-rpc-random-id": true,
- "@metamask/ppom-validator>@metamask/base-controller": true,
- "@metamask/ppom-validator>@metamask/controller-utils": true,
- "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
"await-semaphore": true,
"browserify>buffer": true
}
},
- "@metamask/ppom-validator>@metamask/base-controller": {
- "globals": {
- "setTimeout": true
- },
- "packages": {
- "immer": true
- }
- },
- "@metamask/ppom-validator>@metamask/controller-utils": {
- "globals": {
- "URL": true,
- "console.error": true,
- "fetch": true,
- "setTimeout": true
- },
- "packages": {
- "@ethereumjs/tx>@ethereumjs/util": true,
- "@metamask/controller-utils>@spruceid/siwe-parser": true,
- "@metamask/ethjs>@metamask/ethjs-unit": true,
- "@metamask/ppom-validator>@metamask/utils": true,
- "bn.js": true,
- "browserify>buffer": true,
- "eslint>fast-deep-equal": true,
- "eth-ens-namehash": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors": {
- "packages": {
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
- "@metamask/rpc-errors>fast-safe-stringify": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
- "@metamask/ppom-validator>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
"@metamask/ppom-validator>crypto-js": {
"globals": {
"crypto": true,
diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json
index 880b542673ea..27b06f2ba5b8 100644
--- a/lavamoat/browserify/main/policy.json
+++ b/lavamoat/browserify/main/policy.json
@@ -2028,78 +2028,15 @@
"crypto": true
},
"packages": {
+ "@metamask/base-controller": true,
+ "@metamask/controller-utils": true,
"@metamask/eth-query>json-rpc-random-id": true,
- "@metamask/ppom-validator>@metamask/base-controller": true,
- "@metamask/ppom-validator>@metamask/controller-utils": true,
- "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
"await-semaphore": true,
"browserify>buffer": true
}
},
- "@metamask/ppom-validator>@metamask/base-controller": {
- "globals": {
- "setTimeout": true
- },
- "packages": {
- "immer": true
- }
- },
- "@metamask/ppom-validator>@metamask/controller-utils": {
- "globals": {
- "URL": true,
- "console.error": true,
- "fetch": true,
- "setTimeout": true
- },
- "packages": {
- "@ethereumjs/tx>@ethereumjs/util": true,
- "@metamask/controller-utils>@spruceid/siwe-parser": true,
- "@metamask/ethjs>@metamask/ethjs-unit": true,
- "@metamask/ppom-validator>@metamask/utils": true,
- "bn.js": true,
- "browserify>buffer": true,
- "eslint>fast-deep-equal": true,
- "eth-ens-namehash": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors": {
- "packages": {
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
- "@metamask/rpc-errors>fast-safe-stringify": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
- "@metamask/ppom-validator>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
"@metamask/ppom-validator>crypto-js": {
"globals": {
"crypto": true,
diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json
index 25756f84ccc4..7c19b1b4c76e 100644
--- a/lavamoat/browserify/mmi/policy.json
+++ b/lavamoat/browserify/mmi/policy.json
@@ -2120,78 +2120,15 @@
"crypto": true
},
"packages": {
+ "@metamask/base-controller": true,
+ "@metamask/controller-utils": true,
"@metamask/eth-query>json-rpc-random-id": true,
- "@metamask/ppom-validator>@metamask/base-controller": true,
- "@metamask/ppom-validator>@metamask/controller-utils": true,
- "@metamask/ppom-validator>@metamask/rpc-errors": true,
"@metamask/ppom-validator>crypto-js": true,
"@metamask/ppom-validator>elliptic": true,
"await-semaphore": true,
"browserify>buffer": true
}
},
- "@metamask/ppom-validator>@metamask/base-controller": {
- "globals": {
- "setTimeout": true
- },
- "packages": {
- "immer": true
- }
- },
- "@metamask/ppom-validator>@metamask/controller-utils": {
- "globals": {
- "URL": true,
- "console.error": true,
- "fetch": true,
- "setTimeout": true
- },
- "packages": {
- "@ethereumjs/tx>@ethereumjs/util": true,
- "@metamask/controller-utils>@spruceid/siwe-parser": true,
- "@metamask/ethjs>@metamask/ethjs-unit": true,
- "@metamask/ppom-validator>@metamask/utils": true,
- "bn.js": true,
- "browserify>buffer": true,
- "eslint>fast-deep-equal": true,
- "eth-ens-namehash": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors": {
- "packages": {
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true,
- "@metamask/rpc-errors>fast-safe-stringify": true
- }
- },
- "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
- "@metamask/ppom-validator>@metamask/utils": {
- "globals": {
- "TextDecoder": true,
- "TextEncoder": true
- },
- "packages": {
- "@metamask/utils>@metamask/superstruct": true,
- "@metamask/utils>@scure/base": true,
- "@metamask/utils>pony-cause": true,
- "@noble/hashes": true,
- "browserify>buffer": true,
- "nock>debug": true,
- "semver": true
- }
- },
"@metamask/ppom-validator>crypto-js": {
"globals": {
"crypto": true,
diff --git a/package.json b/package.json
index 5709ea91a51c..3e51e31e1380 100644
--- a/package.json
+++ b/package.json
@@ -340,7 +340,7 @@
"@metamask/permission-log-controller": "^2.0.1",
"@metamask/phishing-controller": "^12.0.1",
"@metamask/post-message-stream": "^8.0.0",
- "@metamask/ppom-validator": "0.34.0",
+ "@metamask/ppom-validator": "0.35.1",
"@metamask/preinstalled-example-snap": "^0.2.0",
"@metamask/profile-sync-controller": "^0.9.7",
"@metamask/providers": "^14.0.2",
diff --git a/yarn.lock b/yarn.lock
index 19e1f3bf1a8b..186f52706a3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5031,21 +5031,6 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/controller-utils@npm:^8.0.1":
- version: 8.0.4
- resolution: "@metamask/controller-utils@npm:8.0.4"
- dependencies:
- "@ethereumjs/util": "npm:^8.1.0"
- "@metamask/eth-query": "npm:^4.0.0"
- "@metamask/ethjs-unit": "npm:^0.3.0"
- "@metamask/utils": "npm:^8.3.0"
- "@spruceid/siwe-parser": "npm:1.1.3"
- eth-ens-namehash: "npm:^2.0.8"
- fast-deep-equal: "npm:^3.1.3"
- checksum: 10/112a07614eec28cff270c99aa0695bec34cd29461d0c4cb83eb913a5bc37b3b72e4f33dad59a0ab23da5d1b091372ee5207657349bfdb814098c5a51d6570554
- languageName: node
- linkType: hard
-
"@metamask/design-tokens@npm:^1.12.0":
version: 1.13.0
resolution: "@metamask/design-tokens@npm:1.13.0"
@@ -6039,21 +6024,21 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/ppom-validator@npm:0.34.0":
- version: 0.34.0
- resolution: "@metamask/ppom-validator@npm:0.34.0"
+"@metamask/ppom-validator@npm:0.35.1":
+ version: 0.35.1
+ resolution: "@metamask/ppom-validator@npm:0.35.1"
dependencies:
- "@metamask/base-controller": "npm:^6.0.2"
- "@metamask/controller-utils": "npm:^8.0.1"
- "@metamask/network-controller": "npm:^20.0.0"
- "@metamask/rpc-errors": "npm:^6.3.1"
- "@metamask/utils": "npm:^8.3.0"
+ "@metamask/base-controller": "npm:^7.0.1"
+ "@metamask/controller-utils": "npm:^11.3.0"
+ "@metamask/utils": "npm:^9.2.1"
await-semaphore: "npm:^0.1.3"
crypto-js: "npm:^4.2.0"
elliptic: "npm:^6.5.4"
eslint-plugin-n: "npm:^16.6.2"
json-rpc-random-id: "npm:^1.0.1"
- checksum: 10/140b2070ddf4a9d7d13518ab1a10aa71961715434053096d0caa6f4ce104bbcaea5f5152edfa9b6c42f9bc929116afbb6a0542c1147e3101d04ef29bcf7a6c9f
+ peerDependencies:
+ "@metamask/network-controller": ^21.0.0
+ checksum: 10/3dd37ced473a78e4b7847c61b6c6fb1e2ae4865dee67de9574462fd618dc5ea7be46874f12ff18383702c46c9c07c32dbac00be2e6ad26cb45a3dcc4ffa09ab7
languageName: node
linkType: hard
@@ -26163,7 +26148,7 @@ __metadata:
"@metamask/phishing-controller": "npm:^12.0.1"
"@metamask/phishing-warning": "npm:^4.0.0"
"@metamask/post-message-stream": "npm:^8.0.0"
- "@metamask/ppom-validator": "npm:0.34.0"
+ "@metamask/ppom-validator": "npm:0.35.1"
"@metamask/preferences-controller": "npm:^13.0.2"
"@metamask/preinstalled-example-snap": "npm:^0.2.0"
"@metamask/profile-sync-controller": "npm:^0.9.7"
From 35b86fc0ad54ef09d08fe6d1ebe9448b28e9e417 Mon Sep 17 00:00:00 2001
From: David Drazic
Date: Thu, 17 Oct 2024 23:04:01 +0200
Subject: [PATCH 32/51] fix: hide options menu that was being shown for
preinstalled Snaps (#27937)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR will hide Snaps `options menu` and `info icon` in Snaps header
for **preinstalled Snaps**.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27937?quickstart=1)
## **Related issues**
Fixes: n/a
## **Manual testing steps**
1. Go to Account watcher Snap and make sure that there is no button with
three dots in Snaps header.
2. Go to ordinary Home Page Snap and make sure that three dots are
available there.
3. Go to ordinary (non-preinstalled) custom UI Snap with confirmation
popup of any type and make sure that there is Info icon in the right
corner of the header.
## **Screenshots/Recordings**
### **Before**
![image](https://github.com/user-attachments/assets/8cf66b7e-6719-44ea-a976-da7b0a94eb4e)
### **After**
![Screenshot 2024-10-17 at 17 44
34](https://github.com/user-attachments/assets/c4014f2d-90ac-4ca1-bd92-095c2a859084)
![Screenshot 2024-10-17 at 17 45
26](https://github.com/user-attachments/assets/08e4fc79-ae24-43bd-bdf1-5b6d7883a075)
![Screenshot 2024-10-17 at 17 46
03](https://github.com/user-attachments/assets/43c9ec79-cf0c-4e01-b0b6-c8a301af2c46)
## **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.
---
.../snap-authorship-header.js | 4 ++--
ui/pages/snaps/snap-view/snap-view.js | 14 ++++++++------
ui/selectors/selectors.js | 1 +
3 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js
index 0cb0a48dd2d6..ed835f6d241d 100644
--- a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js
+++ b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js
@@ -39,7 +39,7 @@ const SnapAuthorshipHeader = ({
const t = useI18nContext();
const [isModalOpen, setIsModalOpen] = useState(false);
- const { name: snapName } = useSelector((state) =>
+ const { name: snapName, hidden } = useSelector((state) =>
getSnapMetadata(state, snapId),
);
@@ -105,7 +105,7 @@ const SnapAuthorshipHeader = ({
- {showInfo && (
+ {showInfo && !hidden && (
+ !snap.hidden && (
+
+ )
}
/>
)}
diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js
index 09c062012731..431d2c1b5d0a 100644
--- a/ui/selectors/selectors.js
+++ b/ui/selectors/selectors.js
@@ -1612,6 +1612,7 @@ export const getSnapsMetadata = createDeepEqualSelector(
snapsMetadata[snapId] = {
name: manifest.proposedName,
description: manifest.description,
+ hidden: snap.hidden,
};
return snapsMetadata;
}, {});
From 02f7ec4479ad06d1dc3618172fdc64d04f04ca85 Mon Sep 17 00:00:00 2001
From: Matteo Scurati
Date: Fri, 18 Oct 2024 06:52:19 +0200
Subject: [PATCH 33/51] fix: bump message signing snap to support portfolio
automatic connections (#27936)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Bumps `@metamask/message-signing-snap` to `0.4.0` to allow portfolio to
support automatic connections.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27306?quickstart=1)
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/NOTIFY-1136
## **Manual testing steps**
This does not effect anything within the wallet.
## **Screenshots/Recordings**
### **Before**
N/A
### **After**
N/A
## **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/snaps/preinstalled-snaps.ts | 2 +-
package.json | 4 +-
yarn.lock | 61 +++++++++++++++----------
3 files changed, 39 insertions(+), 28 deletions(-)
diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts
index f46681ddab57..c725a2cbd837 100644
--- a/app/scripts/snaps/preinstalled-snaps.ts
+++ b/app/scripts/snaps/preinstalled-snaps.ts
@@ -9,7 +9,7 @@ import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/pr
// The casts here are less than ideal but we expect the SnapController to validate the inputs.
const PREINSTALLED_SNAPS = Object.freeze([
- MessageSigningSnap as PreinstalledSnap,
+ MessageSigningSnap as unknown as PreinstalledSnap,
EnsResolverSnap as PreinstalledSnap,
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
AccountWatcherSnap as PreinstalledSnap,
diff --git a/package.json b/package.json
index 3e51e31e1380..873e3ad6324c 100644
--- a/package.json
+++ b/package.json
@@ -328,7 +328,7 @@
"@metamask/logging-controller": "^6.0.0",
"@metamask/logo": "^3.1.2",
"@metamask/message-manager": "^10.1.0",
- "@metamask/message-signing-snap": "^0.3.3",
+ "@metamask/message-signing-snap": "^0.4.0",
"@metamask/metamask-eth-abis": "^3.1.1",
"@metamask/name-controller": "^8.0.0",
"@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch",
@@ -349,7 +349,7 @@
"@metamask/rpc-errors": "^7.0.0",
"@metamask/safe-event-emitter": "^3.1.1",
"@metamask/scure-bip39": "^2.0.3",
- "@metamask/selected-network-controller": "^18.0.1",
+ "@metamask/selected-network-controller": "^18.0.2",
"@metamask/signature-controller": "^19.1.0",
"@metamask/smart-transactions-controller": "^13.0.0",
"@metamask/snaps-controllers": "^9.11.1",
diff --git a/yarn.lock b/yarn.lock
index 186f52706a3e..9f6ea0aa2fc2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5581,6 +5581,17 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/json-rpc-engine@npm:^10.0.0":
+ version: 10.0.0
+ resolution: "@metamask/json-rpc-engine@npm:10.0.0"
+ dependencies:
+ "@metamask/rpc-errors": "npm:^7.0.0"
+ "@metamask/safe-event-emitter": "npm:^3.0.0"
+ "@metamask/utils": "npm:^9.1.0"
+ checksum: 10/2c401a4a64392aeb11c4f7ca8d7b458ba1106cff1e0b3dba8b3e0cc90e82f8c55ac2dc9fdfcd914b289e3298fb726d637cf21382336dde2c207cf76129ce5eab
+ languageName: node
+ linkType: hard
+
"@metamask/json-rpc-engine@npm:^7.1.0, @metamask/json-rpc-engine@npm:^7.1.1, @metamask/json-rpc-engine@npm:^7.3.2":
version: 7.3.3
resolution: "@metamask/json-rpc-engine@npm:7.3.3"
@@ -5603,7 +5614,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2, @metamask/json-rpc-engine@npm:^9.0.3":
+"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2":
version: 9.0.3
resolution: "@metamask/json-rpc-engine@npm:9.0.3"
dependencies:
@@ -5712,18 +5723,18 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/message-signing-snap@npm:^0.3.3":
- version: 0.3.3
- resolution: "@metamask/message-signing-snap@npm:0.3.3"
+"@metamask/message-signing-snap@npm:^0.4.0":
+ version: 0.4.0
+ resolution: "@metamask/message-signing-snap@npm:0.4.0"
dependencies:
- "@metamask/rpc-errors": "npm:^6.2.1"
- "@metamask/snaps-sdk": "npm:^3.1.1"
- "@metamask/utils": "npm:^8.3.0"
- "@noble/ciphers": "npm:^0.5.1"
- "@noble/curves": "npm:^1.4.0"
+ "@metamask/rpc-errors": "npm:^6.3.0"
+ "@metamask/snaps-sdk": "npm:^6.0.0"
+ "@metamask/utils": "npm:^9.0.0"
+ "@noble/ciphers": "npm:^0.5.3"
+ "@noble/curves": "npm:^1.4.2"
"@noble/hashes": "npm:^1.4.0"
- zod: "npm:^3.22.4"
- checksum: 10/8290f9779e826965082ef1c18189e96502a51b9ed3ade486dab91a1bcf4af150ffb04207f620ba2b98b7b268efe107d4953ab64fed0932b66b87c72f98cc944e
+ zod: "npm:^3.23.8"
+ checksum: 10/fb61da8f2999305f99ad5a1d6be2def224c88c1059fcdc8e70d06641d695eef82d9b8463c6b57d797a519aa70dc741b7cb59596f503faf2eff68a1647248b4de
languageName: node
linkType: hard
@@ -6154,7 +6165,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1":
+"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.0, @metamask/rpc-errors@npm:^6.3.1":
version: 6.4.0
resolution: "@metamask/rpc-errors@npm:6.4.0"
dependencies:
@@ -6198,18 +6209,18 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/selected-network-controller@npm:^18.0.1":
- version: 18.0.1
- resolution: "@metamask/selected-network-controller@npm:18.0.1"
+"@metamask/selected-network-controller@npm:^18.0.2":
+ version: 18.0.2
+ resolution: "@metamask/selected-network-controller@npm:18.0.2"
dependencies:
"@metamask/base-controller": "npm:^7.0.1"
- "@metamask/json-rpc-engine": "npm:^9.0.3"
+ "@metamask/json-rpc-engine": "npm:^10.0.0"
"@metamask/swappable-obj-proxy": "npm:^2.2.0"
"@metamask/utils": "npm:^9.1.0"
peerDependencies:
"@metamask/network-controller": ^21.0.0
"@metamask/permission-controller": ^11.0.0
- checksum: 10/79a862f352a819185a7bcc87f380a03bcc929db125467fa7e2ec0fc06647899b611a8cafe6aac14f2a02622f704b77e29cc833ab465b8c233eeb0a37b9a1dffc
+ checksum: 10/cf46a1a7d4ca19d6327aeb5918b2e904933b3ae6959184a2d5773be294d1b0dbe4d16189c46bfcbd83f33d95fe0c6e5cb64e4745fa0c75243db4c8304ab6ec8e
languageName: node
linkType: hard
@@ -6653,7 +6664,7 @@ __metadata:
languageName: node
linkType: hard
-"@noble/ciphers@npm:^0.5.1, @noble/ciphers@npm:^0.5.2":
+"@noble/ciphers@npm:^0.5.2, @noble/ciphers@npm:^0.5.3":
version: 0.5.3
resolution: "@noble/ciphers@npm:0.5.3"
checksum: 10/af0ad96b5807feace93e63549e05de6f5e305b36e2e95f02d90532893fbc3af3f19b9621b6de4caa98303659e5df2e7aa082064e5d4a82e6f38c728d48dfae5d
@@ -6669,7 +6680,7 @@ __metadata:
languageName: node
linkType: hard
-"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.2.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0":
+"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.2.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0":
version: 1.4.2
resolution: "@noble/curves@npm:1.4.2"
dependencies:
@@ -26135,7 +26146,7 @@ __metadata:
"@metamask/logging-controller": "npm:^6.0.0"
"@metamask/logo": "npm:^3.1.2"
"@metamask/message-manager": "npm:^10.1.0"
- "@metamask/message-signing-snap": "npm:^0.3.3"
+ "@metamask/message-signing-snap": "npm:^0.4.0"
"@metamask/metamask-eth-abis": "npm:^3.1.1"
"@metamask/name-controller": "npm:^8.0.0"
"@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch"
@@ -26158,7 +26169,7 @@ __metadata:
"@metamask/rpc-errors": "npm:^7.0.0"
"@metamask/safe-event-emitter": "npm:^3.1.1"
"@metamask/scure-bip39": "npm:^2.0.3"
- "@metamask/selected-network-controller": "npm:^18.0.1"
+ "@metamask/selected-network-controller": "npm:^18.0.2"
"@metamask/signature-controller": "npm:^19.1.0"
"@metamask/smart-transactions-controller": "npm:^13.0.0"
"@metamask/snaps-controllers": "npm:^9.11.1"
@@ -37492,10 +37503,10 @@ __metadata:
languageName: node
linkType: hard
-"zod@npm:^3.22.4":
- version: 3.22.4
- resolution: "zod@npm:3.22.4"
- checksum: 10/73622ca36a916f785cf528fe612a884b3e0f183dbe6b33365a7d0fc92abdbedf7804c5e2bd8df0a278e1472106d46674281397a3dd800fa9031dc3429758c6ac
+"zod@npm:^3.23.8":
+ version: 3.23.8
+ resolution: "zod@npm:3.23.8"
+ checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1
languageName: node
linkType: hard
From 1648b833971ae7c5679af0fde77603d54307fb89 Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Fri, 18 Oct 2024 09:30:22 +0200
Subject: [PATCH 34/51] test: [POM] Migrate contract interaction with snap
account e2e tests to page object modal (#27924)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR migrates the snap account contract interaction e2e tests to Page
Object Model (POM) pattern, improving test stability and
maintainability.
Changes include:
- Migrate test `snap-account-contract-interaction.spec.ts` to POM
- Migrate test `snap-account-signatures-and-disconnects.spec.ts` to POM
- Created related functions in TestDapp class
- Avoid several delays in the original function implementation
- Reduced flakiness
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1)
## **Related issues**
Fixes: #27933
## **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: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Chloe Gao
---
test/e2e/accounts/common.ts | 400 ------------------
.../snap-account-contract-interaction.spec.ts | 90 ----
...account-signatures-and-disconnects.spec.ts | 53 ---
test/e2e/flask/btc/common-btc.ts | 23 +-
test/e2e/flask/btc/create-btc-account.spec.ts | 3 +-
test/e2e/page-objects/pages/test-dapp.ts | 109 ++++-
.../snap-account-contract-interaction.spec.ts | 83 ++++
...account-signatures-and-disconnects.spec.ts | 66 +++
.../account/snap-account-signatures.spec.ts | 1 +
9 files changed, 274 insertions(+), 554 deletions(-)
delete mode 100644 test/e2e/accounts/common.ts
delete mode 100644 test/e2e/accounts/snap-account-contract-interaction.spec.ts
delete mode 100644 test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts
create mode 100644 test/e2e/tests/account/snap-account-contract-interaction.spec.ts
create mode 100644 test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts
diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts
deleted file mode 100644
index 62f3fc082b53..000000000000
--- a/test/e2e/accounts/common.ts
+++ /dev/null
@@ -1,400 +0,0 @@
-import { privateToAddress } from 'ethereumjs-util';
-import messages from '../../../app/_locales/en/messages.json';
-import FixtureBuilder from '../fixture-builder';
-import {
- PRIVATE_KEY,
- PRIVATE_KEY_TWO,
- WINDOW_TITLES,
- clickSignOnSignatureConfirmation,
- switchToOrOpenDapp,
- unlockWallet,
- validateContractDetails,
- multipleGanacheOptions,
-} from '../helpers';
-import { Driver } from '../webdriver/driver';
-import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants';
-import { retry } from '../../../development/lib/retry';
-
-/**
- * These are fixtures specific to Account Snap E2E tests:
- * -- connected to Test Dapp
- * -- two private keys with 25 ETH each
- *
- * @param title
- */
-export const accountSnapFixtures = (title: string | undefined) => {
- return {
- dapp: true,
- fixtures: new FixtureBuilder()
- .withPermissionControllerConnectedToTestDapp({
- restrictReturnedAccounts: false,
- })
- .build(),
- ganacheOptions: multipleGanacheOptions,
- title,
- };
-};
-
-// convert PRIVATE_KEY to public key
-export const PUBLIC_KEY = privateToAddress(
- Buffer.from(PRIVATE_KEY.slice(2), 'hex'),
-).toString('hex');
-
-export async function installSnapSimpleKeyring(
- driver: Driver,
- isAsyncFlow: boolean,
-) {
- await unlockWallet(driver);
-
- // navigate to test Snaps page and connect
- await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL);
-
- await driver.clickElement('#connectButton');
-
- await driver.delay(500);
-
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
-
- await driver.delay(500);
-
- await driver.clickElement({
- text: 'Connect',
- tag: 'button',
- });
-
- await driver.findElement({ text: 'Add to MetaMask', tag: 'h3' });
-
- await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200);
-
- await driver.clickElement({
- text: 'Confirm',
- tag: 'button',
- });
-
- await driver.clickElementAndWaitForWindowToClose({
- text: 'OK',
- tag: 'button',
- });
-
- await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp);
-
- await driver.waitForSelector({
- text: 'Connected',
- tag: 'span',
- });
-
- if (isAsyncFlow) {
- await toggleAsyncFlow(driver);
- }
-}
-
-async function toggleAsyncFlow(driver: Driver) {
- await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp);
-
- await driver.clickElement('[data-testid="use-sync-flow-toggle"]');
-}
-
-export async function importKeyAndSwitch(driver: Driver) {
- await driver.clickElement({
- text: 'Import account',
- tag: 'div',
- });
-
- await driver.fill('#import-account-private-key', PRIVATE_KEY_TWO);
-
- await driver.clickElement({
- text: 'Import Account',
- tag: 'button',
- });
-
- // Click "Create" on the Snap's confirmation popup
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- await driver.clickElement({
- css: '[data-testid="confirmation-submit-button"]',
- text: 'Create',
- });
- // Click the add account button on the naming modal
- await driver.clickElement({
- css: '[data-testid="submit-add-account-with-name"]',
- text: 'Add account',
- });
- // Click the ok button on the success modal
- await driver.clickElement({
- css: '[data-testid="confirmation-submit-button"]',
- text: 'Ok',
- });
- await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp);
-
- await switchToAccount2(driver);
-}
-
-export async function makeNewAccountAndSwitch(driver: Driver) {
- await driver.clickElement({
- text: 'Create account',
- tag: 'div',
- });
-
- await driver.clickElement({
- text: 'Create Account',
- tag: 'button',
- });
-
- // Click "Create" on the Snap's confirmation popup
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- await driver.clickElement({
- css: '[data-testid="confirmation-submit-button"]',
- text: 'Create',
- });
- // Click the add account button on the naming modal
- await driver.clickElement({
- css: '[data-testid="submit-add-account-with-name"]',
- text: 'Add account',
- });
- // Click the ok button on the success modal
- await driver.clickElementAndWaitForWindowToClose({
- css: '[data-testid="confirmation-submit-button"]',
- text: 'Ok',
- });
- await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp);
-
- const newPublicKey = await (
- await driver.findElement({
- text: '0x',
- tag: 'p',
- })
- ).getText();
-
- await switchToAccount2(driver);
-
- return newPublicKey;
-}
-
-async function switchToAccount2(driver: Driver) {
- await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView);
-
- // click on Accounts
- await driver.clickElement('[data-testid="account-menu-icon"]');
-
- await driver.clickElement({
- tag: 'Button',
- text: 'SSK Account',
- });
-
- await driver.assertElementNotPresent({
- tag: 'header',
- text: 'Select an account',
- });
-}
-
-export async function connectAccountToTestDapp(driver: Driver) {
- await switchToOrOpenDapp(driver);
- await driver.clickElement('#connectButton');
-
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
-
- // Extra steps needed to preserve the current network.
- // Those can be removed once the issue is fixed (#27891)
- const edit = await driver.findClickableElements({
- text: 'Edit',
- tag: 'button',
- });
- await edit[1].click();
-
- await driver.clickElement({
- tag: 'p',
- text: 'Localhost 8545',
- });
-
- await driver.clickElement({
- text: 'Update',
- tag: 'button',
- });
-
- // Connect to the test dapp
- await driver.clickElement({
- text: 'Connect',
- tag: 'button',
- });
-
- await driver.switchToWindowWithUrl(DAPP_URL);
- // Ensure network is preserved after connecting
- await driver.waitForSelector({
- css: '[id="chainId"]',
- text: '0x539',
- });
-}
-
-export async function disconnectFromTestDapp(driver: Driver) {
- await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView);
- await driver.clickElement('[data-testid="account-options-menu-button"]');
- await driver.clickElement({ text: 'All Permissions', tag: 'div' });
- await driver.clickElementAndWaitToDisappear({
- text: 'Got it',
- tag: 'button',
- });
- await driver.clickElement({
- text: '127.0.0.1:8080',
- tag: 'p',
- });
- await driver.clickElement({ text: 'Disconnect', tag: 'button' });
- await driver.clickElement('[data-testid ="disconnect-all"]');
-}
-
-export async function approveOrRejectRequest(driver: Driver, flowType: string) {
- await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp);
-
- await driver.clickElementUsingMouseMove({
- text: 'List requests',
- tag: 'div',
- });
-
- await driver.clickElement({
- text: 'List Requests',
- tag: 'button',
- });
-
- // get the JSON from the screen
- const requestJSON = await (
- await driver.findElement({
- text: '"scope":',
- tag: 'div',
- })
- ).getText();
-
- const requestID = JSON.parse(requestJSON)[0].id;
-
- if (flowType === 'approve') {
- await driver.clickElementUsingMouseMove({
- text: 'Approve request',
- tag: 'div',
- });
-
- await driver.fill('#approve-request-request-id', requestID);
-
- await driver.clickElement({
- text: 'Approve Request',
- tag: 'button',
- });
- } else if (flowType === 'reject') {
- await driver.clickElementUsingMouseMove({
- text: 'Reject request',
- tag: 'div',
- });
-
- await driver.fill('#reject-request-request-id', requestID);
-
- await driver.clickElement({
- text: 'Reject Request',
- tag: 'button',
- });
- }
-
- // Close the SnapSimpleKeyringDapp, so that 6 of the same tab doesn't pile up
- await driver.closeWindow();
-
- await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView);
-}
-
-export async function signData(
- driver: Driver,
- locatorID: string,
- newPublicKey: string,
- flowType: string,
-) {
- const isAsyncFlow = flowType !== 'sync';
-
- // This step can frequently fail, so retry it
- await retry(
- {
- retries: 3,
- delay: 2000,
- },
- async () => {
- await switchToOrOpenDapp(driver);
- await driver.clickElement(locatorID);
-
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- },
- );
-
- // these three don't have a contract details page
- if (!['#ethSign', '#personalSign', '#signTypedData'].includes(locatorID)) {
- await validateContractDetails(driver);
- }
-
- await clickSignOnSignatureConfirmation({ driver });
-
- if (isAsyncFlow) {
- await driver.delay(2000);
-
- // This step can frequently fail, so retry it
- await retry(
- {
- retries: 3,
- delay: 1000,
- },
- async () => {
- // Navigate to the Notification window and click 'Go to site' button
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- await driver.clickElement({
- text: 'Go to site',
- tag: 'button',
- });
- },
- );
-
- await driver.delay(1000);
- await approveOrRejectRequest(driver, flowType);
- }
-
- await driver.delay(500);
- await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
-
- if (flowType === 'sync' || flowType === 'approve') {
- if (locatorID === '#ethSign') {
- // there is no Verify button for #ethSign
- await driver.findElement({
- css: '#ethSignResult',
- text: '0x', // we are just making sure that it contains ANY hex value
- });
- } else {
- await driver.clickElement(`${locatorID}Verify`);
-
- const resultLocator =
- locatorID === '#personalSign'
- ? '#personalSignVerifyECRecoverResult' // the verify span IDs are different with Personal Sign
- : `${locatorID}VerifyResult`;
-
- await driver.findElement({
- css: resultLocator,
- text: newPublicKey.toLowerCase(),
- });
- }
- } else if (flowType === 'reject') {
- // ensure the transaction was rejected by the Snap
- await driver.findElement({
- text: 'Error: Request rejected by user or snap.',
- });
- }
-}
-
-export async function createBtcAccount(driver: Driver) {
- await driver.clickElement('[data-testid="account-menu-icon"]');
- await driver.clickElement(
- '[data-testid="multichain-account-menu-popover-action-button"]',
- );
- await driver.clickElement({
- text: messages.addNewBitcoinAccount.message,
- tag: 'button',
- });
- await driver.clickElementAndWaitToDisappear(
- {
- text: 'Add account',
- tag: 'button',
- },
- // Longer timeout than usual, this reduces the flakiness
- // around Bitcoin account creation (mainly required for
- // Firefox)
- 5000,
- );
-}
diff --git a/test/e2e/accounts/snap-account-contract-interaction.spec.ts b/test/e2e/accounts/snap-account-contract-interaction.spec.ts
deleted file mode 100644
index 885e048272d8..000000000000
--- a/test/e2e/accounts/snap-account-contract-interaction.spec.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import GanacheContractAddressRegistry from '../seeder/ganache-contract-address-registry';
-import { scrollAndConfirmAndAssertConfirm } from '../tests/confirmations/helpers';
-import {
- createDepositTransaction,
- TestSuiteArguments,
-} from '../tests/confirmations/transactions/shared';
-import {
- multipleGanacheOptionsForType2Transactions,
- withFixtures,
- openDapp,
- WINDOW_TITLES,
- locateAccountBalanceDOM,
- clickNestedButton,
- ACCOUNT_2,
-} from '../helpers';
-import FixtureBuilder from '../fixture-builder';
-import { installSnapSimpleKeyring } from '../page-objects/flows/snap-simple-keyring.flow';
-import { loginWithBalanceValidation } from '../page-objects/flows/login.flow';
-import { SMART_CONTRACTS } from '../seeder/smart-contracts';
-import { importKeyAndSwitch } from './common';
-
-describe('Snap Account Contract interaction', function () {
- const smartContract = SMART_CONTRACTS.PIGGYBANK;
-
- it('deposits to piggybank contract', async function () {
- await withFixtures(
- {
- dapp: true,
- fixtures: new FixtureBuilder()
- .withPermissionControllerSnapAccountConnectedToTestDapp()
- .withPreferencesController({
- preferences: {
- redesignedConfirmationsEnabled: true,
- isRedesignedConfirmationsDeveloperEnabled: true,
- },
- })
- .build(),
- ganacheOptions: multipleGanacheOptionsForType2Transactions,
- smartContract,
- title: this.test?.fullTitle(),
- },
- async ({
- driver,
- contractRegistry,
- ganacheServer,
- }: TestSuiteArguments) => {
- // Install Snap Simple Keyring and import key
- await loginWithBalanceValidation(driver, ganacheServer);
- await installSnapSimpleKeyring(driver);
- await importKeyAndSwitch(driver);
-
- // Open DApp with contract
- const contractAddress = await (
- contractRegistry as GanacheContractAddressRegistry
- ).getContractAddress(smartContract);
- await openDapp(driver, contractAddress);
-
- // Create and confirm deposit transaction
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.ExtensionInFullScreenView,
- );
- await createDepositTransaction(driver);
- await driver.waitUntilXWindowHandles(4);
- await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
- await driver.waitForSelector({
- css: 'h2',
- text: 'Transaction request',
- });
- await scrollAndConfirmAndAssertConfirm(driver);
-
- // Confirm the transaction activity
- await driver.waitUntilXWindowHandles(3);
- await driver.switchToWindowWithTitle(
- WINDOW_TITLES.ExtensionInFullScreenView,
- );
- await clickNestedButton(driver, 'Activity');
- await driver.waitForSelector(
- '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)',
- );
- await driver.waitForSelector({
- css: '[data-testid="transaction-list-item-primary-currency"]',
- text: '-4 ETH',
- });
-
- // renders the correct ETH balance
- await locateAccountBalanceDOM(driver, ganacheServer, ACCOUNT_2);
- },
- );
- });
-});
diff --git a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts
deleted file mode 100644
index 24e996671da9..000000000000
--- a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Suite } from 'mocha';
-import FixtureBuilder from '../fixture-builder';
-import {
- withFixtures,
- multipleGanacheOptions,
- tempToggleSettingRedesignedConfirmations,
-} from '../helpers';
-import { Driver } from '../webdriver/driver';
-import {
- installSnapSimpleKeyring,
- makeNewAccountAndSwitch,
- connectAccountToTestDapp,
- disconnectFromTestDapp,
- signData,
-} from './common';
-
-describe('Snap Account Signatures and Disconnects', function (this: Suite) {
- it('can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)', async function () {
- await withFixtures(
- {
- dapp: true,
- fixtures: new FixtureBuilder().build(),
- ganacheOptions: multipleGanacheOptions,
- title: this.test?.fullTitle(),
- },
- async ({ driver }: { driver: Driver }) => {
- const flowType = 'approve';
- const isAsyncFlow = true;
-
- await installSnapSimpleKeyring(driver, isAsyncFlow);
-
- const newPublicKey = await makeNewAccountAndSwitch(driver);
-
- await tempToggleSettingRedesignedConfirmations(driver);
-
- // open the Test Dapp and connect Account 2 to it
- await connectAccountToTestDapp(driver);
-
- // do #signTypedDataV3
- await signData(driver, '#signTypedDataV3', newPublicKey, flowType);
-
- // disconnect from the Test Dapp
- await disconnectFromTestDapp(driver);
-
- // reconnect Account 2 to the Test Dapp
- await connectAccountToTestDapp(driver);
-
- // do #signTypedDataV4
- await signData(driver, '#signTypedDataV4', newPublicKey, flowType);
- },
- );
- });
-});
diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts
index 15bf7d49eb0b..6891b3bfd60e 100644
--- a/test/e2e/flask/btc/common-btc.ts
+++ b/test/e2e/flask/btc/common-btc.ts
@@ -4,7 +4,28 @@ import { withFixtures, unlockWallet } from '../../helpers';
import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants';
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
import { Driver } from '../../webdriver/driver';
-import { createBtcAccount } from '../../accounts/common';
+import messages from '../../../../app/_locales/en/messages.json';
+
+export async function createBtcAccount(driver: Driver) {
+ await driver.clickElement('[data-testid="account-menu-icon"]');
+ await driver.clickElement(
+ '[data-testid="multichain-account-menu-popover-action-button"]',
+ );
+ await driver.clickElement({
+ text: messages.addNewBitcoinAccount.message,
+ tag: 'button',
+ });
+ await driver.clickElementAndWaitToDisappear(
+ {
+ text: 'Add account',
+ tag: 'button',
+ },
+ // Longer timeout than usual, this reduces the flakiness
+ // around Bitcoin account creation (mainly required for
+ // Firefox)
+ 5000,
+ );
+}
export async function mockBtcBalanceQuote(
mockServer: Mockttp,
diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts
index a4ac650f8f78..1b10599bf5ca 100644
--- a/test/e2e/flask/btc/create-btc-account.spec.ts
+++ b/test/e2e/flask/btc/create-btc-account.spec.ts
@@ -10,8 +10,7 @@ import {
removeSelectedAccount,
tapAndHoldToRevealSRP,
} from '../../helpers';
-import { createBtcAccount } from '../../accounts/common';
-import { withBtcAccountSnap } from './common-btc';
+import { createBtcAccount, withBtcAccountSnap } from './common-btc';
describe('Create BTC Account', function (this: Suite) {
it('create BTC account from the menu', async function () {
diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts
index c0f71f1b3280..0940225b5e3b 100644
--- a/test/e2e/page-objects/pages/test-dapp.ts
+++ b/test/e2e/page-objects/pages/test-dapp.ts
@@ -1,6 +1,5 @@
import { WINDOW_TITLES } from '../../helpers';
import { Driver } from '../../webdriver/driver';
-import { RawLocator } from '../common';
const DAPP_HOST_ADDRESS = '127.0.0.1:8080';
const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`;
@@ -8,22 +7,55 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`;
class TestDapp {
private driver: Driver;
+ private readonly confirmDepositButton =
+ '[data-testid="confirm-footer-button"]';
+
+ private readonly confirmDialogButton = '[data-testid="confirm-btn"]';
+
private readonly confirmDialogScrollButton =
'[data-testid="signature-request-scroll-button"]';
private readonly confirmSignatureButton =
'[data-testid="page-container-footer-next"]';
+ private readonly connectAccountButton = '#connectButton';
+
+ private readonly connectMetaMaskMessage = {
+ text: 'Connect with MetaMask',
+ tag: 'h2',
+ };
+
+ private readonly connectedAccount = '#accounts';
+
+ private readonly depositPiggyBankContractButton = '#depositButton';
+
+ private readonly editConnectButton = {
+ text: 'Edit',
+ tag: 'button',
+ };
+
private readonly erc1155RevokeSetApprovalForAllButton =
'#revokeERC1155Button';
private readonly erc1155SetApprovalForAllButton =
'#setApprovalForAllERC1155Button';
+ private readonly erc20WatchAssetButton = '#watchAssets';
+
private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton';
private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton';
+ private readonly localhostCheckbox = {
+ text: 'Localhost 8545',
+ tag: 'p',
+ };
+
+ private readonly localhostNetworkMessage = {
+ css: '#chainId',
+ text: '0x539',
+ };
+
private readonly mmlogo = '#mm-logo';
private readonly personalSignButton = '#personalSign';
@@ -37,6 +69,8 @@ class TestDapp {
private readonly personalSignVerifyButton = '#personalSignVerify';
+ private readonly revokePermissionButton = '#revokeAccountsPermission';
+
private readonly signPermitButton = '#signPermit';
private readonly signPermitResult = '#signPermitResult';
@@ -84,16 +118,18 @@ class TestDapp {
private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult';
- private erc20WatchAssetButton: RawLocator;
+ private readonly transactionRequestMessage = {
+ text: 'Transaction request',
+ tag: 'h2',
+ };
+
+ private readonly updateNetworkButton = {
+ text: 'Update',
+ tag: 'button',
+ };
constructor(driver: Driver) {
this.driver = driver;
-
- this.erc721SetApprovalForAllButton = '#setApprovalForAllButton';
- this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button';
- this.erc721RevokeSetApprovalForAllButton = '#revokeButton';
- this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button';
- this.erc20WatchAssetButton = '#watchAssets';
}
async check_pageIsLoaded(): Promise {
@@ -156,6 +192,63 @@ class TestDapp {
await this.driver.clickElement(this.erc20WatchAssetButton);
}
+ /**
+ * Connect account to test dapp.
+ *
+ * @param publicAddress - The public address to connect to test dapp.
+ */
+ async connectAccount(publicAddress: string) {
+ console.log('Connect account to test dapp');
+ await this.driver.clickElement(this.connectAccountButton);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(this.connectMetaMaskMessage);
+
+ // TODO: Extra steps needed to preserve the current network.
+ // Following steps can be removed once the issue is fixed (#27891)
+ const editNetworkButton = await this.driver.findClickableElements(
+ this.editConnectButton,
+ );
+ await editNetworkButton[1].click();
+ await this.driver.clickElement(this.localhostCheckbox);
+ await this.driver.clickElement(this.updateNetworkButton);
+
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmDialogButton,
+ );
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
+ await this.driver.waitForSelector({
+ css: this.connectedAccount,
+ text: publicAddress.toLowerCase(),
+ });
+ await this.driver.waitForSelector(this.localhostNetworkMessage);
+ }
+
+ async createDepositTransaction() {
+ console.log('Create a deposit transaction on test dapp page');
+ await this.driver.clickElement(this.depositPiggyBankContractButton);
+ await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
+ await this.driver.waitForSelector(this.transactionRequestMessage);
+ await this.driver.clickElementAndWaitForWindowToClose(
+ this.confirmDepositButton,
+ );
+ }
+
+ /**
+ * Disconnect current connected account from test dapp.
+ *
+ * @param publicAddress - The public address of the account to disconnect from test dapp.
+ */
+ async disconnectAccount(publicAddress: string) {
+ console.log('Disconnect account from test dapp');
+ await this.driver.clickElement(this.revokePermissionButton);
+ await this.driver.refresh();
+ await this.check_pageIsLoaded();
+ await this.driver.assertElementNotPresent({
+ css: this.connectedAccount,
+ text: publicAddress.toLowerCase(),
+ });
+ }
+
/**
* Verify the failed personal sign signature.
*
diff --git a/test/e2e/tests/account/snap-account-contract-interaction.spec.ts b/test/e2e/tests/account/snap-account-contract-interaction.spec.ts
new file mode 100644
index 000000000000..e4753f5ff05b
--- /dev/null
+++ b/test/e2e/tests/account/snap-account-contract-interaction.spec.ts
@@ -0,0 +1,83 @@
+import { Suite } from 'mocha';
+import { Driver } from '../../webdriver/driver';
+import FixtureBuilder from '../../fixture-builder';
+import { Ganache } from '../../seeder/ganache';
+import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry';
+import {
+ multipleGanacheOptionsForType2Transactions,
+ PRIVATE_KEY_TWO,
+ withFixtures,
+ WINDOW_TITLES,
+} from '../../helpers';
+import { SMART_CONTRACTS } from '../../seeder/smart-contracts';
+import HeaderNavbar from '../../page-objects/pages/header-navbar';
+import HomePage from '../../page-objects/pages/homepage';
+import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page';
+import TestDapp from '../../page-objects/pages/test-dapp';
+import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow';
+import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
+
+describe('Snap Account Contract interaction @no-mmi', function (this: Suite) {
+ const smartContract = SMART_CONTRACTS.PIGGYBANK;
+ it('deposits to piggybank contract', async function () {
+ await withFixtures(
+ {
+ dapp: true,
+ fixtures: new FixtureBuilder()
+ .withPermissionControllerSnapAccountConnectedToTestDapp()
+ .withPreferencesController({
+ preferences: {
+ redesignedConfirmationsEnabled: true,
+ isRedesignedConfirmationsDeveloperEnabled: true,
+ },
+ })
+ .build(),
+ ganacheOptions: multipleGanacheOptionsForType2Transactions,
+ smartContract,
+ title: this.test?.fullTitle(),
+ },
+ async ({
+ driver,
+ contractRegistry,
+ ganacheServer,
+ }: {
+ driver: Driver;
+ contractRegistry: GanacheContractAddressRegistry;
+ ganacheServer?: Ganache;
+ }) => {
+ await loginWithBalanceValidation(driver, ganacheServer);
+ await installSnapSimpleKeyring(driver);
+ const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver);
+
+ // Import snap account with private key on snap simple keyring page.
+ await snapSimpleKeyringPage.importAccountWithPrivateKey(
+ PRIVATE_KEY_TWO,
+ );
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.check_accountLabel('SSK Account');
+
+ // Open Dapp with contract
+ const testDapp = new TestDapp(driver);
+ const contractAddress = await (
+ contractRegistry as GanacheContractAddressRegistry
+ ).getContractAddress(smartContract);
+ await testDapp.openTestDappPage({ contractAddress });
+ await testDapp.check_pageIsLoaded();
+ await testDapp.createDepositTransaction();
+
+ // Confirm the transaction in activity list on MetaMask
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
+ const homePage = new HomePage(driver);
+ await homePage.check_pageIsLoaded();
+ await homePage.goToActivityList();
+ await homePage.check_confirmedTxNumberDisplayedInActivity();
+ await homePage.check_txAmountInActivity('-4 ETH');
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts
new file mode 100644
index 000000000000..7398747671c7
--- /dev/null
+++ b/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts
@@ -0,0 +1,66 @@
+import { Suite } from 'mocha';
+import { Driver } from '../../webdriver/driver';
+import { WINDOW_TITLES, withFixtures } from '../../helpers';
+import FixtureBuilder from '../../fixture-builder';
+import ExperimentalSettings from '../../page-objects/pages/experimental-settings';
+import HeaderNavbar from '../../page-objects/pages/header-navbar';
+import SettingsPage from '../../page-objects/pages/settings-page';
+import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page';
+import TestDapp from '../../page-objects/pages/test-dapp';
+import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow';
+import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
+import {
+ signTypedDataV3WithSnapAccount,
+ signTypedDataV4WithSnapAccount,
+} from '../../page-objects/flows/sign.flow';
+
+describe('Snap Account Signatures and Disconnects @no-mmi', function (this: Suite) {
+ it('can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)', async function () {
+ await withFixtures(
+ {
+ dapp: true,
+ fixtures: new FixtureBuilder()
+ .withPermissionControllerConnectedToTestDapp({
+ restrictReturnedAccounts: false,
+ })
+ .build(),
+ title: this.test?.fullTitle(),
+ },
+ async ({ driver }: { driver: Driver }) => {
+ await loginWithBalanceValidation(driver);
+ await installSnapSimpleKeyring(driver, false);
+ const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver);
+ const newPublicKey = await snapSimpleKeyringPage.createNewAccount();
+
+ // Check snap account is displayed after adding the snap account.
+ await driver.switchToWindowWithTitle(
+ WINDOW_TITLES.ExtensionInFullScreenView,
+ );
+ const headerNavbar = new HeaderNavbar(driver);
+ await headerNavbar.check_accountLabel('SSK Account');
+
+ // Navigate to experimental settings and disable redesigned signature.
+ await headerNavbar.openSettingsPage();
+ const settingsPage = new SettingsPage(driver);
+ await settingsPage.check_pageIsLoaded();
+ await settingsPage.goToExperimentalSettings();
+
+ const experimentalSettings = new ExperimentalSettings(driver);
+ await experimentalSettings.check_pageIsLoaded();
+ await experimentalSettings.toggleRedesignedSignature();
+
+ // Open the Test Dapp and signTypedDataV3
+ const testDapp = new TestDapp(driver);
+ await testDapp.openTestDappPage();
+ await signTypedDataV3WithSnapAccount(driver, newPublicKey, false, true);
+
+ // Disconnect from Test Dapp and reconnect to Test Dapp
+ await testDapp.disconnectAccount(newPublicKey);
+ await testDapp.connectAccount(newPublicKey);
+
+ // SignTypedDataV4 with Test Dapp
+ await signTypedDataV4WithSnapAccount(driver, newPublicKey, false, true);
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts
index f5010fb61269..fd2fe013c3c1 100644
--- a/test/e2e/tests/account/snap-account-signatures.spec.ts
+++ b/test/e2e/tests/account/snap-account-signatures.spec.ts
@@ -18,6 +18,7 @@ import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-
import TestDapp from '../../page-objects/pages/test-dapp';
describe('Snap Account Signatures @no-mmi', function (this: Suite) {
+ this.timeout(120000); // This test is very long, so we need an unusually high timeout
// Run sync, async approve, and async reject flows
// (in Jest we could do this with test.each, but that does not exist here)
From ce8eeb1818acaffd4425ce2ddd7c93fbaf07e007 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Tavares?=
Date: Fri, 18 Oct 2024 11:25:06 +0100
Subject: [PATCH 35/51] chore: add testing-library/dom dependency (#27493)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
We are adding
[testing-library/dom](https://www.npmjs.com/package/@testing-library/dom)
as direct dependency in order to start leveraging the testing library
[testing playground](https://testing-playground.com/). (As a note,
latest versions of `testing-library/react` have `testing-library/dom` as
a peer dep instead of a dependency. Though we are not updating
`testing-library/react` to the latest version as it requires React 18
and we are still in React 16).
Using the `screen` from the `dom` library we can now use
`screen.logTestingPlaygroundURL()` (with or without a specific section
passed into it) and when running the test it will log an url in the
console for that specific component in the testing playground. We can
use that to validate what is getting rendered and what is the suggested
query to infer and write expectations on certain elements/text/values.
Example of a logged url:
https://testing-playground.com/#markup=DwEwlgbgBAxgNgQwM5ILwCIC2mC0AjAewA9YCA7AMzACdcxKCdqCB3KbfYnHTBagc3o4ALgQAOOAEztchIt14ChhYaNzSOc7mIQhwZfkzD8AFsKkzO8nDr31DcAKYVzG2VxzgkYxAE8cFE4kmh6BjvLg1I4wwmDkTKyWWgFBOCzUCBLpmUkeAFYArkixFP4w5MKOZObeCDCO+I7CLI5VudblcATUIuHmIM4IBXDC7dzMBWQDIDhw-OhQxb5OGAQQjtSBrGkZYgBcUAhkviwmG44A3OxCZ8ZmB5IALGJEV2IESGCx5AdRiLHrC7oAB8oEgsEQKAwIWC7msXh8CH8YVhVm4KM8NGi3zICTYMO4hWKYFKOHK1Sq5nqFOoYxwCDgxlxX0cmCQKXCOGKfGEILB0HgyDQWDhdIRfg5qOSDKZOBZbLJlI2fLEEKF0NFHEqRFGWr63EIIH8mBmmEc4AKmDpnW6crIZ2oXxBAFFibxKiBYCYjvxHEhgAB6MSg8AQEPgwVQ9ALYQIPD0AZEDAABgWSxW6HFSIOKKBUBACFjIgIBBGYDEYnNGAWfDACE8fpgjrw5rwvgwsQr-lEpc7OAAjKn84X691jPQGSIvhnXbF3eavT6-YcolBToXrqZRt6K20SVBfAQClB+AQoMITBNTGuviZzyYwEhzxkyEg6jiAHRQAAqD6fj6gIlRgQKAxCicAYjiMgABooDIAhgJPAo+COSpHA-PlajINUoxhSwwHJfDyW4T4AC8GiQK0CR4PhBFxJwXAHMVH0Rfx6EZMgGjwLoYAAa2tUtbQI+JMAKD002EZZHGhZBeLlXhfQOApqDgAAKAByD8AzABS-W08kkADABHAo-U-JAIH4dSAEogVBAMsPs0MnMgFyw0DZz+Rw4U8OorNkSCOl+EyCxqIxbIsl2OkZX4ZlKgValKlpajMCEFgwBAC8cFTUFVUjHzNVwbVdSK-VOCNHhPWom0enoB0nUWSSM1OFkuR0eoDjAhoIrsgBNI91NXQQIHsRYCDNchHCgRw4CQKbK1oR9PnIc8z28KpPQvACEEwI9qigChmCtQ9lMOGBykmYQP0DYMPNcu73NDbyNSsUhKBoOgGDxOlFDo4sJDcNEaKUXEVDUUK4W0XR9EMR0twhoHbBh2ZnFcZjvAlFE6QxSJsSg76wtSCKdhyaigJJMoKkpNq6i4poWjaarBJ6YqGwoIYRjpCYpnNWZ5kaqTVnWTYuhYEn9kOY5TnOK5UtxW4tweZ5XlAj4vig34ZsLSBLj5J78peuR0dYyVsdSXHIPiZh8UhnBydJclKmqRUaWixlYrleL2QxblqF5cMBUhArXr8ljMcC6iYri1l2US5VcuekUQ9KnVLFZ8rjVNc0wEtASulq+0NidYEAGVKymUaYEyG63NrryDaTuQJMFrBaKEBjhAOIZRFlhB5HSzKTAOQdkwAUjs+ug8N4hjfD8I3dleVY6Vag9YjKfG5nlK2+ZMgOIaDamNDjGkTtfefrSjKspy4A8o33yU5K3odQNAgKpNNOytZqOXaSukkfsGfegDRAbSndtHBKK8862lZgMdmwwn7JDwHUXi-BuYzBqvgFBaC9ozAZElMg2t1hc1wbzIgcAFgFiLJUYkIAMCfFEv8fGoheJVBwBABkpkQT9hrg9AOAdE6ELNHBHajgAD6YjUooHsJhHQ2EG54SEeIsRwlsIcFUUReIXIwDkUqrPU+7FgH4B4vxJm+c7T1V5ALDMvAkByR0ggRSUBlJqU0tpXShlVGGRMmZKCH4LJWVsnyByciE4KMKs-UYSiJEcLgKZT+L8M56LMTA-UcCOb+2TEQZMAB2AAbP2R4H5ikAGZdB5IABy8IDJ5Gp906nuQabXIAA
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27493?quickstart=1)
## **Related issues**
Fixes: None
## **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.
---
.depcheckrc.yml | 1 +
package.json | 1 +
.../__snapshots__/nft-details.test.js.snap | 180 -----
.../__snapshots__/nft-full-image.test.js.snap | 2 -
.../connect-accounts-modal.test.tsx.snap | 8 -
.../confirm-legacy-gas-display.test.js.snap | 2 -
.../base-transaction-info.test.tsx.snap | 747 ------------------
.../create-named-snap-account.test.js.snap | 100 ---
.../switch-ethereum-chain.test.js.snap | 2 -
...ctive-replacement-token-page.test.tsx.snap | 54 --
.../confirm-recovery-phrase.test.js | 14 +-
yarn.lock | 76 +-
12 files changed, 50 insertions(+), 1137 deletions(-)
diff --git a/.depcheckrc.yml b/.depcheckrc.yml
index bafacc56c918..d0d6eac5b5bc 100644
--- a/.depcheckrc.yml
+++ b/.depcheckrc.yml
@@ -80,6 +80,7 @@ ignores:
- '@babel/plugin-transform-logical-assignment-operators'
# trezor
- 'ts-mixer'
+ - '@testing-library/dom'
# files depcheck should not parse
ignorePatterns:
diff --git a/package.json b/package.json
index 873e3ad6324c..3c1ea2bfbbba 100644
--- a/package.json
+++ b/package.json
@@ -506,6 +506,7 @@
"@storybook/test-runner": "^0.14.1",
"@storybook/theming": "^7.6.20",
"@swc/helpers": "^0.5.7",
+ "@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^10.4.8",
"@testing-library/react-hooks": "^8.0.1",
diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap
index 22c5342d2026..d6b0de0c043e 100644
--- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap
+++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap
@@ -182,183 +182,3 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = `
`;
-
-exports[`NFT Details should match minimal props and state snapshot 2`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- MUNK #1
-
-
-
-
-
-
- Contract address
-
-
-
-
-
-
-
-
-
- Token standard
-
-
- ERC721
-
-
-
-
-
-
-
- Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted.
-
-
-
-
-
-
-
-`;
-
-exports[`NFT Details should match minimal props and state snapshot 3`] = ``;
diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap
index 086754f9b489..7b4d6b11abc6 100644
--- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap
+++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap
@@ -80,5 +80,3 @@ exports[`NFT full image should match snapshot 1`] = `
`;
-
-exports[`NFT full image should match snapshot 2`] = ``;
diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap
index caf13117e1db..d53c8e7d8d8a 100644
--- a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap
+++ b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap
@@ -442,11 +442,3 @@ exports[`Connect More Accounts Modal should render correctly 1`] = `
-
-