From fad21cddbbe7ca5bb1e8333ff92dfcd490945075 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Tue, 18 Feb 2025 19:32:05 -0500 Subject: [PATCH 01/14] refactor: remove circular dependency in `token-buttons.tsx` (#30299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR solves an issue with circular dependencies. It does not change the behavior of the application. It only rearranges functions/files to avoid circular references. --- development/circular-deps.jsonc | 4 ---- ui/pages/asset/components/token-buttons.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/development/circular-deps.jsonc b/development/circular-deps.jsonc index dcd08ec5b46d..1fa13db041a0 100644 --- a/development/circular-deps.jsonc +++ b/development/circular-deps.jsonc @@ -48,10 +48,6 @@ "ui/components/multichain/pages/send/components/index.ts", "ui/components/multichain/pages/send/components/your-accounts.tsx" ], - [ - "ui/pages/asset/components/asset-page.tsx", - "ui/pages/asset/components/token-buttons.tsx" - ], [ "ui/pages/notifications/notifications-list.tsx", "ui/pages/notifications/notifications.tsx" diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index 9730a23a7475..c92f1c6e877b 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -56,7 +56,7 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF -import { Asset } from './asset-page'; +import type { Asset } from './asset-page'; const TokenButtons = ({ token, From 159a416b114c8f8e76a1a2100af00353b1047e05 Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Wed, 19 Feb 2025 09:37:02 +0000 Subject: [PATCH 02/14] docs: add code comments to better describe date formatting util (#29242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** I'm adding these utils for mobile, and we didn't really explain in tests and code comments what each of the date formatting sections do. This adds code comments on both tests and code to make it clear what the date formatting does. FUTURE NOTE: It would be nice to extract this into CORE to be reused by extension and mobile. However mobile does not support `Intl.DateTimeFormat` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29242?quickstart=1) ## **Related issues** Fixes: N/A ## **Manual testing steps** N/A - we are just adding docs/code comments. ## **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/main/.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/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/helpers/utils/notification.util.ts | 4 ++-- ui/helpers/utils/notification.utils.test.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/helpers/utils/notification.util.ts b/ui/helpers/utils/notification.util.ts index afbba2b88172..9f57bce5e2e2 100644 --- a/ui/helpers/utils/notification.util.ts +++ b/ui/helpers/utils/notification.util.ts @@ -102,7 +102,7 @@ export function formatMenuItemDate(date: Date) { ); } - // E.g. 21 Oct + // E.g. Oct 21 if (isSameYear(currentDate, date)) { return new Intl.DateTimeFormat('en', { month: 'short', @@ -110,7 +110,7 @@ export function formatMenuItemDate(date: Date) { }).format(date); } - // E.g. 21 Oct 2022 + // E.g. Oct 21, 2022 return new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', diff --git a/ui/helpers/utils/notification.utils.test.ts b/ui/helpers/utils/notification.utils.test.ts index 2f4e66c26504..920262440378 100644 --- a/ui/helpers/utils/notification.utils.test.ts +++ b/ui/helpers/utils/notification.utils.test.ts @@ -20,7 +20,7 @@ describe('formatMenuItemDate', () => { const assertToday = (modifyDate?: (d: Date) => void) => { const testDate = new Date(); modifyDate?.(testDate); - expect(formatMenuItemDate(testDate)).toMatch(/^\d{2}:\d{2}$/u); + expect(formatMenuItemDate(testDate)).toMatch(/^\d{2}:\d{2}$/u); // E.g. HH:mm }; // assert current date @@ -53,11 +53,11 @@ describe('formatMenuItemDate', () => { }); }); - it('should format date as "DD Mon" if the date is this year but not today or yesterday', () => { + it('should format date as "MM DD" if the date is this year but not today or yesterday', () => { const assertMonthsAgo = (modifyDate: (d: Date) => Date | void) => { let testDate = new Date(); testDate = modifyDate(testDate) ?? testDate; - expect(formatMenuItemDate(testDate)).toMatch(/^\w{3} \d{1,2}$/u); + expect(formatMenuItemDate(testDate)).toMatch(/^\w{3} \d{1,2}$/u); // E.g. Oct 21 }; // assert exactly 1 month ago @@ -77,11 +77,11 @@ describe('formatMenuItemDate', () => { }); }); - it('should format date as "Mon DD, YYYY" if the date is not this year', () => { + it('should format date as "MM DD, YYYY" if the date is not this year', () => { const assertYearsAgo = (modifyDate: (d: Date) => Date | void) => { let testDate = new Date(); testDate = modifyDate(testDate) ?? testDate; - expect(formatMenuItemDate(testDate)).toMatch(/^\w{3} \d{1,2}, \d{4}$/u); + expect(formatMenuItemDate(testDate)).toMatch(/^\w{3} \d{1,2}, \d{4}$/u); // E.g. Oct 21, 2022 }; // assert exactly 1 year ago From af66eb646f727a195b9f4aed5087c284560162cf Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Wed, 19 Feb 2025 07:45:11 -0300 Subject: [PATCH 03/14] feat(action): Improve bug report creation (#30176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The purpose of this improvement is to automate several steps that I currently do manually when a new release x.y.z is created: 1. Create regression labels on public code repo (metamask-extension) and private planning repo (MetaMask-planning) a. `regression-RC-x.y.z` b. `regression-prod-x.y.z` 2. Create bug report issue on planning repo 3. Add the bug report issue to the Releases Github Project board 4. Set the RC cut date on the Releases Github Project board. These steps are important because the data pipelines of our metrics system consume the data that's present on the [Releases Github Project board](https://github.com/orgs/MetaMask/projects/86/views/1). Finally this improvement includes a check to no longer re-create the bug report issue when it already exists, which used to happen sometimes when the release was re-cut, and which was disturbing other automations (e.g. wrong metrics, duplicated Slack notifications). The following prerequisites are already met: - [x] Add `BUG_REPORT_TOKEN` to repo secrets (fine grained access token with `Issues:Write` and `Metadata:Read` permissions for metamask-extension and MetaMask-planning repos, as well as `Projects: Write` permissions for MetaMask organization) - [x] Add `RELEASES_GITHUB_PROJECT_BOARD_NUMBER` to repo variables - [x] Add `RELEASES_GITHUB_PROJECT_BOARD_VIEW_NUMBER` to repo variables [Same PR for Mobile](https://github.com/MetaMask/metamask-mobile/pull/13397) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30176?quickstart=1) ## **Related issues** None ## **Manual testing steps** 1. Go to this [public code repo](https://github.com/gauthierpetetin-test/repo_test) (equivalent of metamask-extension repo) 2. Create a [new branch](https://github.com/gauthierpetetin-test/repo_test/branches) with the following format: `release/x.y.z` (where x, y, z, are numbers) 3. Wait for 30s, until the Github action execution is finalised 4. Check that `regression-RC-x.y.z` and r`egression-prod-x.y.z` labels have been created on the [public code repo](https://github.com/gauthierpetetin-test/repo_test/labels) 5. Go to this [private planning repo](https://github.com/gauthierpetetin-test/repo_test_2) (equivalent of MetaMask-planning repo) 6. Check that `regression-RC-x.y.z` and r`egression-prod-x.y.z` labels have been created on the [private planning repo](https://github.com/gauthierpetetin-test/repo_test_2/labels) 7. Check that the bug report issue has been created on the [private planning repo](https://github.com/gauthierpetetin-test/repo_test_2/issues) and has the following title: "vx.y.z Bug Report" 8. Go to this [Github Project board](https://github.com/users/gauthierpetetin-test/projects/2/views/1) 9. Check that the bug report issue is present on the board and its "RC Cut" date is set to the current date. In case, you don't have sufficient permissions on these test repos, here's a video where the manual testing steps are demoed. ## **Screenshots/Recordings** ### **Before** None ### **After**

Github action to automate bug report creation - Watch Video

## **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/main/.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/main/.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: Harika <153644847+hjetpoluru@users.noreply.github.com> Co-authored-by: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> --- ...d-release-label-to-pr-and-linked-issues.ts | 7 +- .../scripts/check-template-and-add-labels.ts | 66 +---- .github/scripts/create-bug-report-issue.ts | 129 +++++++++ .github/scripts/shared/issue.ts | 222 ++++++++++---- .github/scripts/shared/label.ts | 95 ++++++ .github/scripts/shared/project.ts | 270 ++++++++++++++++++ .github/scripts/shared/repo.ts | 4 + .github/scripts/shared/utils.ts | 50 ++++ .github/workflows/create-bug-report.yml | 32 +-- package.json | 1 + 10 files changed, 734 insertions(+), 142 deletions(-) create mode 100644 .github/scripts/create-bug-report-issue.ts create mode 100644 .github/scripts/shared/project.ts create mode 100644 .github/scripts/shared/utils.ts diff --git a/.github/scripts/add-release-label-to-pr-and-linked-issues.ts b/.github/scripts/add-release-label-to-pr-and-linked-issues.ts index a9b84532c8e6..1c4d551828c3 100644 --- a/.github/scripts/add-release-label-to-pr-and-linked-issues.ts +++ b/.github/scripts/add-release-label-to-pr-and-linked-issues.ts @@ -6,6 +6,7 @@ import { retrieveLinkedIssues } from './shared/issue'; import { Label } from './shared/label'; import { Labelable, addLabelToLabelable } from './shared/labelable'; import { retrievePullRequest } from './shared/pull-request'; +import { isValidVersionFormat } from './shared/utils'; main().catch((error: Error): void => { console.error(error); @@ -90,9 +91,3 @@ async function main(): Promise { await addLabelToLabelable(octokit, linkedIssue, releaseLabel); } } - -// This helper function checks if version has the correct format: "x.y.z" where "x", "y" and "z" are numbers. -function isValidVersionFormat(str: string): boolean { - const regex = /^\d+\.\d+\.\d+$/; - return regex.test(str); -} diff --git a/.github/scripts/check-template-and-add-labels.ts b/.github/scripts/check-template-and-add-labels.ts index 1a2bc0410beb..dd54fe7e849e 100644 --- a/.github/scripts/check-template-and-add-labels.ts +++ b/.github/scripts/check-template-and-add-labels.ts @@ -13,6 +13,8 @@ import { } from './shared/labelable'; import { Label, + RegressionStage, + craftRegressionLabel, externalContributorLabel, flakyTestsLabel, invalidIssueTemplateLabel, @@ -21,14 +23,6 @@ import { import { TemplateType, templates } from './shared/template'; import { retrievePullRequest } from './shared/pull-request'; -enum RegressionStage { - DevelopmentFeature, - DevelopmentMain, - Testing, - Beta, - Production, -} - const knownBots = [ 'metamaskbot', 'dependabot', @@ -345,59 +339,3 @@ async function userBelongsToMetaMaskOrg( return Boolean(userBelongsToMetaMaskOrgResult?.user?.organization?.id); } - -// This function crafts appropriate label, corresponding to regression stage and release version. -function craftRegressionLabel( - regressionStage: RegressionStage | undefined, - releaseVersion: string | undefined, -): Label { - switch (regressionStage) { - case RegressionStage.DevelopmentFeature: - return { - name: `feature-branch-bug`, - color: '5319E7', // violet - description: `bug that was found on a feature branch, but not yet merged in main branch`, - }; - - case RegressionStage.DevelopmentMain: - return { - name: `regression-main`, - color: '5319E7', // violet - description: `Regression bug that was found on main branch, but not yet present in production`, - }; - - case RegressionStage.Testing: - return { - name: `regression-RC-${releaseVersion || '*'}`, - color: '744C11', // orange - description: releaseVersion - ? `Regression bug that was found in release candidate (RC) for release ${releaseVersion}` - : `TODO: Unknown release version. Please replace with correct 'regression-RC-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, - }; - - case RegressionStage.Beta: - return { - name: `regression-beta-${releaseVersion || '*'}`, - color: 'D94A83', // pink - description: releaseVersion - ? `Regression bug that was found in beta in release ${releaseVersion}` - : `TODO: Unknown release version. Please replace with correct 'regression-beta-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, - }; - - case RegressionStage.Production: - return { - name: `regression-prod-${releaseVersion || '*'}`, - color: '5319E7', // violet - description: releaseVersion - ? `Regression bug that was found in production in release ${releaseVersion}` - : `TODO: Unknown release version. Please replace with correct 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, - }; - - default: - return { - name: `regression-*`, - color: 'EDEDED', // grey - description: `TODO: Unknown regression stage. Please replace with correct regression label: 'regression-main', 'regression-RC-x.y.z', or 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, - }; - } -} diff --git a/.github/scripts/create-bug-report-issue.ts b/.github/scripts/create-bug-report-issue.ts new file mode 100644 index 000000000000..cfd2d7da93ae --- /dev/null +++ b/.github/scripts/create-bug-report-issue.ts @@ -0,0 +1,129 @@ +import * as core from '@actions/core'; +import { context, getOctokit } from '@actions/github'; +import { GitHub } from '@actions/github/lib/utils'; + +import { createIssue, retrieveIssueByTitle } from './shared/issue'; +import { + Label, + RegressionStage, + craftRegressionLabel, + craftTeamLabel, + createOrRetrieveLabel, + typeBugLabel, +} from './shared/label'; +import { codeRepoToPlanningRepo, codeRepoToPlatform, getCurrentDateFormatted, isValidVersionFormat } from './shared/utils'; +import { addIssueToGithubProject, GithubProject, GithubProjectField, retrieveGithubProject, updateGithubProjectDateFieldValue } from './shared/project'; + +main().catch((error: Error): void => { + console.error(error); + process.exit(1); +}); + +async function main(): Promise { + // "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions. + // We can't use "GITHUB_TOKEN" here, as its permissions don't allow neither to create new labels + // nor to retrieve the content of organisations Github Projects. + // In our case, we may want to create "regression-RC-x.y.z" label when it doesn't already exist. + // We may also want to retrieve the content of organisation's Github Projects. + // As a consequence, we need to create our own "BUG_REPORT_TOKEN" with "repo" and "read:org" permissions. + // Such a token allows both to create new labels and fetch the content of organisation's Github Projects. + const personalAccessToken = process.env.BUG_REPORT_TOKEN; + if (!personalAccessToken) { + core.setFailed('BUG_REPORT_TOKEN not found'); + process.exit(1); + } + + const projectNumber = Number(process.env.RELEASES_GITHUB_PROJECT_BOARD_NUMBER); + if (!projectNumber) { + core.setFailed('RELEASES_GITHUB_PROJECT_BOARD_NUMBER not found'); + process.exit(1); + } + + const projectViewNumber = Number(process.env.RELEASES_GITHUB_PROJECT_BOARD_VIEW_NUMBER); + if (!projectViewNumber) { + core.setFailed('RELEASES_GITHUB_PROJECT_BOARD_VIEW_NUMBER not found'); + process.exit(1); + } + + const releaseVersion = process.env.RELEASE_VERSION; + if (!releaseVersion) { + core.setFailed('RELEASE_VERSION not found'); + process.exit(1); + } + if (!isValidVersionFormat(releaseVersion)) { + core.setFailed(`Invalid format for RELEASE_VERSION: ${releaseVersion}. Expected format: x.y.z`); + process.exit(1); + } + + const repoOwner = context.repo.owner; + if (!repoOwner) { + core.setFailed('repo owner not found'); + process.exit(1); + } + const codeRepoName = context.repo.repo; + if (!codeRepoName) { + core.setFailed('code repo name not found'); + process.exit(1); + } + const planningRepoName = codeRepoToPlanningRepo[codeRepoName]; + if (!planningRepoName) { + core.setFailed('planning repo name not found'); + process.exit(1); + } + + // Retrieve platform name + const platformName = codeRepoToPlatform[codeRepoName]; + if (!platformName) { + core.setFailed('platform name not found'); + process.exit(1); + } + + // Initialise octokit, required to call Github GraphQL API + const octokit: InstanceType = getOctokit(personalAccessToken, { + previews: ['bane'], // The "bane" preview is required for adding, updating, creating and deleting labels. + }); + + // Craft regression labels to add + const regressionLabelTesting: Label = craftRegressionLabel(RegressionStage.Testing, releaseVersion); + const regressionLabelProduction: Label = craftRegressionLabel(RegressionStage.Production, releaseVersion); + const teamLabel: Label = craftTeamLabel(`${platformName}-platform`); + + // Create or retrieve the different labels + await createOrRetrieveLabel(octokit, repoOwner, codeRepoName, regressionLabelProduction); + await createOrRetrieveLabel(octokit, repoOwner, codeRepoName, regressionLabelTesting); + await createOrRetrieveLabel(octokit, repoOwner, planningRepoName, regressionLabelProduction); + const regressionLabelTestingId = await createOrRetrieveLabel(octokit, repoOwner, planningRepoName, regressionLabelTesting); + const typeBugLabelId = await createOrRetrieveLabel(octokit, repoOwner, planningRepoName, typeBugLabel); + const teamLabelId = await createOrRetrieveLabel(octokit, repoOwner, planningRepoName, teamLabel); + + const issueTitle = `v${releaseVersion} Bug Report`; + const issueWithSameTitle = await retrieveIssueByTitle(octokit, repoOwner, planningRepoName, issueTitle); + if (issueWithSameTitle) { + core.setFailed(`Bug report already exists: https://github.com/${repoOwner}/${planningRepoName}/issues/${issueWithSameTitle.number}. This is not desired, but can happen in cases where a release gets re-cut.`); + process.exit(1); + } + + const issueBody = `**What is this bug report issue for?**\n\n1. This issue is used to track release dates on this [Github Project board](https://github.com/orgs/MetaMask/projects/${projectNumber}/views/${projectViewNumber}), which content then gets pulled into our metrics system.\n\n2. This issue is also used by our Zapier automations, to determine if automated notifications shall be sent on Slack for release \`${releaseVersion}\`. Notifications will only be sent as long as this issue is open.\n\n**Who created and/or closed this issue?**\n\n- This issue was automatically created by a GitHub action upon the creation of the release branch \`Version-v${releaseVersion}\`, indicating the release was cut.\n\n- This issue gets automatically closed by another GitHub action, once the \`Version-v${releaseVersion}\` branch merges into \`main\`, indicating the release is prepared for store submission.`; + const issueId = await createIssue(octokit, repoOwner, planningRepoName, issueTitle, issueBody, [regressionLabelTestingId, typeBugLabelId, teamLabelId]); + + // Retrieve project, in order to obtain its ID + const project: GithubProject = await retrieveGithubProject(octokit, projectNumber); + + const projectFieldName: string = "RC Cut"; + + const projectField: GithubProjectField | undefined = project.fields.find(field => field.name === projectFieldName); + + if (!projectField) { + throw new Error(`Project field with name ${projectFieldName} was not found on Github Project with ID ${project.id}.`); + } + + if (!projectField.id) { + throw new Error(`Project field with name ${projectFieldName} was found on Github Project with ID ${project.id}, but it has no 'id' property.`); + } + + // Add bug report issue to 'Releases' Github Project Board + await addIssueToGithubProject(octokit, project.id, issueId); + + // Update bug report issue's date property on 'Releases' Github Project Board + await updateGithubProjectDateFieldValue(octokit, project.id, projectField.id, issueId, getCurrentDateFormatted()); +} diff --git a/.github/scripts/shared/issue.ts b/.github/scripts/shared/issue.ts index e5c6804630ed..809667a24b8e 100644 --- a/.github/scripts/shared/issue.ts +++ b/.github/scripts/shared/issue.ts @@ -1,6 +1,30 @@ import { GitHub } from '@actions/github/lib/utils'; import { LabelableType, Labelable } from './labelable'; +import { retrieveRepo } from './repo'; + +interface RawIssue { + id: string; + title: string; + number: number; + createdAt: string; + body: string; + author: { + login: string; + }; + labels: { + nodes: { + id: string; + name: string; + }[]; + }; + repository: { + name: string; + owner: { + login: string; + }; + }; +} // This function retrieves an issue on a specific repo export async function retrieveIssue( @@ -14,6 +38,8 @@ export async function retrieveIssue( repository(owner: $repoOwner, name: $repoName) { issue(number: $issueNumber) { id + title + number createdAt body author { @@ -25,6 +51,12 @@ export async function retrieveIssue( name } } + repository { + name + owner { + login + } + } } } } @@ -32,20 +64,7 @@ export async function retrieveIssue( const retrieveIssueResult: { repository: { - issue: { - id: string; - createdAt: string; - body: string; - author: { - login: string; - }; - labels: { - nodes: { - id: string; - name: string; - }[]; - }; - }; + issue: RawIssue; }; } = await octokit.graphql(retrieveIssueQuery, { repoOwner, @@ -68,6 +87,78 @@ export async function retrieveIssue( return issue; } +// This function retrieves an issue by title on a specific repo +export async function retrieveIssueByTitle( + octokit: InstanceType, + repoOwner: string, + repoName: string, + issueTitle: string, +): Promise { + const searchQuery = `repo:${repoOwner}/${repoName} type:issue in:title ${issueTitle}`; + + const retrieveIssueByTitleQuery = ` + query GetIssueByTitle($searchQuery: String!) { + search( + query: $searchQuery + type: ISSUE + first: 10 + ) { + nodes { + ... on Issue { + id + title + number + createdAt + body + author { + login + } + labels(first: 100) { + nodes { + id + name + } + } + repository { + name + owner { + login + } + } + } + } + issueCount + } + } + `; + + const retrieveIssueByTitleResult: { + search: { + nodes: RawIssue[]; + }; + } = await octokit.graphql(retrieveIssueByTitleQuery, { + searchQuery, + }); + + const issueWithSameTitle = retrieveIssueByTitleResult?.search?.nodes?.find(rawIssue => rawIssue.title === issueTitle); + + const issue: Labelable | undefined = issueWithSameTitle + ? { + id: issueWithSameTitle?.id, + type: LabelableType.Issue, + number: issueWithSameTitle?.number, + repoOwner: repoOwner, + repoName: repoName, + createdAt: issueWithSameTitle?.createdAt, + body: issueWithSameTitle?.body, + author: issueWithSameTitle?.author?.login, + labels: issueWithSameTitle?.labels?.nodes, + } + : undefined; + + return issue; +} + // This function retrieves the list of linked issues for a pull request export async function retrieveLinkedIssues( octokit: InstanceType, @@ -75,6 +166,7 @@ export async function retrieveLinkedIssues( repoName: string, prNumber: number, ): Promise { + // We assume there won't be more than 100 linked issues const retrieveLinkedIssuesQuery = ` query ($repoOwner: String!, $repoName: String!, $prNumber: Int!) { @@ -83,6 +175,7 @@ export async function retrieveLinkedIssues( closingIssuesReferences(first: 100) { nodes { id + title number createdAt body @@ -112,27 +205,7 @@ export async function retrieveLinkedIssues( repository: { pullRequest: { closingIssuesReferences: { - nodes: Array<{ - id: string; - number: number; - createdAt: string; - body: string; - author: { - login: string; - }; - labels: { - nodes: { - id: string; - name: string; - }[]; - }; - repository: { - name: string; - owner: { - login: string; - }; - }; - }>; + nodes: RawIssue[]; }; }; }; @@ -144,27 +217,7 @@ export async function retrieveLinkedIssues( const linkedIssues: Labelable[] = retrieveLinkedIssuesResult?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map( - (issue: { - id: string; - number: number; - createdAt: string; - body: string; - author: { - login: string; - }; - labels: { - nodes: { - id: string; - name: string; - }[]; - }; - repository: { - name: string; - owner: { - login: string; - }; - }; - }) => { + (issue: RawIssue) => { return { id: issue?.id, type: LabelableType.Issue, @@ -181,3 +234,60 @@ export async function retrieveLinkedIssues( return linkedIssues; } + +// This function creates an issue on a specific repo +export async function createIssue( + octokit: InstanceType, + repoOwner: string, + repoName: string, + issueTitle: string, + issueBody: string, + labelIds: string[], +): Promise { + // Retrieve PR's repo + const repoId = await retrieveRepo(octokit, repoOwner, repoName); + + const createIssueMutation = ` + mutation CreateIssue($repoId: ID!, $issueTitle: String!, $issueBody: String!, $labelIds: [ID!]) { + createIssue(input: {repositoryId: $repoId, title: $issueTitle, body: $issueBody, labelIds: $labelIds}) { + issue { + id + title + number + createdAt + body + author { + login + } + labels(first: 100) { + nodes { + id + name + } + } + repository { + name + owner { + login + } + } + } + } + } + `; + + const createIssueResult: { + createIssue: { + issue: RawIssue; + }; + } = await octokit.graphql(createIssueMutation, { + repoId, + issueTitle, + issueBody, + labelIds, + }); + + const issueId = createIssueResult?.createIssue?.issue?.id; + + return issueId; +} diff --git a/.github/scripts/shared/label.ts b/.github/scripts/shared/label.ts index d218dcf42570..42447e27937f 100644 --- a/.github/scripts/shared/label.ts +++ b/.github/scripts/shared/label.ts @@ -2,12 +2,25 @@ import { GitHub } from '@actions/github/lib/utils'; import { retrieveRepo } from './repo'; +export enum RegressionStage { + DevelopmentFeature, + DevelopmentMain, + Testing, + Beta, + Production, +} export interface Label { name: string; color: string; description: string; } +export const typeBugLabel: Label = { + name: 'type-bug', + color: 'D73A4A', + description: `Something isn't working`, +}; + export const externalContributorLabel: Label = { name: 'external-contributor', color: '7057FF', @@ -32,6 +45,88 @@ export const invalidPullRequestTemplateLabel: Label = { description: "PR's body doesn't match template", }; +// This function crafts appropriate label, corresponding to regression stage and release version. +export function craftRegressionLabel( + regressionStage: RegressionStage | undefined, + releaseVersion: string | undefined, +): Label { + switch (regressionStage) { + case RegressionStage.DevelopmentFeature: + return { + name: `feature-branch-bug`, + color: '5319E7', // violet + description: `bug that was found on a feature branch, but not yet merged in main branch`, + }; + + case RegressionStage.DevelopmentMain: + return { + name: `regression-main`, + color: '5319E7', // violet + description: `Regression bug that was found on main branch, but not yet present in production`, + }; + + case RegressionStage.Testing: + return { + name: `regression-RC-${releaseVersion || '*'}`, + color: '744C11', // orange + description: releaseVersion + ? `Regression bug that was found in release candidate (RC) for release ${releaseVersion}` + : `TODO: Unknown release version. Please replace with correct 'regression-RC-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + case RegressionStage.Beta: + return { + name: `regression-beta-${releaseVersion || '*'}`, + color: 'D94A83', // pink + description: releaseVersion + ? `Regression bug that was found in beta in release ${releaseVersion}` + : `TODO: Unknown release version. Please replace with correct 'regression-beta-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + case RegressionStage.Production: + return { + name: `regression-prod-${releaseVersion || '*'}`, + color: '5319E7', // violet + description: releaseVersion + ? `Regression bug that was found in production in release ${releaseVersion}` + : `TODO: Unknown release version. Please replace with correct 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + default: + return { + name: `regression-*`, + color: 'EDEDED', // grey + description: `TODO: Unknown regression stage. Please replace with correct regression label: 'regression-main', 'regression-RC-x.y.z', or 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + } +} + +// This function crafts appropriate label, corresponding to team name. +export function craftTeamLabel(teamName: string): Label { + switch (teamName) { + case 'extension-platform': + return { + name: `team-${teamName}`, + color: '#BFD4F2', // light blue + description: `Extension Platform team`, + }; + + case 'mobile-platform': + return { + name: `team-${teamName}`, + color: '#76E9D0', // light green + description: `Mobile Platform team`, + }; + + default: + return { + name: `team-*`, + color: 'EDEDED', // grey + description: `TODO: Unknown team. Please replace with correct team label.`, + }; + } +} + // This function creates or retrieves the label on a specific repo export async function createOrRetrieveLabel( octokit: InstanceType, diff --git a/.github/scripts/shared/project.ts b/.github/scripts/shared/project.ts new file mode 100644 index 000000000000..5dc672107d16 --- /dev/null +++ b/.github/scripts/shared/project.ts @@ -0,0 +1,270 @@ +import { GitHub } from '@actions/github/lib/utils'; +import { isValidDateFormat } from './utils'; + +const MAX_NB_FETCHES = 10; // For protection against infinite loops. + +export interface GithubProject { + id: string; + fields: GithubProjectField[]; +} + +export interface GithubProjectField { + id: string; + name: string; +} + +export interface GithubProjectIssueFieldValues { + id: string; // ID of the issue (unrelated to the Github Project board) + itemId: string; // ID of the issue, as an item of the Github Project board + cutDate: string; // "RC cut date" field value of the issue, as an item of the Github Project board +} + +interface RawGithubProjectIssueFieldValues { + id: string; + content: { + id: string; + }; + cutDate: { + date: string; + }; +} + +interface RawGithubProjectIssuesFieldValues { + pageInfo: { + endCursor: string; + }; + nodes: RawGithubProjectIssueFieldValues[]; +} + +// This function retrieves a Github Project +export async function retrieveGithubProject( + octokit: InstanceType, + projectNumber: number, +): Promise { + const retrieveProjectQuery = ` + query ($projectNumber: Int!) { + organization(login: "MetaMask") { + projectV2(number: $projectNumber) { + id + fields(first: 20) { + nodes { + ... on ProjectV2Field { + id + name + } + } + } + } + } + } + `; + + const retrieveProjectResult: { + organization: { + projectV2: { + id: string; + fields: { + nodes: { + id: string; + name: string; + }[]; + }; + }; + }; + } = await octokit.graphql(retrieveProjectQuery, { + projectNumber, + }); + + const project: GithubProject = { + id: retrieveProjectResult?.organization?.projectV2?.id, + fields: retrieveProjectResult?.organization.projectV2?.fields?.nodes, + }; + + if (!project) { + throw new Error(`Project with number ${projectNumber} was not found.`); + } + + if (!project.id) { + throw new Error(`Project with number ${projectNumber} was found, but it has no 'id' property.`); + } + + if (!project.fields) { + throw new Error(`Project with number ${projectNumber} was found, but it has no 'fields' property.`); + } + + return project; +} + +// This function retrieves a Github Project's issues' field values +export async function retrieveGithubProjectIssuesFieldValues( + octokit: InstanceType, + projectId: string, + cursor: string | undefined, +): Promise { + const after = cursor ? `after: "${cursor}"` : ''; + + const retrieveProjectIssuesFieldValuesQuery = ` + query ($projectId: ID!) { + node(id: $projectId) { + ... on ProjectV2 { + items( + first: 100 + ${after} + ) { + pageInfo { + endCursor + } + nodes { + id + content { + ... on Issue { + id + } + } + cutDate: fieldValueByName(name: "RC Cut") { + ... on ProjectV2ItemFieldDateValue { + date + } + } + } + } + } + } + } + `; + + const retrieveProjectIssuesFieldValuesResult: { + node: { + items: { + totalCount: number; + pageInfo: { + endCursor: string; + }; + nodes: { + id: string; + content: { + id: string; + }; + cutDate: { + date: string; + }; + }[]; + }; + }; + } = await octokit.graphql(retrieveProjectIssuesFieldValuesQuery, { + projectId, + }); + + const projectIssuesFieldValues: RawGithubProjectIssuesFieldValues = retrieveProjectIssuesFieldValuesResult.node.items; + + return projectIssuesFieldValues; +} + +// This function retrieves a Github Project's issue field values recursively +export async function retrieveGithubProjectIssueFieldValuesRecursively( + nbFetches: number, + octokit: InstanceType, + projectId: string, + issueId: string, + cursor: string | undefined, +): Promise { + if (nbFetches >= MAX_NB_FETCHES) { + throw new Error(`Forbidden: Trying to do more than ${MAX_NB_FETCHES} fetches (${nbFetches}).`); + } + + const projectIssuesFieldValuesResponse: RawGithubProjectIssuesFieldValues = await retrieveGithubProjectIssuesFieldValues( + octokit, + projectId, + cursor, + ); + + const projectIssueFieldValuesResponseWithSameId: RawGithubProjectIssueFieldValues | undefined = + projectIssuesFieldValuesResponse.nodes.find( + (issue) => issue.content?.id === issueId + ); // 'issue.content' can be equal to null in edge case where the Github Project board includes private repo issues that can't be accessed by the access token we're using + + if (projectIssueFieldValuesResponseWithSameId) { + const projectIssueFieldValues: GithubProjectIssueFieldValues = { + id: projectIssueFieldValuesResponseWithSameId.content?.id, + itemId: projectIssueFieldValuesResponseWithSameId.id, + cutDate: projectIssueFieldValuesResponseWithSameId.cutDate?.date, + }; + return projectIssueFieldValues; + } + + const newCursor = projectIssuesFieldValuesResponse.pageInfo.endCursor; + if (newCursor) { + return await retrieveGithubProjectIssueFieldValuesRecursively(nbFetches + 1, octokit, projectId, issueId, newCursor); + } else { + return undefined; + } +} + +// This function adds an issue to a Github Project +export async function addIssueToGithubProject( + octokit: InstanceType, + projectId: string, + issueId: string, +): Promise { + const addIssueToProjectMutation = ` + mutation ($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + clientMutationId + } + } + `; + + await octokit.graphql(addIssueToProjectMutation, { + projectId: projectId, + contentId: issueId, + }); +} + +// This function updates Github Project issue's date field value +export async function updateGithubProjectDateFieldValue( + octokit: InstanceType, + projectId: string, + projectFieldId: string, + issueId: string, + newDatePropertyValue: string, +): Promise { + if (!isValidDateFormat(newDatePropertyValue)) { + throw new Error(`Invalid input: date ${newDatePropertyValue} doesn't match "YYYY-MM-DD" format.`); + } + + const issue: GithubProjectIssueFieldValues | undefined = await retrieveGithubProjectIssueFieldValuesRecursively( + 0, + octokit, + projectId, + issueId, + undefined, + ); + + if (!issue) { + throw new Error(`Issue with ID ${issueId} was not found on Github Project with ID ${projectId}.`); + } + + const updateGithubProjectDatePropertyMutation = ` + mutation ($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) { + updateProjectV2ItemFieldValue( + input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { date: $date } + } + ) { + projectV2Item { + id + } + } + } + `; + + await octokit.graphql(updateGithubProjectDatePropertyMutation, { + projectId: projectId, + itemId: issue.itemId, + fieldId: projectFieldId, + date: newDatePropertyValue, + }); +} diff --git a/.github/scripts/shared/repo.ts b/.github/scripts/shared/repo.ts index 09e475350016..d87a7fb5a198 100644 --- a/.github/scripts/shared/repo.ts +++ b/.github/scripts/shared/repo.ts @@ -25,5 +25,9 @@ export async function retrieveRepo( const repoId = retrieveRepoResult?.repository?.id; + if (!repoId) { + throw new Error(`Repo with owner ${repoOwner} and name ${repoName} was not found.`); + } + return repoId; } diff --git a/.github/scripts/shared/utils.ts b/.github/scripts/shared/utils.ts new file mode 100644 index 000000000000..6ecc4f4d092a --- /dev/null +++ b/.github/scripts/shared/utils.ts @@ -0,0 +1,50 @@ +// This helper function checks if version has the correct format: "x.y.z" where "x", "y" and "z" are numbers. +export function isValidVersionFormat(str: string): boolean { + const regex = /^\d+\.\d+\.\d+$/; + return regex.test(str); +} + +// This helper function checks if a string has the date format "YYYY-MM-DD". +export function isValidDateFormat(dateString: string): boolean { + // Regular expression to match the date format "YYYY-MM-DD" + const dateFormatRegex = /^\d{4}-\d{2}-\d{2}$/; + + // Check if the dateString matches the regex + if (!dateFormatRegex.test(dateString)) { + return false; + } + + // Parse the date components + const [year, month, day] = dateString.split('-').map(Number); + + // Check if the date components form a valid date + const date = new Date(year, month - 1, day); + return ( + date.getFullYear() === year && + date.getMonth() === month - 1 && + date.getDate() === day + ); +} + +// This helper function generates the current date in that format: "YYYY-MM-DD" +export function getCurrentDateFormatted(): string { + const date = new Date(); + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based, so add 1 + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +// This mapping is used to know what planning repo is used for each code repo +export const codeRepoToPlanningRepo: { [key: string]: string } = { + "metamask-extension": "MetaMask-planning", + "metamask-mobile": "mobile-planning" +} + +// This mapping is used to know what platform each code repo is used for +export const codeRepoToPlatform: { [key: string]: string } = { + "metamask-extension": "extension", + "metamask-mobile": "mobile", +} diff --git a/.github/workflows/create-bug-report.yml b/.github/workflows/create-bug-report.yml index c5164af00c02..65ff2f4ab9e5 100644 --- a/.github/workflows/create-bug-report.yml +++ b/.github/workflows/create-bug-report.yml @@ -12,24 +12,24 @@ jobs: if [[ "$GITHUB_REF" =~ ^refs/heads/Version-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then version="${GITHUB_REF#refs/heads/Version-v}" echo "New release branch($version), continue next steps" - echo "version=$version" >> "$GITHUB_ENV" + echo "version=$version" >> "$GITHUB_OUTPUT" else echo "Not a release branch, skip next steps" fi + - name: Checkout repository + if: steps.extract_version.outputs.version + uses: actions/checkout@v4 + + - name: Setup environment + if: steps.extract_version.outputs.version + uses: metamask/github-tools/.github/actions/setup-environment@main + - name: Create bug report issue on planning repo - if: env.version - run: | - payload=$(cat < Date: Wed, 19 Feb 2025 13:05:20 +0100 Subject: [PATCH 04/14] feat: add `@metamask/multichain-network-controller` (#30426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Integration of the new `MultichainTransactionController`. The initial PR was here: - https://github.com/MetaMask/metamask-extension/pull/30309 (@gantunesr) We found some unrelated issues while upgrading the `transaction-controller`, so we decided to drop this change to not block the integration of our new controller. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30426?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** > [!NOTE] > The multichain network logic will be implemented in follow-up PRs. No actual functional changes, it's just a bumps of the `accounts-controller` for now. So the extension should just work like before. ## **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/main/.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/main/.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. --- .../controller-init/controller-list.ts | 3 + .../controller-init/messengers/index.ts | 4 + .../messengers/multichain/index.ts | 2 + ...chain-network-controller-messenger.test.ts | 14 + ...multichain-network-controller-messenger.ts | 36 ++ .../controller-init/multichain/index.ts | 1 + ...multichain-network-controller-init.test.ts | 52 +++ .../multichain-network-controller-init.ts | 25 ++ .../controllers/mmi-controller.test.ts | 1 + .../preferences-controller.test.ts | 3 + app/scripts/metamask-controller.js | 11 +- lavamoat/browserify/beta/policy.json | 117 +++++++ lavamoat/browserify/flask/policy.json | 69 ++++ lavamoat/browserify/main/policy.json | 117 +++++++ lavamoat/browserify/mmi/policy.json | 117 +++++++ package.json | 3 +- test/data/mock-accounts.ts | 18 + ...rs-after-init-opt-in-background-state.json | 1 + .../errors-after-init-opt-in-ui-state.json | 3 + ui/selectors/multichain/networks.test.ts | 331 ++++++++++++++++++ ui/selectors/multichain/networks.ts | 160 +++++++++ yarn.lock | 104 +++++- 22 files changed, 1184 insertions(+), 8 deletions(-) create mode 100644 app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.test.ts create mode 100644 app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.ts create mode 100644 app/scripts/controller-init/multichain/multichain-network-controller-init.test.ts create mode 100644 app/scripts/controller-init/multichain/multichain-network-controller-init.ts create mode 100644 ui/selectors/multichain/networks.test.ts create mode 100644 ui/selectors/multichain/networks.ts diff --git a/app/scripts/controller-init/controller-list.ts b/app/scripts/controller-init/controller-list.ts index 029f22f7da5d..e7ffdf8588e4 100644 --- a/app/scripts/controller-init/controller-list.ts +++ b/app/scripts/controller-init/controller-list.ts @@ -16,6 +16,7 @@ import { MultiChainAssetsRatesController, MultichainBalancesController, } from '@metamask/assets-controllers'; +import { MultichainNetworkController } from '@metamask/multichain-network-controller'; import { MultichainTransactionsController } from '@metamask/multichain-transactions-controller'; import { CronjobController, @@ -46,6 +47,7 @@ export type Controller = | MultiChainAssetsRatesController | MultichainBalancesController | MultichainTransactionsController + | MultichainNetworkController | NetworkController | OnboardingController | PermissionController< @@ -78,6 +80,7 @@ export type ControllerFlatState = AccountsController['state'] & MultiChainAssetsRatesController['state'] & MultichainBalancesController['state'] & MultichainTransactionsController['state'] & + MultichainNetworkController['state'] & NetworkController['state'] & OnboardingController['state'] & PermissionController< diff --git a/app/scripts/controller-init/messengers/index.ts b/app/scripts/controller-init/messengers/index.ts index 62b4f63b5cd8..43f20cd688b1 100644 --- a/app/scripts/controller-init/messengers/index.ts +++ b/app/scripts/controller-init/messengers/index.ts @@ -22,6 +22,7 @@ import { getMultichainBalancesControllerMessenger, getMultichainTransactionsControllerMessenger, getMultichainAssetsControllerMessenger, + getMultichainNetworkControllerMessenger, getMultiChainAssetsRatesControllerMessenger, } from './multichain'; @@ -50,6 +51,9 @@ export const CONTROLLER_MESSENGERS = { getMessenger: getMultichainTransactionsControllerMessenger, getInitMessenger: noop, }, + MultichainNetworkController: { + getMessenger: getMultichainNetworkControllerMessenger, + }, RateLimitController: { getMessenger: getRateLimitControllerMessenger, getInitMessenger: getRateLimitControllerInitMessenger, diff --git a/app/scripts/controller-init/messengers/multichain/index.ts b/app/scripts/controller-init/messengers/multichain/index.ts index dae4039662fe..01aedc2e3627 100644 --- a/app/scripts/controller-init/messengers/multichain/index.ts +++ b/app/scripts/controller-init/messengers/multichain/index.ts @@ -2,8 +2,10 @@ export { getMultichainAssetsControllerMessenger } from './multichain-assets-cont export { getMultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger'; export { getMultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger'; export { getMultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger'; +export { getMultichainNetworkControllerMessenger } from './multichain-network-controller-messenger'; export type { MultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger'; export type { MultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger'; export type { MultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger'; export type { MultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger'; +export type { MultichainNetworkControllerMessenger } from './multichain-network-controller-messenger'; diff --git a/app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.test.ts b/app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.test.ts new file mode 100644 index 000000000000..82fa8f1738ec --- /dev/null +++ b/app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.test.ts @@ -0,0 +1,14 @@ +import { Messenger, RestrictedMessenger } from '@metamask/base-controller'; +import { getMultichainNetworkControllerMessenger } from './multichain-network-controller-messenger'; + +describe('getMultichainNetworkControllerMessenger', () => { + it('returns a restricted messenger', () => { + const messenger = new Messenger(); + const multichainNetworkControllerMessenger = + getMultichainNetworkControllerMessenger(messenger); + + expect(multichainNetworkControllerMessenger).toBeInstanceOf( + RestrictedMessenger, + ); + }); +}); diff --git a/app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.ts b/app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.ts new file mode 100644 index 000000000000..f002cde6595b --- /dev/null +++ b/app/scripts/controller-init/messengers/multichain/multichain-network-controller-messenger.ts @@ -0,0 +1,36 @@ +import { Messenger } from '@metamask/base-controller'; +import { AccountsControllerSelectedAccountChangeEvent } from '@metamask/accounts-controller'; +import { + type NetworkControllerSetActiveNetworkAction, + type NetworkControllerGetStateAction, +} from '@metamask/network-controller'; + +type Actions = + | NetworkControllerSetActiveNetworkAction + | NetworkControllerGetStateAction; + +type Events = AccountsControllerSelectedAccountChangeEvent; + +export type MultichainNetworkControllerMessenger = ReturnType< + typeof getMultichainNetworkControllerMessenger +>; + +/** + * Get a restricted messenger for the Multichain Network controller. This is scoped to the + * actions and events that the Multichain Network controller is allowed to handle. + * + * @param messenger - The controller messenger to restrict. + * @returns The restricted controller messenger. + */ +export function getMultichainNetworkControllerMessenger( + messenger: Messenger, +) { + return messenger.getRestricted({ + name: 'MultichainNetworkController', + allowedActions: [ + 'NetworkController:setActiveNetwork', + 'NetworkController:getState', + ], + allowedEvents: ['AccountsController:selectedAccountChange'], + }); +} diff --git a/app/scripts/controller-init/multichain/index.ts b/app/scripts/controller-init/multichain/index.ts index 4e7d2e132ced..fc3c0a9d8d33 100644 --- a/app/scripts/controller-init/multichain/index.ts +++ b/app/scripts/controller-init/multichain/index.ts @@ -1,4 +1,5 @@ export { MultichainAssetsControllerInit } from './multichain-assets-controller-init'; export { MultichainBalancesControllerInit } from './multichain-balances-controller-init'; export { MultichainTransactionsControllerInit } from './multichain-transactions-controller-init'; +export { MultichainNetworkControllerInit } from './multichain-network-controller-init'; export { MultiChainAssetsRatesControllerInit } from './multichain-rates-assets-controller-init'; diff --git a/app/scripts/controller-init/multichain/multichain-network-controller-init.test.ts b/app/scripts/controller-init/multichain/multichain-network-controller-init.test.ts new file mode 100644 index 000000000000..5a581c6d2db9 --- /dev/null +++ b/app/scripts/controller-init/multichain/multichain-network-controller-init.test.ts @@ -0,0 +1,52 @@ +import { MultichainNetworkController } from '@metamask/multichain-network-controller'; +import { Messenger } from '@metamask/base-controller'; +import { buildControllerInitRequestMock } from '../test/utils'; +import { ControllerInitRequest } from '../types'; +import { + MultichainNetworkControllerMessenger, + getMultichainNetworkControllerMessenger, +} from '../messengers/multichain'; +import { MultichainNetworkControllerInit } from './multichain-network-controller-init'; + +jest.mock('@metamask/multichain-network-controller'); + +const buildInitRequestMock = (): jest.Mocked< + ControllerInitRequest +> => { + const baseControllerMessenger = new Messenger(); + + return { + ...buildControllerInitRequestMock(), + controllerMessenger: getMultichainNetworkControllerMessenger( + baseControllerMessenger, + ), + initMessenger: undefined, + }; +}; + +describe('MultichainNetworkControllerInit', () => { + const multichainNetworkControllerClassMock = jest.mocked( + MultichainNetworkController, + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns controller instance', () => { + const requestMock = buildInitRequestMock(); + expect( + MultichainNetworkControllerInit(requestMock).controller, + ).toBeInstanceOf(MultichainNetworkController); + }); + + it('initializes with correct messenger and state', () => { + const requestMock = buildInitRequestMock(); + MultichainNetworkControllerInit(requestMock); + + expect(multichainNetworkControllerClassMock).toHaveBeenCalledWith({ + messenger: requestMock.controllerMessenger, + state: requestMock.persistedState.MultichainNetworkController, + }); + }); +}); diff --git a/app/scripts/controller-init/multichain/multichain-network-controller-init.ts b/app/scripts/controller-init/multichain/multichain-network-controller-init.ts new file mode 100644 index 000000000000..575df3283b6e --- /dev/null +++ b/app/scripts/controller-init/multichain/multichain-network-controller-init.ts @@ -0,0 +1,25 @@ +import { MultichainNetworkController } from '@metamask/multichain-network-controller'; +import { ControllerInitFunction } from '../types'; +import { MultichainNetworkControllerMessenger } from '../messengers/multichain'; + +/** + * Initialize the Multichain Network controller. + * + * @param request - The request object. + * @param request.controllerMessenger - The messenger to use for the controller. + * @param request.persistedState - The persisted state of the extension. + * @returns The initialized controller. + */ +export const MultichainNetworkControllerInit: ControllerInitFunction< + MultichainNetworkController, + MultichainNetworkControllerMessenger +> = ({ controllerMessenger, persistedState }) => { + const controller = new MultichainNetworkController({ + messenger: controllerMessenger, + state: persistedState.MultichainNetworkController, + }); + + return { + controller, + }; +}; diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 72e5bfd724f6..e33f696acb7e 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -117,6 +117,7 @@ describe('MMIController', function () { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:networkDidChange', ], allowedActions: [ 'AccountsController:setCurrentAccount', diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index efcd766ce1c9..f3703f3c94e7 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -4,6 +4,7 @@ import { Messenger } from '@metamask/base-controller'; import { AccountsController } from '@metamask/accounts-controller'; import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; +import type { MultichainNetworkControllerNetworkDidChangeEvent } from '@metamask/multichain-network-controller'; import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { Hex } from '@metamask/utils'; import { @@ -52,6 +53,7 @@ const setupController = ({ | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent | SnapKeyringAccountTransactionsUpdatedEvent + | MultichainNetworkControllerNetworkDidChangeEvent >(); const preferencesControllerMessenger: PreferencesControllerMessenger = messenger.getRestricted({ @@ -85,6 +87,7 @@ const setupController = ({ 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:networkDidChange', ], allowedActions: [], }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7596e11cb93a..14724fbf1baa 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -351,14 +351,15 @@ import { handleBridgeTransactionFailed, handleTransactionFailedTypeBridge, } from './lib/bridge-status/metrics'; -///: BEGIN:ONLY_INCLUDE_IF(build-flask) import { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) MultichainAssetsControllerInit, MultichainTransactionsControllerInit, MultichainBalancesControllerInit, MultiChainAssetsRatesControllerInit, + ///: END:ONLY_INCLUDE_IF + MultichainNetworkControllerInit, } from './controller-init/multichain'; -///: END:ONLY_INCLUDE_IF import { TransactionControllerInit } from './controller-init/confirmations/transaction-controller-init'; import { PPOMControllerInit } from './controller-init/confirmations/ppom-controller-init'; import { initControllers } from './controller-init/utils'; @@ -626,6 +627,7 @@ export default class MetamaskController extends EventEmitter { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:networkDidChange', ], allowedActions: [ 'KeyringController:getAccounts', @@ -2021,6 +2023,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: MultichainBalancesControllerInit, MultichainTransactionsController: MultichainTransactionsControllerInit, ///: END:ONLY_INCLUDE_IF + MultichainNetworkController: MultichainNetworkControllerInit, }; const { @@ -2058,6 +2061,8 @@ export default class MetamaskController extends EventEmitter { this.multiChainAssetsRatesController = controllersByName.MultiChainAssetsRatesController; ///: END:ONLY_INCLUDE_IF + this.multichainNetworkController = + controllersByName.MultichainNetworkController; this.controllerMessenger.subscribe( 'TransactionController:transactionStatusUpdated', @@ -2185,6 +2190,7 @@ export default class MetamaskController extends EventEmitter { MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, + MultichainNetworkController: this.multichainNetworkController, NetworkController: this.networkController, AlertController: this.alertController, OnboardingController: this.onboardingController, @@ -2234,6 +2240,7 @@ export default class MetamaskController extends EventEmitter { MultichainTransactionsController: this.multichainTransactionsController, MultiChainAssetsRatesController: this.multiChainAssetsRatesController, ///: END:ONLY_INCLUDE_IF + MultichainNetworkController: this.multichainNetworkController, NetworkController: this.networkController, KeyringController: this.keyringController, PreferencesController: this.preferencesController, diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 907bd244f3ec..9a0873cb879c 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1434,6 +1434,14 @@ "@metamask/keyring-api>bech32": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/keyring-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@metamask/keyring-api>bech32": true + } + }, "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1460,6 +1468,14 @@ "@metamask/keyring-snap-client>uuid": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": { + "packages": { + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>@metamask/keyring-api": true, + "@metamask/keyring-api>@metamask/keyring-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>uuid": true + } + }, "@metamask/keyring-api>@metamask/keyring-utils": { "globals": { "URL": true @@ -1518,6 +1534,27 @@ "lodash": true } }, + "@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/multichain-network-controller>@metamask/utils": true, + "@metamask/multichain-network-controller>@solana/addresses": true + } + }, + "@metamask/multichain-transactions-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, + "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true + } + }, "@metamask/name-controller": { "globals": { "fetch": true @@ -2397,6 +2434,36 @@ "semver": true } }, + "@metamask/multichain-network-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/name-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2982,6 +3049,20 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, @@ -2991,11 +3072,25 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-core": { "packages": { "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": { + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-strings": { "globals": { "TextDecoder": true, @@ -3008,11 +3103,28 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/errors": { "globals": { "btoa": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -5874,6 +5986,11 @@ "crypto": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>uuid": { + "globals": { + "crypto": true + } + }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 7c180c8360db..9a0873cb879c 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1534,6 +1534,15 @@ "lodash": true } }, + "@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/multichain-network-controller>@metamask/utils": true, + "@metamask/multichain-network-controller>@solana/addresses": true + } + }, "@metamask/multichain-transactions-controller": { "globals": { "console.error": true @@ -2425,6 +2434,21 @@ "semver": true } }, + "@metamask/multichain-network-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/multichain-transactions-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -3025,6 +3049,20 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, @@ -3034,11 +3072,25 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-core": { "packages": { "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": { + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-strings": { "globals": { "TextDecoder": true, @@ -3051,11 +3103,28 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/errors": { "globals": { "btoa": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 907bd244f3ec..9a0873cb879c 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1434,6 +1434,14 @@ "@metamask/keyring-api>bech32": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/keyring-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@metamask/keyring-api>bech32": true + } + }, "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1460,6 +1468,14 @@ "@metamask/keyring-snap-client>uuid": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": { + "packages": { + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>@metamask/keyring-api": true, + "@metamask/keyring-api>@metamask/keyring-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>uuid": true + } + }, "@metamask/keyring-api>@metamask/keyring-utils": { "globals": { "URL": true @@ -1518,6 +1534,27 @@ "lodash": true } }, + "@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/multichain-network-controller>@metamask/utils": true, + "@metamask/multichain-network-controller>@solana/addresses": true + } + }, + "@metamask/multichain-transactions-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, + "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true + } + }, "@metamask/name-controller": { "globals": { "fetch": true @@ -2397,6 +2434,36 @@ "semver": true } }, + "@metamask/multichain-network-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/name-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2982,6 +3049,20 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, @@ -2991,11 +3072,25 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-core": { "packages": { "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": { + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-strings": { "globals": { "TextDecoder": true, @@ -3008,11 +3103,28 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/errors": { "globals": { "btoa": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -5874,6 +5986,11 @@ "crypto": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>uuid": { + "globals": { + "crypto": true + } + }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 6372097263b5..7f6f58c9686d 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1526,6 +1526,14 @@ "@metamask/keyring-api>bech32": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/keyring-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@metamask/keyring-api>bech32": true + } + }, "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1552,6 +1560,14 @@ "@metamask/keyring-snap-client>uuid": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": { + "packages": { + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>@metamask/keyring-api": true, + "@metamask/keyring-api>@metamask/keyring-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>uuid": true + } + }, "@metamask/keyring-api>@metamask/keyring-utils": { "globals": { "URL": true @@ -1610,6 +1626,27 @@ "lodash": true } }, + "@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/multichain-network-controller>@metamask/utils": true, + "@metamask/multichain-network-controller>@solana/addresses": true + } + }, + "@metamask/multichain-transactions-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, + "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true + } + }, "@metamask/name-controller": { "globals": { "fetch": true @@ -2489,6 +2526,36 @@ "semver": true } }, + "@metamask/multichain-network-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/name-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -3074,6 +3141,20 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, @@ -3083,11 +3164,25 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-core": { "packages": { "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": { + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/codecs-strings": { "globals": { "TextDecoder": true, @@ -3100,11 +3195,28 @@ "@solana/addresses>@solana/errors": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/errors": { "globals": { "btoa": true } }, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -5966,6 +6078,11 @@ "crypto": true } }, + "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client>uuid": { + "globals": { + "crypto": true + } + }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true diff --git a/package.json b/package.json index 466dbbca4274..d63825d882a6 100644 --- a/package.json +++ b/package.json @@ -293,7 +293,7 @@ "@metamask-institutional/types": "^1.2.0", "@metamask/abi-utils": "^2.0.2", "@metamask/account-watcher": "^4.1.2", - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^24.0.0", "@metamask/address-book-controller": "^6.0.3", "@metamask/announcement-controller": "^7.0.3", "@metamask/approval-controller": "^7.0.0", @@ -328,6 +328,7 @@ "@metamask/message-signing-snap": "^0.6.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/multichain": "^2.1.0", + "@metamask/multichain-network-controller": "^0.1.0", "@metamask/multichain-transactions-controller": "^0.3.0", "@metamask/name-controller": "^8.0.3", "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A22.1.1#~/.yarn/patches/@metamask-network-controller-npm-22.1.1-09b6510f1e.patch", diff --git a/test/data/mock-accounts.ts b/test/data/mock-accounts.ts index 0ac8f31c27a4..932ed9b736f0 100644 --- a/test/data/mock-accounts.ts +++ b/test/data/mock-accounts.ts @@ -4,8 +4,11 @@ import { EthAccountType, BtcMethod, BtcAccountType, + SolAccountType, EthScope, BtcScope, + SolMethod, + SolScope, } from '@metamask/keyring-api'; import { ETH_EOA_METHODS, @@ -73,6 +76,21 @@ export const MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET: InternalAccount = { }, }; +export const MOCK_ACCOUNT_SOLANA_MAINNET: InternalAccount = { + id: 'a3f9c2d4-6b8e-4d3a-9b2e-7f4b8e1a9c3d', + address: '3yZe7d5m8V9x2Q1w4u6t8b9n7k5j3h2g1f4d6s8a9p7q2r5t8v', + options: {}, + methods: [SolMethod.SendAndConfirmTransaction], + scopes: [SolScope.Mainnet], + type: SolAccountType.DataAccount, + metadata: { + name: 'Solana Account', + keyring: { type: KeyringTypes.snap }, + importTime: 1691592567600, + lastSelected: 1955565999999, + }, +}; + export const MOCK_ACCOUNTS = { [MOCK_ACCOUNT_EOA.id]: MOCK_ACCOUNT_EOA, [MOCK_ACCOUNT_ERC4337.id]: MOCK_ACCOUNT_ERC4337, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 2e84b6944a18..f62e291abfd0 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -143,6 +143,7 @@ "metaMetricsDataDeletionId": null, "metaMetricsDataDeletionTimestamp": 0 }, + "MultichainNetworkController": "object", "MultichainRatesController": { "fiatCurrency": "usd", "rates": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 8446d437617f..0c193a44a642 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -157,6 +157,9 @@ "previousAppVersion": "", "previousMigrationVersion": 0, "currentMigrationVersion": "number", + "multichainNetworkConfigurationsByChainId": "object", + "selectedMultichainNetworkChainId": "string", + "isEvmSelected": "boolean", "selectedNetworkClientId": "string", "networksMetadata": { "networkConfigurationId": { diff --git a/ui/selectors/multichain/networks.test.ts b/ui/selectors/multichain/networks.test.ts new file mode 100644 index 000000000000..b5efe28b7173 --- /dev/null +++ b/ui/selectors/multichain/networks.test.ts @@ -0,0 +1,331 @@ +import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { + type NetworkConfiguration, + RpcEndpointType, + NetworkStatus, +} from '@metamask/network-controller'; +import type { Hex, CaipChainId } from '@metamask/utils'; +import { type MultichainNetworkConfiguration } from '@metamask/multichain-network-controller'; + +import { type NetworkState } from '../../../shared/modules/selectors/networks'; +import type { AccountsState } from '../accounts'; +import { + MOCK_ACCOUNT_EOA, + MOCK_ACCOUNT_BIP122_P2WPKH, + MOCK_ACCOUNT_SOLANA_MAINNET, +} from '../../../test/data/mock-accounts'; +import { + type MultichainNetworkControllerState, + getNonEvmMultichainNetworkConfigurationsByChainId, + getMultichainNetworkConfigurationsByChainId, + getSelectedMultichainNetworkChainId, + getSelectedMultichainNetworkConfiguration, + getIsEvmMultichainNetworkSelected, +} from './networks'; + +type TestState = AccountsState & + MultichainNetworkControllerState & + NetworkState & { + metamask: { solanaSupportEnabled: boolean; bitcoinSupportEnabled: boolean }; + }; + +const mockNonEvmNetworks: Record = + { + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, + name: 'Solana Mainnet', + nativeCurrency: `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`, + isEvm: false, + }, + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + nativeCurrency: `${BtcScope.Mainnet}/slip44:0`, + isEvm: false, + }, + }; + +const mockEvmNetworksWithNewConfig: Record< + CaipChainId, + MultichainNetworkConfiguration +> = { + 'eip155:1': { + chainId: 'eip155:1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + isEvm: true, + }, + 'eip155:11155111': { + chainId: 'eip155:11155111', + name: 'Sepolia', + nativeCurrency: 'SepoliaETH', + blockExplorerUrls: ['https://sepolia.etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + isEvm: true, + }, +}; + +const mockEvmNetworksWithOldConfig: Record = { + '0x1': { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: RpcEndpointType.Infura, + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + defaultRpcEndpointIndex: 1, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + lastUpdatedAt: 1739466375574, + }, + '0xaa36a7': { + blockExplorerUrls: ['https://sepolia.etherscan.io'], + chainId: '0xaa36a7', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Sepolia', + nativeCurrency: 'SepoliaETH', + rpcEndpoints: [ + { + networkClientId: 'sepolia', + type: RpcEndpointType.Infura, + url: 'https://sepolia.infura.io/v3/{infuraProjectId}', + }, + ], + }, +}; + +const mockState: TestState = { + metamask: { + solanaSupportEnabled: true, + bitcoinSupportEnabled: true, + multichainNetworkConfigurationsByChainId: { + ...mockNonEvmNetworks, + }, + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: false, + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + ...mockEvmNetworksWithOldConfig, + }, + networksMetadata: { + mainnet: { + EIPS: { 1559: true }, + status: NetworkStatus.Available, + }, + sepolia: { + EIPS: { 1559: true }, + status: NetworkStatus.Available, + }, + }, + internalAccounts: { + selectedAccount: MOCK_ACCOUNT_EOA.id, + accounts: { + [MOCK_ACCOUNT_EOA.id]: MOCK_ACCOUNT_EOA, + }, + }, + }, +}; + +describe('Multichain network selectors', () => { + describe('getNonEvmMultichainNetworkConfigurationsByChainId', () => { + it('returns the non-EVM multichain network configurations by chain ID', () => { + expect( + getNonEvmMultichainNetworkConfigurationsByChainId(mockState), + ).toStrictEqual({ + ...mockNonEvmNetworks, + }); + }); + }); + + describe('getMultichainNetworkConfigurationsByChainId', () => { + it('returns all multichain network configurations by chain ID when Solana and Bitcoin are enabled', () => { + expect( + getMultichainNetworkConfigurationsByChainId(mockState), + ).toStrictEqual({ + ...mockNonEvmNetworks, + ...mockEvmNetworksWithNewConfig, + }); + }); + + it('returns all multichain network configurations by chain ID excluding Solana when support is disabled and there is no Solana account', () => { + const mockMultichainNetworkStateWithSolanaSupportDisabled = { + ...mockState, + metamask: { + ...mockState.metamask, + solanaSupportEnabled: false, + }, + }; + + expect( + getMultichainNetworkConfigurationsByChainId( + mockMultichainNetworkStateWithSolanaSupportDisabled, + ), + ).toStrictEqual({ + [BtcScope.Mainnet]: mockNonEvmNetworks[BtcScope.Mainnet], + ...mockEvmNetworksWithNewConfig, + }); + }); + + it('returns all multichain network configurations by chain ID excluding Bitcoin when support is disabled and there no Bitcoin account', () => { + const mockMultichainNetworkStateWithBitcoinSupportDisabled = { + ...mockState, + metamask: { + ...mockState.metamask, + bitcoinSupportEnabled: false, + }, + }; + + expect( + getMultichainNetworkConfigurationsByChainId( + mockMultichainNetworkStateWithBitcoinSupportDisabled, + ), + ).toStrictEqual({ + [SolScope.Mainnet]: mockNonEvmNetworks[SolScope.Mainnet], + ...mockEvmNetworksWithNewConfig, + }); + }); + + it('returns all multichain network configurations by chain ID excluding Bitcoin and Solana when support is disabled and no accounts related to those networks', () => { + const mockMultichainNetworkStateWithBitcoinSupportDisabled = { + ...mockState, + metamask: { + ...mockState.metamask, + solanaSupportEnabled: false, + bitcoinSupportEnabled: false, + }, + }; + + expect( + getMultichainNetworkConfigurationsByChainId( + mockMultichainNetworkStateWithBitcoinSupportDisabled, + ), + ).toStrictEqual({ ...mockEvmNetworksWithNewConfig }); + }); + + it('returns Solana as part of the multichain network configurations if there is a Solana account', () => { + const mockMultichainNetworkStateWithBitcoinSupportDisabled = { + ...mockState, + metamask: { + ...mockState.metamask, + solanaSupportEnabled: false, + bitcoinSupportEnabled: false, + internalAccounts: { + ...mockState.metamask.internalAccounts, + accounts: { + ...mockState.metamask.internalAccounts.accounts, + [MOCK_ACCOUNT_SOLANA_MAINNET.id]: MOCK_ACCOUNT_SOLANA_MAINNET, + }, + }, + }, + }; + + expect( + getMultichainNetworkConfigurationsByChainId( + mockMultichainNetworkStateWithBitcoinSupportDisabled, + ), + ).toStrictEqual({ + ...mockEvmNetworksWithNewConfig, + [SolScope.Mainnet]: mockNonEvmNetworks[SolScope.Mainnet], + }); + }); + + it('returns Bitcoin as part of the multichain network configurations if there is a Bitcoin account', () => { + const mockMultichainNetworkStateWithBitcoinSupportDisabled = { + ...mockState, + metamask: { + ...mockState.metamask, + solanaSupportEnabled: false, + bitcoinSupportEnabled: false, + internalAccounts: { + ...mockState.metamask.internalAccounts, + accounts: { + ...mockState.metamask.internalAccounts.accounts, + [MOCK_ACCOUNT_BIP122_P2WPKH.id]: MOCK_ACCOUNT_BIP122_P2WPKH, + }, + }, + }, + }; + + expect( + getMultichainNetworkConfigurationsByChainId( + mockMultichainNetworkStateWithBitcoinSupportDisabled, + ), + ).toStrictEqual({ + ...mockEvmNetworksWithNewConfig, + [BtcScope.Mainnet]: mockNonEvmNetworks[BtcScope.Mainnet], + }); + }); + + it('returns Bitcoin and Solana as part of the multichain network configurations if there is Bitcoin and Solana accounts', () => { + const mockMultichainNetworkStateWithBitcoinSupportDisabled = { + ...mockState, + metamask: { + ...mockState.metamask, + solanaSupportEnabled: false, + bitcoinSupportEnabled: false, + internalAccounts: { + ...mockState.metamask.internalAccounts, + accounts: { + ...mockState.metamask.internalAccounts.accounts, + [MOCK_ACCOUNT_BIP122_P2WPKH.id]: MOCK_ACCOUNT_BIP122_P2WPKH, + [MOCK_ACCOUNT_SOLANA_MAINNET.id]: MOCK_ACCOUNT_SOLANA_MAINNET, + }, + }, + }, + }; + + expect( + getMultichainNetworkConfigurationsByChainId( + mockMultichainNetworkStateWithBitcoinSupportDisabled, + ), + ).toStrictEqual({ + ...mockEvmNetworksWithNewConfig, + ...mockNonEvmNetworks, + }); + }); + }); + + describe('getSelectedMultichainNetworkChainId', () => { + it('returns the selected multichain network chain ID', () => { + expect(getSelectedMultichainNetworkChainId(mockState)).toStrictEqual( + SolScope.Mainnet, + ); + }); + }); + + describe('getIsEvmMultichainNetworkSelected', () => { + it('returns whether the EVM network is selected', () => { + expect(getIsEvmMultichainNetworkSelected(mockState)).toStrictEqual(false); + }); + }); + + describe('getSelectedMultichainNetworkConfiguration', () => { + it('returns the selected non EVM multichain network configuration if isEvmSelected is false', () => { + expect( + getSelectedMultichainNetworkConfiguration(mockState), + ).toStrictEqual(mockNonEvmNetworks[SolScope.Mainnet]); + }); + it('returns the selected EVM multichain network configuration if isEvmSelected is true', () => { + const mockMultichainNetworkStateWithEvmSelected = { + ...mockState, + metamask: { + ...mockState.metamask, + isEvmSelected: true, + }, + }; + + expect( + getSelectedMultichainNetworkConfiguration( + mockMultichainNetworkStateWithEvmSelected, + ), + ).toStrictEqual(mockEvmNetworksWithNewConfig['eip155:1']); + }); + }); +}); diff --git a/ui/selectors/multichain/networks.ts b/ui/selectors/multichain/networks.ts new file mode 100644 index 000000000000..d64e0ee12470 --- /dev/null +++ b/ui/selectors/multichain/networks.ts @@ -0,0 +1,160 @@ +import { + type MultichainNetworkControllerState as InternalMultichainNetworkState, + type MultichainNetworkConfiguration as InternalMultichainNetworkConfiguration, + toEvmCaipChainId, + toMultichainNetworkConfigurationsByChainId, +} from '@metamask/multichain-network-controller'; +import { type NetworkConfiguration as InternalNetworkConfiguration } from '@metamask/network-controller'; +import { type CaipChainId, BtcScope, SolScope } from '@metamask/keyring-api'; + +import { + type ProviderConfigState, + type SelectedNetworkClientIdState, + getProviderConfig, + getNetworkConfigurationsByChainId, +} from '../../../shared/modules/selectors/networks'; +import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; +import { + getIsBitcoinSupportEnabled, + getIsSolanaSupportEnabled, +} from '../selectors'; +import { getInternalAccounts } from '../accounts'; + +// Selector types + +export type MultichainNetworkControllerState = { + metamask: InternalMultichainNetworkState; +}; + +export type SelectedNetworkChainIdState = { + metamask: Pick< + InternalMultichainNetworkState, + 'selectedMultichainNetworkChainId' + >; +}; + +export type IsEvmSelectedState = { + metamask: Pick; +}; + +export type MultichainNetworkConfigurationsByChainIdState = { + metamask: { + multichainNetworkConfigurationsByChainId: Record< + string, + InternalMultichainNetworkConfiguration + >; + networkConfigurationsByChainId: Record< + string, + InternalNetworkConfiguration + >; + }; +}; + +/** + * This type takes into account the state + * of the multichain-network-controller and + * the network-controller. + */ +export type MultichainNetworkConfigState = + MultichainNetworkConfigurationsByChainIdState & + SelectedNetworkChainIdState & + IsEvmSelectedState & + SelectedNetworkClientIdState & + ProviderConfigState; + +// Selectors + +export const getNonEvmMultichainNetworkConfigurationsByChainId = ( + state: MultichainNetworkConfigurationsByChainIdState, +) => state.metamask.multichainNetworkConfigurationsByChainId; + +export const getIsNonEvmNetworksEnabled = createDeepEqualSelector( + getIsBitcoinSupportEnabled, + getIsSolanaSupportEnabled, + getInternalAccounts, + (isBitcoinEnabled, isSolanaEnabled, internalAccounts) => { + if (isBitcoinEnabled && isSolanaEnabled) { + return { bitcoinEnabled: true, solanaEnabled: true }; + } + + let bitcoinEnabled = isBitcoinEnabled; + let solanaEnabled = isSolanaEnabled; + + for (const { scopes } of internalAccounts) { + if (scopes.includes(BtcScope.Mainnet)) { + bitcoinEnabled = true; + } + if (scopes.includes(SolScope.Mainnet)) { + solanaEnabled = true; + } + if (bitcoinEnabled && solanaEnabled) { + break; + } + } + + return { bitcoinEnabled, solanaEnabled }; + }, +); + +export const getMultichainNetworkConfigurationsByChainId = + createDeepEqualSelector( + getNonEvmMultichainNetworkConfigurationsByChainId, + getNetworkConfigurationsByChainId, + getIsNonEvmNetworksEnabled, + ( + nonEvmNetworkConfigurationsByChainId, + networkConfigurationsByChainId, + isNonEvmNetworksEnabled, + ): Record => { + const filteredNonEvmNetworkConfigurationsByChainId: Record< + CaipChainId, + InternalMultichainNetworkConfiguration + > = {}; + + // This is not ideal but since there are only two non EVM networks + // we can just filter them out based on the support enabled + const { bitcoinEnabled, solanaEnabled } = isNonEvmNetworksEnabled; + if (bitcoinEnabled) { + filteredNonEvmNetworkConfigurationsByChainId[BtcScope.Mainnet] = + nonEvmNetworkConfigurationsByChainId[BtcScope.Mainnet]; + } + + if (solanaEnabled) { + filteredNonEvmNetworkConfigurationsByChainId[SolScope.Mainnet] = + nonEvmNetworkConfigurationsByChainId[SolScope.Mainnet]; + } + + const networks = { + ...filteredNonEvmNetworkConfigurationsByChainId, + ...toMultichainNetworkConfigurationsByChainId( + networkConfigurationsByChainId, + ), + }; + + return networks; + }, + ); + +export const getIsEvmMultichainNetworkSelected = (state: IsEvmSelectedState) => + state.metamask.isEvmSelected; + +export const getSelectedMultichainNetworkChainId = ( + state: MultichainNetworkConfigState, +) => { + const isEvmSelected = getIsEvmMultichainNetworkSelected(state); + + if (isEvmSelected) { + const evmNetworkConfig = getProviderConfig(state); + return toEvmCaipChainId(evmNetworkConfig.chainId); + } + return state.metamask.selectedMultichainNetworkChainId; +}; + +export const getSelectedMultichainNetworkConfiguration = ( + state: MultichainNetworkConfigState, +) => { + const chainId = getSelectedMultichainNetworkChainId(state); + const networkConfigurationsByChainId = + getMultichainNetworkConfigurationsByChainId(state); + return networkConfigurationsByChainId[chainId]; +}; diff --git a/yarn.lock b/yarn.lock index c8148932a092..fe1b9e70c6f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4822,15 +4822,16 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^23.0.1": - version: 23.0.1 - resolution: "@metamask/accounts-controller@npm:23.0.1" +"@metamask/accounts-controller@npm:^24.0.0": + version: 24.0.0 + resolution: "@metamask/accounts-controller@npm:24.0.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/base-controller": "npm:^8.0.0" "@metamask/eth-snap-keyring": "npm:^10.0.0" "@metamask/keyring-api": "npm:^17.0.0" "@metamask/keyring-internal-api": "npm:^4.0.1" + "@metamask/network-controller": "npm:^22.2.1" "@metamask/snaps-sdk": "npm:^6.17.1" "@metamask/snaps-utils": "npm:^8.10.0" "@metamask/utils": "npm:^11.1.0" @@ -4840,10 +4841,11 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@metamask/keyring-controller": ^19.0.0 + "@metamask/network-controller": ^22.0.0 "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.19.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/65ae8e09fe90de224ed7705dd23ef232ef2308cf5e8e7c1d3e4d13f9692c610e78553ca5d5095352f069a0038d587756bcbedcf14147ad5fd446bf855db69915 + checksum: 10/2a67f5cd13bfde1abfec8d5676d33447dc1520c8b50d4322d56ea97904b72dc7b4d7b8b10f5e4c947a989a7b7dcb0cc421da23982ee5c0eea466048cfa4e18e6 languageName: node linkType: hard @@ -5705,6 +5707,21 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-network-controller@npm:^0.1.0": + version: 0.1.1 + resolution: "@metamask/multichain-network-controller@npm:0.1.1" + dependencies: + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/keyring-api": "npm:^17.0.0" + "@metamask/utils": "npm:^11.1.0" + "@solana/addresses": "npm:^2.0.0" + peerDependencies: + "@metamask/accounts-controller": ^24.0.0 + "@metamask/network-controller": ^22.0.0 + checksum: 10/0753831d802d84d154dbdfacac8ad8f7277d25957a3ec369a5fa588bee219656c4b6b0992699191eafa965ef2b43d1d4c827c0c7e417b82d94bf244040ca8451 + languageName: node + linkType: hard + "@metamask/multichain-transactions-controller@npm:^0.3.0": version: 0.3.0 resolution: "@metamask/multichain-transactions-controller@npm:0.3.0" @@ -8115,6 +8132,31 @@ __metadata: languageName: node linkType: hard +"@solana/addresses@npm:^2.0.0": + version: 2.0.0 + resolution: "@solana/addresses@npm:2.0.0" + dependencies: + "@solana/assertions": "npm:2.0.0" + "@solana/codecs-core": "npm:2.0.0" + "@solana/codecs-strings": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/f99d09c72046c73858aa8b7bc323e634a60b1023a4d280036bc94489e431075c7f29d2889e8787e33a04cfdecbe77cd8ca26c31ded73f735dc98e49c3151cc17 + languageName: node + linkType: hard + +"@solana/assertions@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/assertions@npm:2.0.0" + dependencies: + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/c1af37ae1bd79b1657395d9315ac261dabc9908a64af6ed80e3b7e5140909cd8c8c757f0c41fff084e26fbb4d32f091c89c092a8c1ed5e6f4565dfe7426c0979 + languageName: node + linkType: hard + "@solana/assertions@npm:2.0.0-rc.4": version: 2.0.0-rc.4 resolution: "@solana/assertions@npm:2.0.0-rc.4" @@ -8135,6 +8177,17 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-core@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-core@npm:2.0.0" + dependencies: + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/e58a72e67bee3e5da60201eecda345c604b49138d5298e39b8e7d4d57a4dee47be3b0ecc8fc3429a2a60a42c952eaf860d43d3df1eb2b1d857e35368eca9c820 + languageName: node + linkType: hard + "@solana/codecs-core@npm:2.0.0-rc.4": version: 2.0.0-rc.4 resolution: "@solana/codecs-core@npm:2.0.0-rc.4" @@ -8146,6 +8199,18 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-numbers@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-numbers@npm:2.0.0" + dependencies: + "@solana/codecs-core": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/500144d549ea0292c2f672300610df9054339a31cb6a4e61b29623308ef3b14f15eb587ee6139cf3334d2e0f29db1da053522da244b12184bb8fbdb097b7102b + languageName: node + linkType: hard + "@solana/codecs-numbers@npm:2.0.0-rc.4": version: 2.0.0-rc.4 resolution: "@solana/codecs-numbers@npm:2.0.0-rc.4" @@ -8158,6 +8223,20 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-strings@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-strings@npm:2.0.0" + dependencies: + "@solana/codecs-core": "npm:2.0.0" + "@solana/codecs-numbers": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5" + checksum: 10/4380136e2603c2cee12a28438817beb34b0fe45da222b8c38342c5b3680f02086ec7868cde0bb7b4e5dd459af5988613af1d97230c6a193db3be1c45122aba39 + languageName: node + linkType: hard + "@solana/codecs-strings@npm:2.0.0-rc.4": version: 2.0.0-rc.4 resolution: "@solana/codecs-strings@npm:2.0.0-rc.4" @@ -8172,6 +8251,20 @@ __metadata: languageName: node linkType: hard +"@solana/errors@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/errors@npm:2.0.0" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.1.0" + peerDependencies: + typescript: ">=5" + bin: + errors: bin/cli.mjs + checksum: 10/4191f96cad47c64266ec501ae1911a6245fd02b2f68a2c53c3dabbc63eb7c5462f170a765b584348b195da2387e7ca02096d792c67352c2c30a4f3a3cc7e4270 + languageName: node + linkType: hard + "@solana/errors@npm:2.0.0-rc.4": version: 2.0.0-rc.4 resolution: "@solana/errors@npm:2.0.0-rc.4" @@ -26651,7 +26744,7 @@ __metadata: "@metamask-institutional/types": "npm:^1.2.0" "@metamask/abi-utils": "npm:^2.0.2" "@metamask/account-watcher": "npm:^4.1.2" - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^24.0.0" "@metamask/address-book-controller": "npm:^6.0.3" "@metamask/announcement-controller": "npm:^7.0.3" "@metamask/api-specs": "npm:^0.10.15" @@ -26697,6 +26790,7 @@ __metadata: "@metamask/message-signing-snap": "npm:^0.6.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/multichain": "npm:^2.1.0" + "@metamask/multichain-network-controller": "npm:^0.1.0" "@metamask/multichain-transactions-controller": "npm:^0.3.0" "@metamask/name-controller": "npm:^8.0.3" "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A22.1.1#~/.yarn/patches/@metamask-network-controller-npm-22.1.1-09b6510f1e.patch" From f73fd30e04702183fe00fa586bcfb55c879fe4a1 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:37:58 +0100 Subject: [PATCH 05/14] chore: bump `@metamask/keyring-controller` to `^19.1.0` (#30367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates `@metamask/keyring-controller` to `^19.1.0` The main change that requires adjustments is a new error thrown by the controller when some of its methods are invoked while the controller is locked. See [the changelog](https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.7...@metamask/keyring-controller@19.1.0) for more details [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30367?quickstart=1) ## **Related issues** - Related: https://github.com/MetaMask/core/issues/5171 - Related: https://github.com/MetaMask/metamask-extension/issues/20317 ## **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/main/.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/main/.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/controllers/mmi-controller.test.ts | 3 +++ app/scripts/metamask-controller.actions.test.js | 3 +++ app/scripts/metamask-controller.test.js | 12 +++++++----- package.json | 2 +- yarn.lock | 10 +++++----- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index e33f696acb7e..591603c0f2d5 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -774,6 +774,9 @@ describe('MMIController', function () { describe('handleMmiDashboardData', () => { it('should return internalAccounts as identities', async () => { + jest + .spyOn(mmiController.keyringController, 'getAccounts') + .mockReturnValue([mockAccount.address, mockAccount2.address]); const controllerMessengerSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.handleMmiDashboardData(); diff --git a/app/scripts/metamask-controller.actions.test.js b/app/scripts/metamask-controller.actions.test.js index f3dc22b46865..5ee364171ce2 100644 --- a/app/scripts/metamask-controller.actions.test.js +++ b/app/scripts/metamask-controller.actions.test.js @@ -202,7 +202,10 @@ describe('MetaMaskController', function () { describe('#setLocked', function () { it('should lock the wallet', async function () { + await metamaskController.createNewVaultAndKeychain('test@123'); + await metamaskController.setLocked(); + expect( metamaskController.keyringController.state.isUnlocked, ).toStrictEqual(false); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 932acec5a519..ab0f4cb64f5c 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -638,6 +638,7 @@ describe('MetaMaskController', () => { describe('setLocked', () => { it('should lock KeyringController', async () => { + await metamaskController.createNewVaultAndKeychain('password'); jest.spyOn(metamaskController.keyringController, 'setLocked'); await metamaskController.setLocked(); @@ -2650,17 +2651,18 @@ describe('MetaMaskController', () => { }); describe('#addNewAccount', () => { - it('errors when an primary keyring is does not exist', async () => { + it('throws an error if the keyring controller is locked', async () => { const addNewAccount = metamaskController.addNewAccount(); - - await expect(addNewAccount).rejects.toThrow('No HD keyring found'); + await expect(addNewAccount).rejects.toThrow( + 'KeyringController - The operation cannot be completed while the controller is locked.', + ); }); }); describe('#getSeedPhrase', () => { - it('errors when no password is provided', async () => { + it('throws error if keyring controller is locked', async () => { await expect(metamaskController.getSeedPhrase()).rejects.toThrow( - 'KeyringController - Cannot unlock without a previous vault.', + 'KeyringController - The operation cannot be completed while the controller is locked.', ); }); diff --git a/package.json b/package.json index d63825d882a6..a86a0d6c1d08 100644 --- a/package.json +++ b/package.json @@ -319,7 +319,7 @@ "@metamask/json-rpc-engine": "^10.0.0", "@metamask/json-rpc-middleware-stream": "^8.0.4", "@metamask/keyring-api": "^17.0.0", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@metamask/keyring-internal-api": "^4.0.2", "@metamask/keyring-snap-client": "^4.0.0", "@metamask/logging-controller": "^6.0.4", diff --git a/yarn.lock b/yarn.lock index fe1b9e70c6f0..10c711b56b5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5568,9 +5568,9 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.7": - version: 19.0.7 - resolution: "@metamask/keyring-controller@npm:19.0.7" +"@metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@npm:^19.1.0": + version: 19.1.0 + resolution: "@metamask/keyring-controller@npm:19.1.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@keystonehq/metamask-airgapped-keyring": "npm:^0.14.1" @@ -5586,7 +5586,7 @@ __metadata: async-mutex: "npm:^0.5.0" ethereumjs-wallet: "npm:^1.0.1" immer: "npm:^9.0.6" - checksum: 10/9b385b2e4f17126ed992ff4b4ef2cfa86c7404fbda464b482f57ed2aa1254b0ea98b59f9182ee73a4a6dfc6eea93646950eced9370536be2837987556c3b16c3 + checksum: 10/2121c8935f019edc92c1fef9901d9b67c1555905b96ac90640b3a991896c33261a6ca59fc23aeb995ca9f39d6c3b33a7b04d95ef48ec440f9683c3dd309bfd7b languageName: node linkType: hard @@ -26781,7 +26781,7 @@ __metadata: "@metamask/json-rpc-engine": "npm:^10.0.0" "@metamask/json-rpc-middleware-stream": "npm:^8.0.4" "@metamask/keyring-api": "npm:^17.0.0" - "@metamask/keyring-controller": "npm:^19.0.7" + "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.2" "@metamask/keyring-snap-client": "npm:^4.0.0" "@metamask/logging-controller": "npm:^6.0.4" From 0832bdd86a9c9d1dd03d4377053a688540569d00 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:21:59 +0100 Subject: [PATCH 06/14] fix: flaky tests `Test Snap bip-44 can pop up bip-44 snap and get private key result` and others using `clickElementSafe` (#30430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Several snap tests are flaky because of the `clickElementSafe` method. This method is used in several snap tests for clicking the scroll arrow before confirming. The problem is that the clickElementSafe has a default timeout of 1second, meaning, if the scroll arrow doesn't appear within 1 second, the click is skipped and we jump into the next step which is click Confirm. That timeout of 1 second sometimes is not sufficient, meaning we then try to click Confirm button, which is disabled because we need to click the arrow first. ![image](https://github.com/user-attachments/assets/f53f8dc2-7dfd-4e80-a2fe-485c7d0e1424) ![Screenshot from 2025-02-19 14-02-13](https://github.com/user-attachments/assets/c0542e6d-1146-4dd3-a401-d79e800d4579) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30430?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Check ci works ## **Screenshots/Recordings** https://github.com/user-attachments/assets/5092d1b0-aa8d-4d11-8590-f63a63077a52 ## **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/main/.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/main/.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/webdriver/driver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 1ec1f195c354..6eb3390af5ed 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -704,7 +704,7 @@ class Driver { * @param rawLocator - Element locator * @param timeout - The maximum time in ms to wait for the element */ - async clickElementSafe(rawLocator, timeout = 1000) { + async clickElementSafe(rawLocator, timeout = 2000) { try { const locator = this.buildLocator(rawLocator); const elements = await this.driver.wait( From 2abb96a2fda5ff96eb8b1c79e8f14dc32059f023 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 19 Feb 2025 16:29:32 +0100 Subject: [PATCH 07/14] chore: Bump Snaps dependencies (#30396) ## **Description** Bump Snaps packages and handle any required changes. Summary of Snaps changes: - Add additional properties to `snap_getPreferences` - Add `crossAlignment` to `Box` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30396?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/30368 --------- Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Co-authored-by: Guillaume Roux Co-authored-by: Guillaume Roux Co-authored-by: MetaMask Bot --- .../snaps/cronjob-controller-init.ts | 1 - .../snaps/execution-service-init.ts | 2 - .../snaps/snap-insights-controller-init.ts | 1 - .../snaps/snap-interface-controller-init.ts | 1 - .../snaps/snaps-registry-init.ts | 1 - app/scripts/metamask-controller.js | 52 ++++- builds.yml | 8 +- lavamoat/browserify/beta/policy.json | 94 +++++++-- lavamoat/browserify/flask/policy.json | 94 +++++++-- lavamoat/browserify/main/policy.json | 94 +++++++-- lavamoat/browserify/mmi/policy.json | 94 +++++++-- package.json | 12 +- test/e2e/snaps/enums.js | 2 +- .../snaps/snap-ui-renderer/components/box.ts | 28 ++- ui/hooks/snaps/useDisplayName.ts | 9 +- yarn.lock | 181 +++++++++++------- 16 files changed, 501 insertions(+), 173 deletions(-) diff --git a/app/scripts/controller-init/snaps/cronjob-controller-init.ts b/app/scripts/controller-init/snaps/cronjob-controller-init.ts index f959d87786ad..ce3b7ee2d00f 100644 --- a/app/scripts/controller-init/snaps/cronjob-controller-init.ts +++ b/app/scripts/controller-init/snaps/cronjob-controller-init.ts @@ -19,7 +19,6 @@ export const CronjobControllerInit: ControllerInitFunction< // with the expected type. // TODO: Look into the type mismatch. state: persistedState.CronjobController, - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: controllerMessenger, }); diff --git a/app/scripts/controller-init/snaps/execution-service-init.ts b/app/scripts/controller-init/snaps/execution-service-init.ts index fff8cd4f2b32..01763747d259 100644 --- a/app/scripts/controller-init/snaps/execution-service-init.ts +++ b/app/scripts/controller-init/snaps/execution-service-init.ts @@ -53,7 +53,6 @@ export const ExecutionServiceInit: ControllerInitFunction< memStateKey: null, persistedStateKey: null, controller: new OffscreenExecutionService({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: controllerMessenger, setupSnapProvider, offscreenPromise, @@ -68,7 +67,6 @@ export const ExecutionServiceInit: ControllerInitFunction< memStateKey: null, persistedStateKey: null, controller: new IframeExecutionService({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: controllerMessenger, iframeUrl: new URL(iframeUrl), setupSnapProvider, diff --git a/app/scripts/controller-init/snaps/snap-insights-controller-init.ts b/app/scripts/controller-init/snaps/snap-insights-controller-init.ts index f7fc09baecbd..7ba84b1f4c46 100644 --- a/app/scripts/controller-init/snaps/snap-insights-controller-init.ts +++ b/app/scripts/controller-init/snaps/snap-insights-controller-init.ts @@ -19,7 +19,6 @@ export const SnapInsightsControllerInit: ControllerInitFunction< // compatible with the expected type. // TODO: Look into the type mismatch. state: persistedState.SnapInsightsController, - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: controllerMessenger, }); diff --git a/app/scripts/controller-init/snaps/snap-interface-controller-init.ts b/app/scripts/controller-init/snaps/snap-interface-controller-init.ts index 4a3791c03580..f4947c7f90d2 100644 --- a/app/scripts/controller-init/snaps/snap-interface-controller-init.ts +++ b/app/scripts/controller-init/snaps/snap-interface-controller-init.ts @@ -19,7 +19,6 @@ export const SnapInterfaceControllerInit: ControllerInitFunction< // with the expected type. // TODO: Look into the type mismatch. state: persistedState.SnapInterfaceController, - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: controllerMessenger, }); diff --git a/app/scripts/controller-init/snaps/snaps-registry-init.ts b/app/scripts/controller-init/snaps/snaps-registry-init.ts index 86d1297e17c6..f9dc8f720afa 100644 --- a/app/scripts/controller-init/snaps/snaps-registry-init.ts +++ b/app/scripts/controller-init/snaps/snaps-registry-init.ts @@ -22,7 +22,6 @@ export const SnapsRegistryInit: ControllerInitFunction< // with the expected type. // TODO: Look into the type mismatch. state: persistedState.SnapsRegistry, - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: controllerMessenger, refetchOnAllowlistMiss: requireAllowlist, }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 14724fbf1baa..29ea67419583 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2551,14 +2551,32 @@ export default class MetamaskController extends EventEmitter { } /** - * Gets whether the privacy mode is enabled from the PreferencesController. + * Gets a subset of preferences from the PreferencesController to pass to a snap. * - * @returns {boolean} Whether the privacy mode is enabled. + * @returns {object} A subset of preferences. */ - getPrivacyMode() { - const { privacyMode } = this.preferencesController.state; + getPreferences() { + const { + preferences, + securityAlertsEnabled, + useCurrencyRateCheck, + useTransactionSimulations, + useTokenDetection, + useMultiAccountBalanceChecker, + openSeaEnabled, + useNftDetection, + } = this.preferencesController.state; - return privacyMode; + return { + privacyMode: preferences.privacyMode, + securityAlertsEnabled, + useCurrencyRateCheck, + useTransactionSimulations, + useTokenDetection, + useMultiAccountBalanceChecker, + openSeaEnabled, + useNftDetection, + }; } /** @@ -2573,8 +2591,28 @@ export default class MetamaskController extends EventEmitter { getPreferences: () => { const locale = this.getLocale(); const currency = this.currencyRateController.state.currentCurrency; - const hideBalances = this.getPrivacyMode(); - return { locale, currency, hideBalances }; + const { + privacyMode, + securityAlertsEnabled, + useCurrencyRateCheck, + useTransactionSimulations, + useTokenDetection, + useMultiAccountBalanceChecker, + openSeaEnabled, + useNftDetection, + } = this.getPreferences(); + return { + locale, + currency, + hideBalances: privacyMode, + useSecurityAlerts: securityAlertsEnabled, + useExternalPricingData: useCurrencyRateCheck, + simulateOnChainActions: useTransactionSimulations, + useTokenDetection, + batchCheckBalances: useMultiAccountBalanceChecker, + displayNftMedia: openSeaEnabled, + useNftDetection, + }; }, clearSnapState: this.controllerMessenger.call.bind( this.controllerMessenger, diff --git a/builds.yml b/builds.yml index eea8601d2754..a3b7fc3b71ff 100644 --- a/builds.yml +++ b/builds.yml @@ -27,7 +27,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.14.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/7.0.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -48,7 +48,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.14.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/7.0.0/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 @@ -72,7 +72,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - REJECT_INVALID_SNAPS_PLATFORM_VERSION: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.14.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/7.0.0/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -96,7 +96,7 @@ buildTypes: - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.14.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/7.0.0/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/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 9a0873cb879c..f2be8708fa4f 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -894,7 +894,7 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/rpc-errors": true, "nanoid": true } @@ -928,7 +928,7 @@ "@metamask/metamask-eth-abis": true, "@metamask/polling-controller": true, "@metamask/rpc-errors": true, - "@metamask/snaps-utils": true, + "@metamask/assets-controllers>@metamask/snaps-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, @@ -954,14 +954,6 @@ "immer": true } }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/network-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1010,14 +1002,6 @@ "immer": true } }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/transaction-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1551,7 +1535,7 @@ "@metamask/base-controller": true, "@metamask/keyring-api": true, "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, - "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": true, "@metamask/multichain-transactions-controller>@metamask/utils": true } }, @@ -1886,7 +1870,7 @@ "setTimeout": true }, "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, @@ -1991,6 +1975,76 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/assets-controllers>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/assets-controllers>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/transaction-controller": { "globals": { "clearTimeout": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 9a0873cb879c..f2be8708fa4f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -894,7 +894,7 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/rpc-errors": true, "nanoid": true } @@ -928,7 +928,7 @@ "@metamask/metamask-eth-abis": true, "@metamask/polling-controller": true, "@metamask/rpc-errors": true, - "@metamask/snaps-utils": true, + "@metamask/assets-controllers>@metamask/snaps-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, @@ -954,14 +954,6 @@ "immer": true } }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/network-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1010,14 +1002,6 @@ "immer": true } }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/transaction-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1551,7 +1535,7 @@ "@metamask/base-controller": true, "@metamask/keyring-api": true, "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, - "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": true, "@metamask/multichain-transactions-controller>@metamask/utils": true } }, @@ -1886,7 +1870,7 @@ "setTimeout": true }, "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, @@ -1991,6 +1975,76 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/assets-controllers>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/assets-controllers>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/transaction-controller": { "globals": { "clearTimeout": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 9a0873cb879c..f2be8708fa4f 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -894,7 +894,7 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/rpc-errors": true, "nanoid": true } @@ -928,7 +928,7 @@ "@metamask/metamask-eth-abis": true, "@metamask/polling-controller": true, "@metamask/rpc-errors": true, - "@metamask/snaps-utils": true, + "@metamask/assets-controllers>@metamask/snaps-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, @@ -954,14 +954,6 @@ "immer": true } }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/network-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1010,14 +1002,6 @@ "immer": true } }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/transaction-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1551,7 +1535,7 @@ "@metamask/base-controller": true, "@metamask/keyring-api": true, "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, - "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": true, "@metamask/multichain-transactions-controller>@metamask/utils": true } }, @@ -1886,7 +1870,7 @@ "setTimeout": true }, "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, @@ -1991,6 +1975,76 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/assets-controllers>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/assets-controllers>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/transaction-controller": { "globals": { "clearTimeout": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7f6f58c9686d..44d73b794f60 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -986,7 +986,7 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/rpc-errors": true, "nanoid": true } @@ -1020,7 +1020,7 @@ "@metamask/metamask-eth-abis": true, "@metamask/polling-controller": true, "@metamask/rpc-errors": true, - "@metamask/snaps-utils": true, + "@metamask/assets-controllers>@metamask/snaps-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, @@ -1046,14 +1046,6 @@ "immer": true } }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/network-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1102,14 +1094,6 @@ "immer": true } }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/transaction-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -1643,7 +1627,7 @@ "@metamask/base-controller": true, "@metamask/keyring-api": true, "@metamask/multichain-transactions-controller>@metamask/keyring-snap-client": true, - "@metamask/snaps-utils": true, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": true, "@metamask/multichain-transactions-controller>@metamask/utils": true } }, @@ -1978,7 +1962,7 @@ "setTimeout": true }, "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, @@ -2083,6 +2067,76 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/assets-controllers>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/assets-controllers>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, + "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/multichain-transactions-controller>@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/transaction-controller": { "globals": { "clearTimeout": true, diff --git a/package.json b/package.json index a86a0d6c1d08..173f1c240d7c 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,7 @@ "@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", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", - "@metamask/snaps-sdk": "^6.17.1", + "@metamask/snaps-sdk": "^6.18.0", "@swc/types@0.1.5": "^0.1.6", "@babel/core": "patch:@babel/core@npm%3A7.25.9#~/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch", "@babel/runtime": "patch:@babel/runtime@npm%3A7.25.9#~/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch", @@ -353,11 +353,11 @@ "@metamask/selected-network-controller": "^19.0.0", "@metamask/signature-controller": "^23.1.0", "@metamask/smart-transactions-controller": "^16.0.1", - "@metamask/snaps-controllers": "^9.19.1", - "@metamask/snaps-execution-environments": "^6.14.0", - "@metamask/snaps-rpc-methods": "^11.11.0", - "@metamask/snaps-sdk": "^6.17.1", - "@metamask/snaps-utils": "^8.10.0", + "@metamask/snaps-controllers": "^10.0.0", + "@metamask/snaps-execution-environments": "^7.0.0", + "@metamask/snaps-rpc-methods": "^11.12.0", + "@metamask/snaps-sdk": "^6.18.0", + "@metamask/snaps-utils": "^9.0.0", "@metamask/solana-wallet-snap": "^1.2.0", "@metamask/transaction-controller": "^45.0.0", "@metamask/user-operation-controller": "^24.0.1", diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 8844ee3eca89..be4ec454a198 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.18.1', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.19.0', }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/box.ts b/ui/components/app/snaps/snap-ui-renderer/components/box.ts index fccf81729285..065be7cb5ab3 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/box.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/box.ts @@ -31,6 +31,29 @@ function generateJustifyContent(alignment?: BoxProps['alignment']) { } } +function generateAlignItems( + crossAlignment: BoxProps['crossAlignment'], + center?: BoxProps['center'], +) { + if (center) { + return AlignItems.center; + } + + switch (crossAlignment) { + default: + // align-items defaults to stretch but it's not available in the JustifyContent enum + return undefined; + case 'start': + return AlignItems.flexStart; + + case 'center': + return AlignItems.center; + + case 'end': + return AlignItems.flexEnd; + } +} + export const box: UIComponentFactory = ({ element, ...params @@ -46,7 +69,10 @@ export const box: UIComponentFactory = ({ ? FlexDirection.Row : FlexDirection.Column, justifyContent: generateJustifyContent(element.props.alignment), - alignItems: element.props.center && AlignItems.center, + alignItems: generateAlignItems( + element.props.crossAlignment, + element.props.center, + ), className: 'snap-ui-renderer__panel', color: TextColor.textDefault, }, diff --git a/ui/hooks/snaps/useDisplayName.ts b/ui/hooks/snaps/useDisplayName.ts index 6a6d3d7e6b51..3ac5080d4a9f 100644 --- a/ui/hooks/snaps/useDisplayName.ts +++ b/ui/hooks/snaps/useDisplayName.ts @@ -1,5 +1,8 @@ -import { NamespaceId } from '@metamask/snaps-utils'; -import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; +import { + CaipChainId, + KnownCaipNamespace, + CaipNamespace, +} from '@metamask/utils'; import { useSelector } from 'react-redux'; import { getMemoizedAccountName, @@ -12,7 +15,7 @@ import { decimalToHex } from '../../../shared/modules/conversion.utils'; export type UseDisplayNameParams = { chain: { - namespace: NamespaceId; + namespace: CaipNamespace; reference: string; }; chainId: CaipChainId; diff --git a/yarn.lock b/yarn.lock index 10c711b56b5a..875664b1f752 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4876,15 +4876,15 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^7.0.0, @metamask/approval-controller@npm:^7.1.2": - version: 7.1.2 - resolution: "@metamask/approval-controller@npm:7.1.2" +"@metamask/approval-controller@npm:^7.0.0, @metamask/approval-controller@npm:^7.1.2, @metamask/approval-controller@npm:^7.1.3": + version: 7.1.3 + resolution: "@metamask/approval-controller@npm:7.1.3" dependencies: - "@metamask/base-controller": "npm:^7.1.1" + "@metamask/base-controller": "npm:^8.0.0" "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/utils": "npm:^11.0.1" + "@metamask/utils": "npm:^11.1.0" nanoid: "npm:^3.3.8" - checksum: 10/e5903e8c3799484a3f42b2683ed733e68aca5204070c84f6012ee3830b04ebffdc7fc21fe80d2ea46f2a9ee3557a38e4031186f47e84d9079c2c902361543b0d + checksum: 10/2d88378dcc4f6c32ad544766cf26b7f59b1aee1d7ca0909b966c8c08eaac1d7109004120ca254d7ad008a6e5241b144d4dd831f926ec928a66f93cbd2e6f1693 languageName: node linkType: hard @@ -5519,15 +5519,15 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-middleware-stream@npm:^8.0.4, @metamask/json-rpc-middleware-stream@npm:^8.0.6": - version: 8.0.6 - resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.6" +"@metamask/json-rpc-middleware-stream@npm:^8.0.4, @metamask/json-rpc-middleware-stream@npm:^8.0.6, @metamask/json-rpc-middleware-stream@npm:^8.0.7": + version: 8.0.7 + resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.7" dependencies: - "@metamask/json-rpc-engine": "npm:^10.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.3" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^11.0.1" + "@metamask/utils": "npm:^11.1.0" readable-stream: "npm:^3.6.2" - checksum: 10/4df2ddf068ee935b5ea29b833df243ee43e0a17ea0151bc312d4eaeec541612f7416761be2b66f316c0b12f577f0257831b83844f6b9addbaf5fe9d9c5638262 + checksum: 10/54dadd16876ad1637b1fccd0d35c66dcc9a03d8614814d78b6730f9bb325e80ad1fdbb99d5b75785245aadf4064417ac35bc710a5549d13128f12153d0cc0432 languageName: node linkType: hard @@ -6077,27 +6077,6 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^18.3.1": - version: 18.3.1 - resolution: "@metamask/providers@npm:18.3.1" - dependencies: - "@metamask/json-rpc-engine": "npm:^10.0.2" - "@metamask/json-rpc-middleware-stream": "npm:^8.0.6" - "@metamask/object-multiplex": "npm:^2.0.0" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/safe-event-emitter": "npm:^3.1.1" - "@metamask/utils": "npm:^11.0.1" - detect-browser: "npm:^5.2.0" - extension-port-stream: "npm:^4.1.0" - fast-deep-equal: "npm:^3.1.3" - is-stream: "npm:^2.0.0" - readable-stream: "npm:^3.6.2" - peerDependencies: - webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/0e21ba9cce926a49dedbfe30fc964cd2349ee6bf9156f525fb894dcbc147a3ae480384884131a6b1a0a508989b547d8c8d2aeb3d10e11f67a8ee5230c45631a8 - languageName: node - linkType: hard - "@metamask/providers@npm:^20.0.0": version: 20.0.0 resolution: "@metamask/providers@npm:20.0.0" @@ -6256,7 +6235,48 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.19.0, @metamask/snaps-controllers@npm:^9.19.1": +"@metamask/snaps-controllers@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/snaps-controllers@npm:10.0.0" + dependencies: + "@metamask/approval-controller": "npm:^7.1.3" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.2" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.7" + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/permission-controller": "npm:^11.0.6" + "@metamask/phishing-controller": "npm:^12.3.2" + "@metamask/post-message-stream": "npm:^9.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-registry": "npm:^3.2.3" + "@metamask/snaps-rpc-methods": "npm:^11.12.0" + "@metamask/snaps-sdk": "npm:^6.18.0" + "@metamask/snaps-utils": "npm:^9.0.0" + "@metamask/utils": "npm:^11.2.0" + "@xstate/fsm": "npm:^2.0.0" + async-mutex: "npm:^0.5.0" + browserify-zlib: "npm:^0.2.0" + concat-stream: "npm:^2.0.0" + fast-deep-equal: "npm:^3.1.3" + get-npm-tarball-url: "npm:^2.0.3" + immer: "npm:^9.0.6" + luxon: "npm:^3.5.0" + nanoid: "npm:^3.1.31" + readable-stream: "npm:^3.6.2" + readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" + tar-stream: "npm:^3.1.7" + peerDependencies: + "@metamask/snaps-execution-environments": ^7.0.0 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 10/eeb094883987a309ee7087bf540428ecd27d39189e494c81cc5f55623f46577fc9b6b4b907f2fc14cc5940486c56fca9074d204f5e43924d793e974cb3f63f34 + languageName: node + linkType: hard + +"@metamask/snaps-controllers@npm:^9.19.0": version: 9.19.1 resolution: "@metamask/snaps-controllers@npm:9.19.1" dependencies: @@ -6297,22 +6317,22 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.14.0": - version: 6.14.0 - resolution: "@metamask/snaps-execution-environments@npm:6.14.0" +"@metamask/snaps-execution-environments@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/snaps-execution-environments@npm:7.0.0" dependencies: "@metamask/json-rpc-engine": "npm:^10.0.2" "@metamask/object-multiplex": "npm:^2.1.0" "@metamask/post-message-stream": "npm:^9.0.0" - "@metamask/providers": "npm:^18.3.1" + "@metamask/providers": "npm:^20.0.0" "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/snaps-sdk": "npm:^6.17.0" - "@metamask/snaps-utils": "npm:^8.10.0" + "@metamask/snaps-sdk": "npm:^6.18.0" + "@metamask/snaps-utils": "npm:^9.0.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" + "@metamask/utils": "npm:^11.2.0" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/7ee10aacc5b94f51a1e4414aef097509c35625a5bccf7d8b693a87ced85a466244a64471452ee2fa8ee8b0a953b8859c0703bfff30b2ac89c327561d62228e46 + checksum: 10/c63b9e827cf6069af3fd8b5378f6f595c28cced32a46ea81ab7a5729d2425c8e466094dad935d360f79fbf5905e1b51b74f14aacc711effe03a315cd650fab9d languageName: node linkType: hard @@ -6328,33 +6348,33 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.11.0": - version: 11.11.0 - resolution: "@metamask/snaps-rpc-methods@npm:11.11.0" +"@metamask/snaps-rpc-methods@npm:^11.11.0, @metamask/snaps-rpc-methods@npm:^11.12.0": + version: 11.12.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.12.0" dependencies: "@metamask/key-tree": "npm:^10.0.2" - "@metamask/permission-controller": "npm:^11.0.5" + "@metamask/permission-controller": "npm:^11.0.6" "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/snaps-sdk": "npm:^6.17.0" - "@metamask/snaps-utils": "npm:^8.10.0" + "@metamask/snaps-sdk": "npm:^6.18.0" + "@metamask/snaps-utils": "npm:^9.0.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" + "@metamask/utils": "npm:^11.2.0" "@noble/hashes": "npm:^1.3.1" luxon: "npm:^3.5.0" - checksum: 10/cd88db675062e848a65dc4edcd26ed24184430af77ed58f3e7949879255cbf94d1b5fcc51127646494a239c390fe6398c2ffaa5f3d2f63e7f859225e2eeae832 + checksum: 10/75e83542b7fa7302e02ff010561d1a598cac348f3e31f874cb99b86032e0e1d634a96a0a5037df2f0dc67b87af9a40b202cf2d9e4b44863495deac6a358f5239 languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.17.1": - version: 6.17.1 - resolution: "@metamask/snaps-sdk@npm:6.17.1" +"@metamask/snaps-sdk@npm:^6.18.0": + version: 6.18.0 + resolution: "@metamask/snaps-sdk@npm:6.18.0" dependencies: "@metamask/key-tree": "npm:^10.0.2" - "@metamask/providers": "npm:^18.3.1" + "@metamask/providers": "npm:^20.0.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" - checksum: 10/05c5170c6250115535bc6d06a417157bb55005dd6fe86e768d70fabfba610ec8114cf45a8a5aad1219b1cfb0bcf5e080974735a0ac9a8c8bd0ac102f5c3cf42f + "@metamask/utils": "npm:^11.2.0" + checksum: 10/9ab80f884b6f63916af854c71c27fcee60ac9090a4f47253f06c8eeac7a8a3b9bfcee84e5f40fbb34391de4b0d77b936fe8b51d822f2c4944f9b4e98b14e6fd2 languageName: node linkType: hard @@ -6389,6 +6409,37 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-utils@npm:^9.0.0": + version: 9.0.0 + resolution: "@metamask/snaps-utils@npm:9.0.0" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/permission-controller": "npm:^11.0.6" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/slip44": "npm:^4.1.0" + "@metamask/snaps-registry": "npm:^3.2.3" + "@metamask/snaps-sdk": "npm:^6.18.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.2.0" + "@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/0aaaa584e5950daee8c8277f6e8a405ba416a829bef01f8c729f510afa634a06ee1c89a28d25074921b0c2bdd77631c5f37ec0b25111b9bd789a0082d306905d + languageName: node + linkType: hard + "@metamask/solana-wallet-snap@npm:^1.2.0": version: 1.2.0 resolution: "@metamask/solana-wallet-snap@npm:1.2.0" @@ -6517,9 +6568,9 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0": - version: 11.1.0 - resolution: "@metamask/utils@npm:11.1.0" +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.2.0": + version: 11.2.0 + resolution: "@metamask/utils@npm:11.2.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -6530,7 +6581,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/756f13987881fe26adaa0a54354bc5af20cedee4dd228a736d481697dc634adb9e6e54d8f1dcc1d487b2376ab4ba8c576ecbb24beab2fb63aff721d0d5c0f5fe + checksum: 10/9cc2cb6af4627085e72a310ba9b8921c69757d94e2992d4664627e5a0d99b1f2f7f8069c6f22262515135e1172bd66b82d00512d90ea2ec6da4e768f3d7d4ae2 languageName: node linkType: hard @@ -26817,11 +26868,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^19.0.0" "@metamask/signature-controller": "npm:^23.1.0" "@metamask/smart-transactions-controller": "npm:^16.0.1" - "@metamask/snaps-controllers": "npm:^9.19.1" - "@metamask/snaps-execution-environments": "npm:^6.14.0" - "@metamask/snaps-rpc-methods": "npm:^11.11.0" - "@metamask/snaps-sdk": "npm:^6.17.1" - "@metamask/snaps-utils": "npm:^8.10.0" + "@metamask/snaps-controllers": "npm:^10.0.0" + "@metamask/snaps-execution-environments": "npm:^7.0.0" + "@metamask/snaps-rpc-methods": "npm:^11.12.0" + "@metamask/snaps-sdk": "npm:^6.18.0" + "@metamask/snaps-utils": "npm:^9.0.0" "@metamask/solana-wallet-snap": "npm:^1.2.0" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:9.0.0" From a19a39862c633884bdbe836549267407042930b9 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 19 Feb 2025 17:10:00 +0100 Subject: [PATCH 08/14] fix: fix spinner display in NFT tab (#30427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix spinner display in NFt tab. No functional changes [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30427?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to nft tab 2. Spinner should be centered ## **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/main/.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/main/.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/assets/nfts/nft-grid/nft-grid.tsx | 28 +++++++++++++------ .../app/assets/nfts/nfts-tab/nfts-tab.tsx | 8 +++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/ui/components/app/assets/nfts/nft-grid/nft-grid.tsx b/ui/components/app/assets/nfts/nft-grid/nft-grid.tsx index 626010dbea18..c07f7fb66a45 100644 --- a/ui/components/app/assets/nfts/nft-grid/nft-grid.tsx +++ b/ui/components/app/assets/nfts/nft-grid/nft-grid.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { Hex } from '@metamask/utils'; -import { Display } from '../../../../../helpers/constants/design-system'; +import { + AlignItems, + Display, + JustifyContent, +} from '../../../../../helpers/constants/design-system'; import { Box } from '../../../../component-library'; import Spinner from '../../../../ui/spinner'; import { getNftImageAlt } from '../../../../../helpers/utils/nfts'; @@ -99,15 +103,21 @@ export default function NftGrid({ ); })} - {nftsStillFetchingIndication ? ( - - - - ) : null} + {nftsStillFetchingIndication ? ( + + + + ) : null} ); } diff --git a/ui/components/app/assets/nfts/nfts-tab/nfts-tab.tsx b/ui/components/app/assets/nfts/nfts-tab/nfts-tab.tsx index 3faae7c68e16..5acda9db178d 100644 --- a/ui/components/app/assets/nfts/nfts-tab/nfts-tab.tsx +++ b/ui/components/app/assets/nfts/nfts-tab/nfts-tab.tsx @@ -134,7 +134,13 @@ export default function NftsTab() { if (!hasAnyNfts && nftsStillFetchingIndication) { return ( - + Date: Wed, 19 Feb 2025 12:02:31 -0600 Subject: [PATCH 09/14] feat: MMS-1868 new quote card and story (#30303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** New quote card design for Solana cross-chain swaps. integration blocked by dependencies, separate integration PR will come in the future, probably next week, to add it to the prepare-bridge page. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30303?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMS-1868 (item no. 3) ## **Manual testing steps** Not relevant. Only on storybook for now. Component is not rendered anywhere in the app. ## **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/main/.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/main/.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. --- .../multichain-bridge-quote-card.stories.tsx | 101 ++++++ .../quotes/multichain-bridge-quote-card.tsx | 312 ++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx create mode 100644 ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx new file mode 100644 index 000000000000..79616e27271a --- /dev/null +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import { MultichainBridgeQuoteCard } from './multichain-bridge-quote-card'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json'; + +const storybook = { + title: 'Pages/Bridge/MultichainBridgeQuoteCard', + component: MultichainBridgeQuoteCard, +}; + +const Container = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +export const DefaultStory = () => { + return ( + + + + ); +}; +DefaultStory.storyName = 'Default'; +DefaultStory.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export const WithDestinationAddress = () => { + return ( + + + + ); +}; +WithDestinationAddress.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export const WithLowEstimatedReturn = () => { + return ( + + + + ); +}; +WithLowEstimatedReturn.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export default storybook; diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx new file mode 100644 index 000000000000..87767298f014 --- /dev/null +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx @@ -0,0 +1,312 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + Text, + PopoverPosition, + IconName, + ButtonLink, + Icon, + IconSize, + AvatarNetwork, + AvatarNetworkSize, +} from '../../../components/component-library'; +import { + getBridgeQuotes, + getFromChain, + getToChain, + getValidationErrors, +} from '../../../ducks/bridge/selectors'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + formatCurrencyAmount, + formatTokenAmount, + formatEtaInMinutes, +} from '../utils/quote'; +import { + getCurrentCurrency, + getNativeCurrency, +} from '../../../ducks/metamask/metamask'; +import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker'; +import { useRequestProperties } from '../../../hooks/bridge/events/useRequestProperties'; +import { useRequestMetadataProperties } from '../../../hooks/bridge/events/useRequestMetadataProperties'; +import { useQuoteProperties } from '../../../hooks/bridge/events/useQuoteProperties'; +import { MetaMetricsEventName } from '../../../../shared/constants/metametrics'; +import { + AlignItems, + BackgroundColor, + BlockSize, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { Row, Column, Tooltip } from '../layout'; +import { + BRIDGE_MM_FEE_RATE, + NETWORK_TO_SHORT_NETWORK_NAME_MAP, +} from '../../../../shared/constants/bridge'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; +import { decimalToHex } from '../../../../shared/modules/conversion.utils'; +import { TERMS_OF_USE_LINK } from '../../../../shared/constants/terms'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import { shortenString } from '../../../helpers/utils/util'; +import { BridgeQuotesModal } from './bridge-quotes-modal'; + +type MultichainBridgeQuoteCardProps = { + destinationAddress?: string; +}; + +export const MultichainBridgeQuoteCard = ({ + destinationAddress, +}: MultichainBridgeQuoteCardProps) => { + const t = useI18nContext(); + const { activeQuote } = useSelector(getBridgeQuotes); + const currency = useSelector(getCurrentCurrency); + const ticker = useSelector(getNativeCurrency); + const { isEstimatedReturnLow } = useSelector(getValidationErrors); + + const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker(); + const { quoteRequestProperties } = useRequestProperties(); + const requestMetadataProperties = useRequestMetadataProperties(); + const quoteListProperties = useQuoteProperties(); + + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + const locale = useSelector(getIntlLocale); + + const [showAllQuotes, setShowAllQuotes] = useState(false); + const [shouldShowNetworkFeesInGasToken, setShouldShowNetworkFeesInGasToken] = + useState(false); + + return ( + <> + setShowAllQuotes(false)} + /> + {activeQuote ? ( + + + + {t('bestPrice')} + + {t('howQuotesWorkExplanation', [BRIDGE_MM_FEE_RATE])} + + + + { + quoteRequestProperties && + requestMetadataProperties && + quoteListProperties && + trackCrossChainSwapsEvent({ + event: MetaMetricsEventName.AllQuotesOpened, + properties: { + ...quoteRequestProperties, + ...requestMetadataProperties, + ...quoteListProperties, + }, + }); + setShowAllQuotes(true); + }} + > + {t('moreQuotes')} + + + + + + + + + { + NETWORK_TO_SHORT_NETWORK_NAME_MAP[ + `0x${decimalToHex( + activeQuote.quote.srcChainId, + )}` as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP + ] + } + + + + + { + NETWORK_TO_SHORT_NETWORK_NAME_MAP[ + `0x${decimalToHex( + activeQuote.quote.destChainId, + )}` as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP + ] + } + + + {destinationAddress && ( + + {shortenString(destinationAddress)} + + )} + + + + + + + {shouldShowNetworkFeesInGasToken + ? `${ + activeQuote.totalNetworkFee?.valueInCurrency + ? formatTokenAmount( + locale, + activeQuote.totalNetworkFee?.amount, + ) + : undefined + } - ${ + activeQuote.totalMaxNetworkFee?.valueInCurrency + ? formatTokenAmount( + locale, + activeQuote.totalMaxNetworkFee?.amount, + ticker, + ) + : undefined + }` + : `${ + formatCurrencyAmount( + activeQuote.totalNetworkFee?.valueInCurrency, + currency, + 2, + ) ?? + formatTokenAmount( + locale, + activeQuote.totalNetworkFee?.amount, + ) + } - ${ + formatCurrencyAmount( + activeQuote.totalMaxNetworkFee?.valueInCurrency, + currency, + 2, + ) ?? + formatTokenAmount( + locale, + activeQuote.totalMaxNetworkFee?.amount, + ticker, + ) + }`} + + + setShouldShowNetworkFeesInGasToken( + !shouldShowNetworkFeesInGasToken, + ) + } + /> + + + + + + {t('bridgeTimingMinutes', [ + formatEtaInMinutes( + activeQuote.estimatedProcessingTimeInSeconds, + ), + ])} + + + + + + + {t('rateIncludesMMFee', [BRIDGE_MM_FEE_RATE])} + + + {t('bridgeTerms')} + + + + + ) : null} + + ); +}; From 67aa6aad5e785d1ee7e94e6e32deb3067aeace80 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo <100321200+micaelae@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:25:38 -0800 Subject: [PATCH 10/14] chore: set swap input parameters (#30284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Modifying bridge input component logic to allow fetching swap quotes [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30284?quickstart=1) ## **Related issues** Fixes:NA ## **Manual testing steps** 1. Load flask build and activate non-EVM chain 3. Click "Swap" 4. Page should match Swap mocks (using Bridge ui) 5. Select dest token 6. "Switch inputs" button should be enabled and switch token selections on click 7. Verify that quotes are fetched in the background 8. Bridge experience should not be affected ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [X] 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/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../controllers/bridge/bridge-controller.ts | 4 +- .../components/bridge-asset-picker-button.tsx | 4 +- .../bridge/prepare/prepare-bridge-page.tsx | 137 ++++++++++-------- 3 files changed, 80 insertions(+), 65 deletions(-) diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 90a2a4dee193..27d7f230805d 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -207,9 +207,7 @@ export default class BridgeController extends StaticIntervalPollingController
{ this.#abortController?.abort('New quote request'); this.#abortController = new AbortController(); - if (updatedQuoteRequest.srcChainId === updatedQuoteRequest.destChainId) { - return; - } + const { bridgeState } = this.state; this.update((_state) => { _state.bridgeState = { diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx b/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx index b04da2b981fa..8296a950f541 100644 --- a/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx +++ b/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx @@ -68,9 +68,9 @@ export const BridgeAssetPickerButton = ({ diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index affc55ccde9f..afc6ad6886c1 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -178,6 +178,8 @@ const PrepareBridgePage = () => { const millisecondsUntilNextRefresh = useCountdownTimer(); + const isSwap = useIsMultichainSwap(); + const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); // Resets the banner visibility when the estimated return is low @@ -275,9 +277,10 @@ const PrepareBridgePage = () => { srcChainId: fromChain?.chainId ? Number(hexToDecimal(fromChain.chainId)) : undefined, - destChainId: toChain?.chainId - ? Number(hexToDecimal(toChain.chainId)) - : undefined, + destChainId: + isSwap && fromChain?.chainId + ? Number(hexToDecimal(fromChain.chainId)) + : toChain?.chainId && Number(hexToDecimal(toChain.chainId)), // This override allows quotes to be returned when the rpcUrl is a tenderly fork // Otherwise quotes get filtered out by the bridge-api when the wallet's real // balance is less than the tenderly balance @@ -285,6 +288,7 @@ const PrepareBridgePage = () => { slippage, }), [ + isSwap, fromToken, toToken, fromChain?.chainId, @@ -366,8 +370,6 @@ const PrepareBridgePage = () => { } }, [fromChain, fromToken, fromTokens, search, isFromTokensLoading]); - const isSwap = useIsMultichainSwap(); - return ( { value: token.address, }); }} - networkProps={{ - network: fromChain, - networks: fromChains, - onNetworkChange: (networkConfig) => { - networkConfig.chainId !== fromChain?.chainId && - trackInputEvent({ - input: 'chain_source', - value: networkConfig.chainId, - }); - if (networkConfig.chainId === toChain?.chainId) { - dispatch(setToChainId(null)); - dispatch(setToToken(null)); - } - if (isNetworkAdded(networkConfig)) { - dispatch( - setActiveNetwork( - networkConfig.rpcEndpoints[ - networkConfig.defaultRpcEndpointIndex - ].networkClientId, - ), - ); - } - dispatch(setFromToken(null)); - dispatch(setFromTokenInputValue(null)); - }, - header: t('yourNetworks'), - }} - isMultiselectEnabled + networkProps={ + isSwap + ? undefined + : { + network: fromChain, + networks: fromChains, + onNetworkChange: (networkConfig) => { + networkConfig.chainId !== fromChain?.chainId && + trackInputEvent({ + input: 'chain_source', + value: networkConfig.chainId, + }); + if (networkConfig.chainId === toChain?.chainId) { + dispatch(setToChainId(null)); + dispatch(setToToken(null)); + } + if (isNetworkAdded(networkConfig)) { + dispatch( + setActiveNetwork( + networkConfig.rpcEndpoints[ + networkConfig.defaultRpcEndpointIndex + ].networkClientId, + ), + ); + } + dispatch(setFromToken(null)); + dispatch(setFromTokenInputValue(null)); + }, + header: t('yourNetworks'), + } + } + isMultiselectEnabled={!isSwap} onMaxButtonClick={(value: string) => { dispatch(setFromTokenInputValue(value)); }} @@ -468,10 +474,10 @@ const PrepareBridgePage = () => { disabled={ isSwitchingTemporarilyDisabled || !isValidQuoteRequest(quoteRequest, false) || - !isNetworkAdded(toChain) + (!isSwap && !isNetworkAdded(toChain)) } onClick={() => { - if (!isNetworkAdded(toChain)) { + if (!isSwap && !isNetworkAdded(toChain)) { return; } setRotateSwitchTokens(!rotateSwitchTokens); @@ -480,15 +486,19 @@ const PrepareBridgePage = () => { event: MetaMetricsEventName.InputSourceDestinationFlipped, properties: flippedRequestProperties, }); - const toChainClientId = - toChain?.defaultRpcEndpointIndex !== undefined && - toChain?.rpcEndpoints - ? toChain.rpcEndpoints[toChain.defaultRpcEndpointIndex] - .networkClientId - : undefined; - toChainClientId && dispatch(setActiveNetwork(toChainClientId)); + if (!isSwap) { + // Only flip networks if bridging + const toChainClientId = + toChain?.defaultRpcEndpointIndex !== undefined && + toChain?.rpcEndpoints && + isNetworkAdded(toChain) + ? toChain.rpcEndpoints[toChain.defaultRpcEndpointIndex] + .networkClientId + : undefined; + toChainClientId && dispatch(setActiveNetwork(toChainClientId)); + fromChain?.chainId && dispatch(setToChainId(fromChain.chainId)); + } dispatch(setFromToken(toToken)); - fromChain?.chainId && dispatch(setToChainId(fromChain.chainId)); dispatch(setToToken(fromToken)); }} /> @@ -505,23 +515,30 @@ const PrepareBridgePage = () => { }); dispatch(setToToken(token)); }} - networkProps={{ - network: toChain, - networks: toChains, - onNetworkChange: (networkConfig) => { - networkConfig.chainId !== toChain?.chainId && - trackInputEvent({ - input: 'chain_destination', - value: networkConfig.chainId, - }); - dispatch(setToChainId(networkConfig.chainId)); - dispatch(setToToken(null)); - }, - header: isSwap ? t('swapSwapTo') : t('bridgeTo'), - shouldDisableNetwork: ({ chainId }) => - chainId === fromChain?.chainId, - }} - customTokenListGenerator={toChain ? toTokenListGenerator : undefined} + networkProps={ + isSwap + ? undefined + : { + network: toChain, + networks: toChains, + onNetworkChange: (networkConfig) => { + networkConfig.chainId !== toChain?.chainId && + trackInputEvent({ + input: 'chain_destination', + value: networkConfig.chainId, + }); + dispatch(setToChainId(networkConfig.chainId)); + dispatch(setToToken(null)); + }, + header: isSwap ? t('swapSwapTo') : t('bridgeTo'), + shouldDisableNetwork: ({ chainId }) => + chainId === fromChain?.chainId, + } + } + customTokenListGenerator={ + // TODO use custom generator when we have a way to get all tokens for an unimported chain + toChain && !isSwap ? toTokenListGenerator : undefined + } amountInFiat={ activeQuote?.toTokenAmount?.valueInCurrency || undefined } From c1f1f6bec6149120810ea2359e6a5d7750d38186 Mon Sep 17 00:00:00 2001 From: George Marshall Date: Wed, 19 Feb 2025 10:42:33 -0800 Subject: [PATCH 11/14] fix: text visibility issues in error page in dark mode (#30408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses an issue where the error page text was not visible in dark mode due to missing theme handling and color inheritance. The changes ensure proper text visibility regardless of the user's system theme by: 1. Adding `TextColor.inherit` to all text components in the error page to ensure proper color inheritance 2. Fixing the theme handling in base styles to properly apply dark mode colors 3. Ensuring the banner alert text inherits the correct color 4. Adding max-width to textarea components to prevent overflow issues 5. Adding comprehensive Storybook stories for the error page component to improve test coverage and enable isolated development [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30408?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/30407 ## **Manual testing steps** 1. Set your system to dark mode 2. Trigger an error in MetaMask by setting [if (error)](https://github.com/MetaMask/metamask-extension/blob/7d050e1e7af78d663e862ddf8d034c49590101d6/ui/pages/index.js#L34) { to ` if (true) {` in `ui/pages/index.js` 3. Verify the error page text is visible and properly contrasted 4. Switch to light mode and repeat steps 2-3 5. Test the `Textarea` component to ensure it doesn't overflow its container 6. Run Storybook and verify all error page stories render correctly ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/3bea795f-4507-4ce0-8b6c-e6f7e50d8b7c ### **After** https://github.com/user-attachments/assets/5111f8ed-a57f-4857-a5df-1481a97d04ca https://github.com/user-attachments/assets/30f1c9cc-cf7d-4826-b567-8a46b170e3c7 ## **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/main/.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/main/.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. --- .../component-library/textarea/textarea.scss | 2 ++ ui/css/base-styles.scss | 3 +++ ui/pages/error-page/error-page.component.tsx | 26 ++++++++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ui/components/component-library/textarea/textarea.scss b/ui/components/component-library/textarea/textarea.scss index d6ea35fee110..fc312562ae1c 100644 --- a/ui/components/component-library/textarea/textarea.scss +++ b/ui/components/component-library/textarea/textarea.scss @@ -1,6 +1,8 @@ .mm-textarea { $resize: none, both, horizontal, vertical, initial, inherit; + max-width: 100%; + &--is-disabled, &:disabled { opacity: var(--opacity-disabled); diff --git a/ui/css/base-styles.scss b/ui/css/base-styles.scss index 408e0e0eb973..7d252baa4c65 100644 --- a/ui/css/base-styles.scss +++ b/ui/css/base-styles.scss @@ -17,7 +17,10 @@ body { html { min-height: 500px; +} +html, +body { @include design-system.screen-sm-max { &:not([data-theme]) { background-color: var(--color-background-default); diff --git a/ui/pages/error-page/error-page.component.tsx b/ui/pages/error-page/error-page.component.tsx index 9a094057dcc0..17da4edab91d 100644 --- a/ui/pages/error-page/error-page.component.tsx +++ b/ui/pages/error-page/error-page.component.tsx @@ -110,16 +110,27 @@ const ErrorPage: React.FC = ({ error }) => { size={IconSize.Xl} color={IconColor.warningDefault} /> - + {t('errorPageTitle')}
- {t('errorPageInfo')} + + {t('errorPageInfo')} +
- {t('errorPageMessageTitle')} + + {t('errorPageMessageTitle')} + = ({ error }) => { variant={TextVariant.bodyXs} marginBottom={2} data-testid="error-page-error-message" + color={TextColor.inherit} > {t('errorMessage', [error.message])} @@ -145,6 +157,7 @@ const ErrorPage: React.FC = ({ error }) => { variant={TextVariant.bodyXs} marginBottom={2} data-testid="error-page-error-code" + color={TextColor.inherit} > {t('errorCode', [error.code])} @@ -154,13 +167,18 @@ const ErrorPage: React.FC = ({ error }) => { variant={TextVariant.bodyXs} marginBottom={2} data-testid="error-page-error-name" + color={TextColor.inherit} > {t('errorName', [error.name])} ) : null} {error.stack ? ( <> - + {t('errorStack')}
Date: Wed, 19 Feb 2025 10:48:12 -0800
Subject: [PATCH 12/14] fix: cp-12.13.0 fixes drag and drop in network list
 menu modal (#30437)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit



## **Description**

This PR fixes an issue with the drag-and-drop functionality in the
network list menu by isolating the modal animation to only use opacity
instead of transform. The transform animation was interfering with the
`react-beautiful-dnd` library's positioning calculations, causing the
draggable items to behave incorrectly.

The solution introduces a custom `network-menu-fade` animation in the
`index.scss` file for the network menu modal, which only animates
opacity and removes the `transform` property. This ensures the
drag-and-drop functionality works as expected while maintaining a smooth
fade-in animation.

### Key Changes:
- Added a custom `network-menu-fade` animation in `index.scss`.
- Removed `transform` from the modal to fix drag-and-drop behavior.
- Isolated the animation to the network menu component to avoid
affecting other modals.

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30437?quickstart=1)

## **Related issues**

Fixes: #30436

## **Manual testing steps**

1. Open the MetaMask extension.
2. Click on the network selector in the top left corner.
3. Try to drag and reorder any of the enabled networks.
4. Verify that the drag-and-drop functionality works smoothly and the
modal fades in correctly.

## **Screenshots/Recordings**



### **Before**


- Dragging network items was inconsistent or broken due to transform
interference.


https://github.com/user-attachments/assets/e26cf959-c064-4f12-bad7-6c2308caf50c

### **After**


- Network items can be dragged and reordered smoothly.
- The modal fades in without any transform-related issues.


https://github.com/user-attachments/assets/9052359d-98f6-4267-8d60-92cdef44388b

## **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/main/.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/main/.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.
---
 .../multichain/network-list-menu/index.scss      | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/ui/components/multichain/network-list-menu/index.scss b/ui/components/multichain/network-list-menu/index.scss
index 33e4c2b6caae..d8b60b1958ef 100644
--- a/ui/components/multichain/network-list-menu/index.scss
+++ b/ui/components/multichain/network-list-menu/index.scss
@@ -2,6 +2,22 @@
   &__dialog {
     height: 100vh;
     max-height: 100%;
+    // Needed to ensure the drag and drop works. All other modals use a slide up animation with transform: translateY(0)
+    // https://github.com/MetaMask/metamask-extension/issues/30436
+    transform: unset;
+
+    // Custom animation for network menu modal that doesn't use transform: translateY(0) to ensure the drag and drop works
+    animation: network-menu-fade 400ms cubic-bezier(0.3, 0.8, 0.3, 1) forwards;
+  }
+}
+
+@keyframes network-menu-fade {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
   }
 }
 

From e04e2f78af094e0caad19116c0872b0649be0187 Mon Sep 17 00:00:00 2001
From: David Murdoch <187813+davidmurdoch@users.noreply.github.com>
Date: Wed, 19 Feb 2025 15:18:19 -0500
Subject: [PATCH 13/14] refactor: remove circular dependency between
 `actions.ts` and `swaps.util.ts` (#30306)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This PR solves an issue with circular dependencies. It does not change
the behavior of the application. It only rearranges functions/files to
avoid circular references.


---
 development/circular-deps.jsonc   |  4 ----
 ui/pages/swaps/swaps.util.gas.ts  | 15 +++++++++++++++
 ui/pages/swaps/swaps.util.test.js | 16 ++++++++--------
 ui/pages/swaps/swaps.util.ts      |  2 +-
 ui/store/actions.ts               |  9 ---------
 5 files changed, 24 insertions(+), 22 deletions(-)
 create mode 100644 ui/pages/swaps/swaps.util.gas.ts

diff --git a/development/circular-deps.jsonc b/development/circular-deps.jsonc
index 1fa13db041a0..d36302a5e26d 100644
--- a/development/circular-deps.jsonc
+++ b/development/circular-deps.jsonc
@@ -58,9 +58,5 @@
     "ui/pages/snap-account-redirect/components/snap-account-redirect-message.tsx",
     "ui/pages/snap-account-redirect/components/url-display-box.tsx",
     "ui/pages/snap-account-redirect/snap-account-redirect.tsx"
-  ],
-  [
-    "ui/pages/swaps/swaps.util.ts",
-    "ui/store/actions.ts"
   ]
 ]
diff --git a/ui/pages/swaps/swaps.util.gas.ts b/ui/pages/swaps/swaps.util.gas.ts
new file mode 100644
index 000000000000..bc81b547c82b
--- /dev/null
+++ b/ui/pages/swaps/swaps.util.gas.ts
@@ -0,0 +1,15 @@
+import { NetworkClientId } from '@metamask/network-controller';
+import {
+  GasFeeEstimates,
+  TransactionParams,
+} from '@metamask/transaction-controller';
+import { Hex } from '@metamask/utils';
+import { submitRequestToBackground } from '../../store/background-connection';
+
+export function estimateGasFee(request: {
+  transactionParams: TransactionParams;
+  chainId?: Hex;
+  networkClientId?: NetworkClientId;
+}): Promise<{ estimates: GasFeeEstimates }> {
+  return submitRequestToBackground('estimateGasFee', [request]);
+}
diff --git a/ui/pages/swaps/swaps.util.test.js b/ui/pages/swaps/swaps.util.test.js
index d081e8d58ee1..0973577638de 100644
--- a/ui/pages/swaps/swaps.util.test.js
+++ b/ui/pages/swaps/swaps.util.test.js
@@ -18,13 +18,6 @@ import {
   LINEA,
   BASE,
 } from '../../../shared/constants/swaps';
-import { estimateGasFee } from '../../store/actions';
-import {
-  TOKENS,
-  EXPECTED_TOKENS_RESULT,
-  AGGREGATOR_METADATA,
-  TOP_ASSETS,
-} from './swaps-util-test-constants';
 import {
   fetchTokens,
   fetchAggregatorMetadata,
@@ -39,13 +32,20 @@ import {
   fetchTopAssetsList,
   getSwap1559GasFeeEstimates,
 } from './swaps.util';
+import { estimateGasFee } from './swaps.util.gas';
+import {
+  TOKENS,
+  EXPECTED_TOKENS_RESULT,
+  AGGREGATOR_METADATA,
+  TOP_ASSETS,
+} from './swaps-util-test-constants';
 
 jest.mock('../../../shared/lib/storage-helpers', () => ({
   getStorageItem: jest.fn(),
   setStorageItem: jest.fn(),
 }));
 
-jest.mock('../../store/actions', () => ({
+jest.mock('./swaps.util.gas', () => ({
   estimateGasFee: jest.fn(),
 }));
 
diff --git a/ui/pages/swaps/swaps.util.ts b/ui/pages/swaps/swaps.util.ts
index 7beb613bce58..0ccb2dcc99f7 100644
--- a/ui/pages/swaps/swaps.util.ts
+++ b/ui/pages/swaps/swaps.util.ts
@@ -50,7 +50,7 @@ import {
   sumHexes,
 } from '../../../shared/modules/conversion.utils';
 import { EtherDenomination } from '../../../shared/constants/common';
-import { estimateGasFee } from '../../store/actions';
+import { estimateGasFee } from './swaps.util.gas';
 
 const CACHE_REFRESH_FIVE_MINUTES = 300000;
 const USD_CURRENCY_CODE = 'usd';
diff --git a/ui/store/actions.ts b/ui/store/actions.ts
index deb1c10650e2..033cf0185161 100644
--- a/ui/store/actions.ts
+++ b/ui/store/actions.ts
@@ -29,7 +29,6 @@ import {
   UpdateProposedNamesResult,
 } from '@metamask/name-controller';
 import {
-  GasFeeEstimates,
   TransactionMeta,
   TransactionParams,
   TransactionType,
@@ -4511,14 +4510,6 @@ export function estimateGas(params: TransactionParams): Promise {
   return submitRequestToBackground('estimateGas', [params]);
 }
 
-export function estimateGasFee(request: {
-  transactionParams: TransactionParams;
-  chainId?: Hex;
-  networkClientId?: NetworkClientId;
-}): Promise<{ estimates: GasFeeEstimates }> {
-  return submitRequestToBackground('estimateGasFee', [request]);
-}
-
 export async function updateTokenType(
   tokenAddress: string,
 ): Promise {

From 468ab8fbe86c33d610502e1f050caf52178105d9 Mon Sep 17 00:00:00 2001
From: seaona <54408225+seaona@users.noreply.github.com>
Date: Wed, 19 Feb 2025 21:21:01 +0100
Subject: [PATCH 14/14] fix: flaky test `Speed Up and Cancel Transaction Tests
 Cancel transaction Successfully cancels a pending transaction` (#30435)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit



## **Description**
In our ganache instance, auto-mining is set with `blockTime: 2` , so a
new block is mined every 2 seconds.
In the specs cancel/speed up, we are sending a transaction with low gas,
so then we can speed it up/cancel. The problem is if this whole confirm
and speed up/cancel, happens in the same block (within the 2 seconds).

Then the transaction stays forever in a Pending state, and the spec
fails as it cannot find the correct tx status.
To fix this, we can make sure that we are working with a deterministic
block behaviour. So after we confirm the tx we mine a block, and after
we speed up/cancel, we mine another block.


![image](https://github.com/user-attachments/assets/d100db76-31f4-4df2-b966-b11e42451608)



Ci failure:
https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/125141/workflows/92ff1531-7350-490d-bfd1-811098db7535/jobs/4556227/tests

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30435?quickstart=1)

## **Related issues**

Fixes:

## **Manual testing steps**

1. Check ci webpack job
https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/126169/workflows/f76d3a08-d90e-4cae-8033-897e85cf29d6/jobs/4563925

## **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/main/.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/main/.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.
---
 .../speed-up-and-cancel-confirmations.spec.ts       | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/test/e2e/tests/confirmations/transactions/speed-up-and-cancel-confirmations.spec.ts b/test/e2e/tests/confirmations/transactions/speed-up-and-cancel-confirmations.spec.ts
index bc41a491708f..fd85c073a1d1 100644
--- a/test/e2e/tests/confirmations/transactions/speed-up-and-cancel-confirmations.spec.ts
+++ b/test/e2e/tests/confirmations/transactions/speed-up-and-cancel-confirmations.spec.ts
@@ -10,7 +10,6 @@ import { createDappTransaction } from '../../../page-objects/flows/transaction';
 import Confirmation from '../../../page-objects/pages/confirmations/redesign/confirmation';
 import ActivityListPage from '../../../page-objects/pages/home/activity-list';
 import HomePage from '../../../page-objects/pages/home/homepage';
-import { SMART_CONTRACTS } from '../../../seeder/smart-contracts';
 import { TestSuiteArguments } from './shared';
 
 const { WINDOW_TITLES, withFixtures } = require('../../../helpers');
@@ -28,10 +27,9 @@ describe('Speed Up and Cancel Transaction Tests', function () {
             .withPermissionControllerConnectedToTestDapp()
             .build(),
           localNodeOptions: defaultGanacheOptionsForType2Transactions,
-          smartContract: SMART_CONTRACTS.PIGGYBANK,
           title: this.test?.fullTitle(),
         },
-        async ({ driver }: TestSuiteArguments) => {
+        async ({ driver, ganacheServer }: TestSuiteArguments) => {
           await unlockWallet(driver);
 
           // Create initial stuck transaction
@@ -43,7 +41,6 @@ describe('Speed Up and Cancel Transaction Tests', function () {
           });
 
           // Wait for confirmation dialog and confirm initial transaction
-          await driver.waitUntilXWindowHandles(3);
           await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
 
           const confirmationPage = new Confirmation(driver);
@@ -53,6 +50,7 @@ describe('Speed Up and Cancel Transaction Tests', function () {
           await driver.switchToWindowWithTitle(
             WINDOW_TITLES.ExtensionInFullScreenView,
           );
+          await ganacheServer?.mineBlock();
 
           const homePage = new HomePage(driver);
           await homePage.goToActivityList();
@@ -63,6 +61,7 @@ describe('Speed Up and Cancel Transaction Tests', function () {
           await activityListPage.click_transactionListItem();
           await activityListPage.click_speedUpTransaction();
           await activityListPage.click_confirmTransactionReplacement();
+          await ganacheServer?.mineBlock();
 
           await activityListPage.check_waitForTransactionStatus('confirmed');
         },
@@ -79,10 +78,9 @@ describe('Speed Up and Cancel Transaction Tests', function () {
             .withPermissionControllerConnectedToTestDapp()
             .build(),
           localNodeOptions: defaultGanacheOptionsForType2Transactions,
-          smartContract: SMART_CONTRACTS.PIGGYBANK,
           title: this.test?.fullTitle(),
         },
-        async ({ driver }: TestSuiteArguments) => {
+        async ({ driver, ganacheServer }: TestSuiteArguments) => {
           await unlockWallet(driver);
 
           // Create initial stuck transaction
@@ -93,11 +91,11 @@ describe('Speed Up and Cancel Transaction Tests', function () {
             to: DEFAULT_FIXTURE_ACCOUNT,
           });
 
-          await driver.waitUntilXWindowHandles(3);
           await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
 
           const confirmationPage = new Confirmation(driver);
           await confirmationPage.clickFooterConfirmButton();
+          await ganacheServer?.mineBlock();
 
           await driver.switchToWindowWithTitle(
             WINDOW_TITLES.ExtensionInFullScreenView,
@@ -111,6 +109,7 @@ describe('Speed Up and Cancel Transaction Tests', function () {
 
           await activityListPage.click_cancelTransaction();
           await activityListPage.click_confirmTransactionReplacement();
+          await ganacheServer?.mineBlock();
 
           await activityListPage.check_waitForTransactionStatus('cancelled');
         },