From 13c758a831ad1dbbdd4a3cacfd6dd999c45f9bba Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Wed, 9 Oct 2024 17:09:23 +0000 Subject: [PATCH 01/51] Version v12.4.1 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5fbcff7a94f..7cae12b166c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.4.1] + ## [12.4.0] ### Added - Added a receive button to the home screen, allowing users to easily get their address or QR-code for receiving cryptocurrency ([#26148](https://github.com/MetaMask/metamask-extension/pull/26148)) @@ -5139,7 +5141,8 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...HEAD +[12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1 [12.4.0]: https://github.com/MetaMask/metamask-extension/compare/v12.3.1...v12.4.0 [12.3.1]: https://github.com/MetaMask/metamask-extension/compare/v12.3.0...v12.3.1 [12.3.0]: https://github.com/MetaMask/metamask-extension/compare/v12.2.4...v12.3.0 diff --git a/package.json b/package.json index d8fece95d0c0..19c9a6c0400d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.4.0", + "version": "12.4.1", "private": true, "repository": { "type": "git", From 3d096c2d673aa8756a27d14887f2ea748a2ea780 Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Thu, 10 Oct 2024 06:56:47 -0400 Subject: [PATCH 02/51] fix: remove old phishfort list from clients (#27743) (#27746) Cherry-pick #27743 for v12.4.1 Co-authored-by: Mark Stacey --- app/scripts/migrations/126.1.test.ts | 142 +++++++++++++++++++++++++++ app/scripts/migrations/126.1.ts | 54 ++++++++++ app/scripts/migrations/index.js | 1 + 3 files changed, 197 insertions(+) create mode 100644 app/scripts/migrations/126.1.test.ts create mode 100644 app/scripts/migrations/126.1.ts diff --git a/app/scripts/migrations/126.1.test.ts b/app/scripts/migrations/126.1.test.ts new file mode 100644 index 000000000000..0d21a675ebcc --- /dev/null +++ b/app/scripts/migrations/126.1.test.ts @@ -0,0 +1,142 @@ +import { migrate, version } from './126.1'; + +const oldVersion = 126.1; + +const mockPhishingListMetaMask = { + allowlist: [], + blocklist: ['malicious1.com'], + c2DomainBlocklist: ['malicious2.com'], + fuzzylist: [], + tolerance: 0, + version: 1, + lastUpdated: Date.now(), + name: 'MetaMask', +}; + +const mockPhishingListPhishfort = { + allowlist: [], + blocklist: ['phishfort1.com'], + c2DomainBlocklist: ['phishfort2.com'], + fuzzylist: [], + tolerance: 0, + version: 1, + lastUpdated: Date.now(), + name: 'Phishfort', +}; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('keeps only the MetaMask phishing list in PhishingControllerState', async () => { + const oldState = { + PhishingController: { + phishingLists: [mockPhishingListMetaMask, mockPhishingListPhishfort], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + const updatedPhishingController = transformedState.data + .PhishingController as Record; + + expect(updatedPhishingController.phishingLists).toStrictEqual([ + mockPhishingListMetaMask, + ]); + }); + + it('removes all phishing lists if MetaMask is not present', async () => { + const oldState = { + PhishingController: { + phishingLists: [mockPhishingListPhishfort], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + const updatedPhishingController = transformedState.data + .PhishingController as Record; + + expect(updatedPhishingController.phishingLists).toStrictEqual([]); + }); + + it('does nothing if PhishingControllerState is empty', async () => { + const oldState = { + PhishingController: { + phishingLists: [], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + const updatedPhishingController = transformedState.data + .PhishingController as Record; + + expect(updatedPhishingController.phishingLists).toStrictEqual([]); + }); + + it('does nothing if PhishingController is not in the state', async () => { + const oldState = { + NetworkController: { + providerConfig: { + chainId: '0x1', + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); + + it('does nothing if phishingLists is not an array (null)', async () => { + const oldState: Record = { + PhishingController: { + phishingLists: null, + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/126.1.ts b/app/scripts/migrations/126.1.ts new file mode 100644 index 000000000000..81e609e672f1 --- /dev/null +++ b/app/scripts/migrations/126.1.ts @@ -0,0 +1,54 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 126.1; + +/** + * This migration removes `providerConfig` from the network controller state. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState( + state: Record, +): Record { + if ( + hasProperty(state, 'PhishingController') && + isObject(state.PhishingController) && + hasProperty(state.PhishingController, 'phishingLists') + ) { + const phishingController = state.PhishingController; + + if (!Array.isArray(phishingController.phishingLists)) { + console.error( + `Migration ${version}: Invalid PhishingController.phishingLists state`, + ); + return state; + } + + phishingController.phishingLists = phishingController.phishingLists.filter( + (list) => list.name === 'MetaMask', + ); + + state.PhishingController = phishingController; + } + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index e5cfb6218019..119dfd79ede1 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -146,6 +146,7 @@ const migrations = [ require('./125'), require('./125.1'), require('./126'), + require('./126.1'), ]; export default migrations; From 3ebc8a73f9aed0bef56f1230027b104a92a61bae Mon Sep 17 00:00:00 2001 From: Jack Clancy Date: Thu, 10 Oct 2024 14:06:50 +0100 Subject: [PATCH 03/51] fix: cherry pick swaps undefined object access crash hotfix into v12.4.1 RC (#27757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Cherry picks swaps undefined object access crash hotfix intov12.4.1RC. See [here](https://github.com/MetaMask/metamask-extension/pull/27708) for more info [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27757?quickstart=1) ## **Related issues** https://consensyssoftware.atlassian.net/jira/software/projects/MMS/boards/447/backlog?assignee=5ae37c7e42b8a62c4e15d92a&selectedIssue=MMS-1569 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Mark Stacey --- CHANGELOG.md | 2 ++ ui/pages/swaps/prepare-swap-page/prepare-swap-page.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cae12b166c2..d69b3b7c6fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [12.4.1] +### Fixed +- Fix crash on swaps review page ([27708](https://github.com/MetaMask/metamask-extension/pull/27708)) ## [12.4.0] ### Added diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js index 98bb6933d0c3..45120d9f6a6b 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -52,6 +52,7 @@ import { getTransactionSettingsOpened, setTransactionSettingsOpened, getLatestAddedTokenTo, + getUsedQuote, } from '../../../ducks/swaps/swaps'; import { getSwapsDefaultToken, @@ -190,9 +191,10 @@ export default function PrepareSwapPage({ const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); const tokenList = useSelector(getTokenList, isEqual); const quotes = useSelector(getQuotes, isEqual); + const usedQuote = useSelector(getUsedQuote, isEqual); const latestAddedTokenTo = useSelector(getLatestAddedTokenTo, isEqual); const numberOfQuotes = Object.keys(quotes).length; - const areQuotesPresent = numberOfQuotes > 0; + const areQuotesPresent = numberOfQuotes > 0 && usedQuote; const swapsErrorKey = useSelector(getSwapsErrorKey); const aggregatorMetadata = useSelector(getAggregatorMetadata, shallowEqual); const transactionSettingsOpened = useSelector( From ac46289e78a401af2dc2bcdd7a387cfdd1549e6b Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 10 Oct 2024 16:03:52 -0230 Subject: [PATCH 04/51] Update CHANGELOG.md for v12.4.1 (#27775) --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d69b3b7c6fb1..f7b07834e387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [12.4.1] ### Fixed -- Fix crash on swaps review page ([27708](https://github.com/MetaMask/metamask-extension/pull/27708)) +- Fix crash on swaps review page ([#27708](https://github.com/MetaMask/metamask-extension/pull/27708)) +- Fix bug that could prevent the phishing detection feature from having the most up to date info on which web pages to block ([#27743](https://github.com/MetaMask/metamask-extension/pull/27743)) ## [12.4.0] ### Added From 71de55b8a3c97f17ea92653b1607e2e78f976967 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:32:07 +0200 Subject: [PATCH 05/51] fix: flaky test `ERC1155 NFTs testdapp interaction should batch transfers ERC1155 token` (#27897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Same problem as https://github.com/MetaMask/metamask-extension/pull/27889. We are looking for transactions by its text in the activity tab, but the transaction element updates its state, from pending to confirm, meaning that it can become stale when we do the assertion after. ``` await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); assert.equal(await transactionItem.isDisplayed(), true); ``` ![Screenshot from 2024-10-16 12-05-08](https://github.com/user-attachments/assets/df2066a1-b692-4e5f-9961-6e2e4626aa00) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27897?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27896 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../tokens/nft/erc1155-interaction.spec.js | 150 ++++++++---------- 1 file changed, 66 insertions(+), 84 deletions(-) diff --git a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js index 31425140c7f4..1fed3946dea9 100644 --- a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js @@ -38,33 +38,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#batchMintButton'); // Notification - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Mint await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -90,33 +84,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.fill('#batchTransferTokenAmounts', '1, 1, 1000000000000'); await driver.clickElement('#batchTransferFromButton'); - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Transfer await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -147,26 +135,20 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#setApprovalForAllERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - const displayedMessageTitle = await driver.findElement( - '[data-testid="confirm-approve-title"]', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector({ + css: '[data-testid="confirm-approve-title"]', + text: expectedMessageTitle, + }); + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -185,27 +167,29 @@ describe('ERC1155 NFTs testdapp interaction', function () { '.set-approval-for-all-warning__content__header', ); assert.equal(await displayedWarning.getText(), expectedWarningMessage); - await driver.clickElement({ text: 'Approve', tag: 'button' }); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Approve', + tag: 'button', + }); // Switch to extension and check set approval for all transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const setApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await setApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that set approval for all action completed message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - const setApprovalStatus = await driver.findElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.waitForSelector({ css: '#erc1155Status', text: 'Set Approval For All completed', }); - assert.equal(await setApprovalStatus.isDisplayed(), true); }, ); }); @@ -235,27 +219,22 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#revokeERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const displayedMessageTitle = await driver.findElement( - '.confirm-approve-content__title', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.waitForSelector({ + css: '.confirm-approve-content__title', + text: expectedMessageTitle, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -269,22 +248,25 @@ describe('ERC1155 NFTs testdapp interaction', function () { assert.equal(await params.getText(), 'Parameters: false'); // Click on extension popup to confirm revoke approval for all - await driver.clickElement('[data-testid="page-container-footer-next"]'); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); // Switch to extension and check revoke approval transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const revokeApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await revokeApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that revoke approval for all message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const revokeApprovalStatus = await driver.findElement({ css: '#erc1155Status', text: 'Revoke completed', From 130bdbf5d02702ef4227644aa7827715847e00fb Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:43:59 -0400 Subject: [PATCH 06/51] test: [POM] Migrate signature with snap account e2e tests to page object modal (#27829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the snap account signature e2e tests to the Page Object Model (POM) pattern, improving test stability and maintainability. Changes include: - Migrate test `snap-account-signatures.spec.ts` to POM - Created all signature related functions in TestDapp class - Avoid several delays in the original function implementation - Reduced flakiness [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: #27835 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chloe Gao Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com> Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- .../accounts/snap-account-signatures.spec.ts | 51 --- test/e2e/page-objects/flows/sign.flow.ts | 169 +++++++++ .../pages/experimental-settings.ts | 10 +- .../pages/snap-simple-keyring-page.ts | 42 ++- test/e2e/page-objects/pages/test-dapp.ts | 335 +++++++++++++++++- .../account/snap-account-settings.spec.ts | 2 +- .../account/snap-account-signatures.spec.ts | 100 ++++++ ...55-revoke-set-approval-for-all-redesign.ts | 2 +- ...1155-set-approval-for-all-redesign.spec.ts | 2 +- ...21-revoke-set-approval-for-all-redesign.ts | 2 +- ...c721-set-approval-for-all-redesign.spec.ts | 2 +- 11 files changed, 624 insertions(+), 93 deletions(-) delete mode 100644 test/e2e/accounts/snap-account-signatures.spec.ts create mode 100644 test/e2e/page-objects/flows/sign.flow.ts create mode 100644 test/e2e/tests/account/snap-account-signatures.spec.ts diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts deleted file mode 100644 index 536d8168b1a3..000000000000 --- a/test/e2e/accounts/snap-account-signatures.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Suite } from 'mocha'; -import { - tempToggleSettingRedesignedConfirmations, - withFixtures, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - installSnapSimpleKeyring, - makeNewAccountAndSwitch, - signData, -} from './common'; - -describe('Snap Account Signatures', function (this: Suite) { - this.timeout(120000); // This test is very long, so we need an unusually high timeout - - // Run sync, async approve, and async reject flows - // (in Jest we could do this with test.each, but that does not exist here) - ['sync', 'approve', 'reject'].forEach((flowType) => { - // generate title of the test from flowType - const title = `can sign with ${flowType} flow`; - - it(title, async () => { - await withFixtures( - accountSnapFixtures(title), - async ({ driver }: { driver: Driver }) => { - const isAsyncFlow = flowType !== 'sync'; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - - const newPublicKey = await makeNewAccountAndSwitch(driver); - - await tempToggleSettingRedesignedConfirmations(driver); - - // Run all 5 signature types - const locatorIDs = [ - '#personalSign', - '#signTypedData', - '#signTypedDataV3', - '#signTypedDataV4', - '#signPermit', - ]; - - for (const locatorID of locatorIDs) { - await signData(driver, locatorID, newPublicKey, flowType); - } - }, - ); - }); - }); -}); diff --git a/test/e2e/page-objects/flows/sign.flow.ts b/test/e2e/page-objects/flows/sign.flow.ts new file mode 100644 index 000000000000..c7d03bb4f96e --- /dev/null +++ b/test/e2e/page-objects/flows/sign.flow.ts @@ -0,0 +1,169 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import TestDapp from '../pages/test-dapp'; + +/** + * This function initiates the steps for a personal sign with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const personalSignWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.personalSign(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successPersonalSign(publicAddress); + } else { + await testDapp.check_failedPersonalSign( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedData with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedData(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedData(publicAddress); + } else { + await testDapp.check_failedSignTypedData( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV3 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV3WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV3(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV3(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV3( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV4 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV4WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV4(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV4(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV4( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signPermit with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signPermitWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signPermit(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignPermit(publicAddress); + } else { + await testDapp.check_failedSignPermit( + 'Error: Request rejected by user or snap.', + ); + } +}; diff --git a/test/e2e/page-objects/pages/experimental-settings.ts b/test/e2e/page-objects/pages/experimental-settings.ts index 8c7129b17555..7cd780229acd 100644 --- a/test/e2e/page-objects/pages/experimental-settings.ts +++ b/test/e2e/page-objects/pages/experimental-settings.ts @@ -9,9 +9,12 @@ class ExperimentalSettings { private readonly experimentalPageTitle: object = { text: 'Experimental', - css: '.h4', + tag: 'h4', }; + private readonly redesignedSignatureToggle: string = + '[data-testid="toggle-redesigned-confirmations-container"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -33,6 +36,11 @@ class ExperimentalSettings { console.log('Toggle Add Account Snap on experimental setting page'); await this.driver.clickElement(this.addAccountSnapToggle); } + + async toggleRedesignedSignature(): Promise { + console.log('Toggle Redesigned Signature on experimental setting page'); + await this.driver.clickElement(this.redesignedSignatureToggle); + } } export default ExperimentalSettings; diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts index 7f7a97d7d861..c75adb06da3a 100644 --- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -9,11 +9,6 @@ class SnapSimpleKeyringPage { tag: 'h3', }; - private readonly accountSupportedMethods = { - text: 'Account Supported Methods', - tag: 'p', - }; - private readonly addtoMetamaskMessage = { text: 'Add to MetaMask', tag: 'h3', @@ -104,6 +99,11 @@ class SnapSimpleKeyringPage { tag: 'div', }; + private readonly newAccountMessage = { + text: '"address":', + tag: 'div', + }; + private readonly pageTitle = { text: 'Snap Simple Keyring', tag: 'p', @@ -161,16 +161,25 @@ class SnapSimpleKeyringPage { * Approves or rejects a transaction from a snap account on Snap Simple Keyring page. * * @param approveTransaction - Indicates if the transaction should be approved. Defaults to true. + * @param isSignatureRequest - Indicates if the request is a signature request. Defaults to false. */ async approveRejectSnapAccountTransaction( approveTransaction: boolean = true, + isSignatureRequest: boolean = false, ): Promise { console.log( 'Approve/Reject snap account transaction on Snap Simple Keyring page', ); - await this.driver.clickElementAndWaitToDisappear( - this.confirmationSubmitButton, - ); + if (isSignatureRequest) { + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + } else { + // For send eth requests, the origin screen is not closed automatically, so we cannot call clickElementAndWaitForWindowToClose here. + await this.driver.clickElementAndWaitToDisappear( + this.confirmationSubmitButton, + ); + } await this.driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, ); @@ -242,7 +251,7 @@ class SnapSimpleKeyringPage { await this.driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, ); - await this.check_accountSupportedMethodsDisplayed(); + await this.driver.waitForSelector(this.newAccountMessage); } async confirmCreateSnapOnConfirmationScreen(): Promise { @@ -255,15 +264,21 @@ class SnapSimpleKeyringPage { * * @param accountName - Optional: name for the snap account. Defaults to "SSK Account". * @param isFirstAccount - Indicates if this is the first snap account being created. Defaults to true. + * @returns the public key of the new created account */ async createNewAccount( accountName: string = 'SSK Account', isFirstAccount: boolean = true, - ): Promise { + ): Promise { console.log('Create new account on Snap Simple Keyring page'); await this.openCreateSnapAccountConfirmationScreen(isFirstAccount); await this.confirmCreateSnapOnConfirmationScreen(); await this.confirmAddAccountDialog(accountName); + const newAccountJSONMessage = await ( + await this.driver.waitForSelector(this.newAccountMessage) + ).getText(); + const newPublicKey = JSON.parse(newAccountJSONMessage).address; + return newPublicKey; } /** @@ -331,13 +346,6 @@ class SnapSimpleKeyringPage { await this.driver.clickElement(this.useSyncApprovalToggle); } - async check_accountSupportedMethodsDisplayed(): Promise { - console.log( - 'Check new created account supported methods are displayed on simple keyring snap page', - ); - await this.driver.waitForSelector(this.accountSupportedMethods); - } - async check_errorRequestMessageDisplayed(): Promise { console.log( 'Check error request message is displayed on snap simple keyring page', diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 89ee6bc9cbd3..ffb1f9033bdb 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -1,5 +1,5 @@ import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { WINDOW_TITLES } from '../../helpers'; const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; @@ -7,40 +7,120 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; class TestDapp { private driver: Driver; - private erc721SetApprovalForAllButton: RawLocator; + private readonly confirmDialogScrollButton = + '[data-testid="signature-request-scroll-button"]'; - private erc1155SetApprovalForAllButton: RawLocator; + private readonly confirmSignatureButton = + '[data-testid="page-container-footer-next"]'; - private erc721RevokeSetApprovalForAllButton: RawLocator; + private readonly erc1155RevokeSetApprovalForAllButton = + '#revokeERC1155Button'; - private erc1155RevokeSetApprovalForAllButton: RawLocator; + private readonly erc1155SetApprovalForAllButton = + '#setApprovalForAllERC1155Button'; + + private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton'; + + private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton'; + + private readonly mmlogo = '#mm-logo'; + + private readonly personalSignButton = '#personalSign'; + + private readonly personalSignResult = '#personalSignVerifyECRecoverResult'; + + private readonly personalSignSignatureRequestMessage = { + text: 'personal_sign', + tag: 'div', + }; + + private readonly personalSignVerifyButton = '#personalSignVerify'; + + private readonly signPermitButton = '#signPermit'; + + private readonly signPermitResult = '#signPermitResult'; + + private readonly signPermitSignatureRequestMessage = { + text: 'Permit', + tag: 'p', + }; + + private readonly signPermitVerifyButton = '#signPermitVerify'; + + private readonly signPermitVerifyResult = '#signPermitVerifyResult'; + + private readonly signTypedDataButton = '#signTypedData'; + + private readonly signTypedDataResult = '#signTypedDataResult'; + + private readonly signTypedDataSignatureRequestMessage = { + text: 'Hi, Alice!', + tag: 'div', + }; + + private readonly signTypedDataV3Button = '#signTypedDataV3'; + + private readonly signTypedDataV3Result = '#signTypedDataV3Result'; + + private readonly signTypedDataV3V4SignatureRequestMessage = { + text: 'Hello, Bob!', + tag: 'div', + }; + + private readonly signTypedDataV3VerifyButton = '#signTypedDataV3Verify'; + + private readonly signTypedDataV3VerifyResult = '#signTypedDataV3VerifyResult'; + + private readonly signTypedDataV4Button = '#signTypedDataV4'; + + private readonly signTypedDataV4Result = '#signTypedDataV4Result'; + + private readonly signTypedDataV4VerifyButton = '#signTypedDataV4Verify'; + + private readonly signTypedDataV4VerifyResult = '#signTypedDataV4VerifyResult'; + + private readonly signTypedDataVerifyButton = '#signTypedDataVerify'; + + private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult'; constructor(driver: Driver) { this.driver = driver; + } - this.erc721SetApprovalForAllButton = '#setApprovalForAllButton'; - this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; - this.erc721RevokeSetApprovalForAllButton = '#revokeButton'; - this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.mmlogo); + } catch (e) { + console.log('Timeout while waiting for Test Dapp page to be loaded', e); + throw e; + } + console.log('Test Dapp page is loaded'); } - async open({ - contractAddress, + /** + * Open the test dapp page. + * + * @param options - The options for opening the test dapp page. + * @param options.contractAddress - The contract address to open the dapp with. Defaults to null. + * @param options.url - The URL of the dapp. Defaults to DAPP_URL. + * @returns A promise that resolves when the new page is opened. + */ + async openTestDappPage({ + contractAddress = null, url = DAPP_URL, }: { - contractAddress?: string; + contractAddress?: string | null; url?: string; - }) { + } = {}): Promise { const dappUrl = contractAddress ? `${url}/?contract=${contractAddress}` : url; - - return await this.driver.openNewPage(dappUrl); + await this.driver.openNewPage(dappUrl); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async request(method: string, params: any[]) { - await this.open({ + await this.openTestDappPage({ url: `${DAPP_URL}/request?method=${method}¶ms=${JSON.stringify( params, )}`, @@ -55,13 +135,230 @@ class TestDapp { await this.driver.clickElement(this.erc1155SetApprovalForAllButton); } - public async clickERC721RevokeSetApprovalForAllButton() { + async clickERC721RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc721RevokeSetApprovalForAllButton); } - public async clickERC1155RevokeSetApprovalForAllButton() { + async clickERC1155RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton); } -} + /** + * Verify the failed personal sign signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedPersonalSign(expectedFailedMessage: string) { + console.log('Verify failed personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.personalSignButton, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signPermit signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignPermit(expectedFailedMessage: string) { + console.log('Verify failed signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signPermitResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedData signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedData(expectedFailedMessage: string) { + console.log('Verify failed signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV3 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV3(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV3Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV4 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV4(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV4Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the successful personal sign signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successPersonalSign(publicKey: string) { + console.log('Verify successful personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.personalSignVerifyButton); + await this.driver.waitForSelector({ + css: this.personalSignResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signPermit signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignPermit(publicKey: string) { + console.log('Verify successful signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signPermitVerifyButton); + await this.driver.waitForSelector({ + css: this.signPermitVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedData signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedData(publicKey: string) { + console.log('Verify successful signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataVerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV3 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV3(publicKey: string) { + console.log('Verify successful signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV3VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV3VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV4 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV4(publicKey: string) { + console.log('Verify successful signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV4VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV4VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Sign a message with the personal sign method. + */ + async personalSign() { + console.log('Sign message with personal sign'); + await this.driver.clickElement(this.personalSignButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.personalSignSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign message with the signPermit method. + */ + async signPermit() { + console.log('Sign message with signPermit'); + await this.driver.clickElement(this.signPermitButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.signPermitSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedData method. + */ + async signTypedData() { + console.log('Sign message with signTypedData'); + await this.driver.clickElement(this.signTypedDataButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataSignatureRequestMessage, + ); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV3 method. + */ + async signTypedDataV3() { + console.log('Sign message with signTypedDataV3'); + await this.driver.clickElement(this.signTypedDataV3Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV4 method. + */ + async signTypedDataV4() { + console.log('Sign message with signTypedDataV4'); + await this.driver.clickElement(this.signTypedDataV4Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } +} export default TestDapp; diff --git a/test/e2e/tests/account/snap-account-settings.spec.ts b/test/e2e/tests/account/snap-account-settings.spec.ts index cbd5f8814b7b..1a0c761fb4df 100644 --- a/test/e2e/tests/account/snap-account-settings.spec.ts +++ b/test/e2e/tests/account/snap-account-settings.spec.ts @@ -33,7 +33,7 @@ describe('Add snap account experimental settings @no-mmi', function (this: Suite await settingsPage.goToExperimentalSettings(); const experimentalSettings = new ExperimentalSettings(driver); - await settingsPage.check_pageIsLoaded(); + await experimentalSettings.check_pageIsLoaded(); await experimentalSettings.toggleAddAccountSnap(); // Make sure the "Add account Snap" button is visible. diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts new file mode 100644 index 000000000000..f5010fb61269 --- /dev/null +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -0,0 +1,100 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES, withFixtures } from '../../helpers'; +import ExperimentalSettings from '../../page-objects/pages/experimental-settings'; +import FixtureBuilder from '../../fixture-builder'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + personalSignWithSnapAccount, + signPermitWithSnapAccount, + signTypedDataV3WithSnapAccount, + signTypedDataV4WithSnapAccount, + signTypedDataWithSnapAccount, +} from '../../page-objects/flows/sign.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; + +describe('Snap Account Signatures @no-mmi', function (this: Suite) { + // Run sync, async approve, and async reject flows + // (in Jest we could do this with test.each, but that does not exist here) + + ['sync', 'approve', 'reject'].forEach((flowType) => { + // generate title of the test from flowType + const title = `can sign with ${flowType} flow`; + + it(title, async () => { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp({ + restrictReturnedAccounts: false, + }) + .build(), + title, + }, + async ({ driver }: { driver: Driver }) => { + const isSyncFlow = flowType === 'sync'; + const approveTransaction = flowType === 'approve'; + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver, isSyncFlow); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const newPublicKey = await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to experimental settings and disable redesigned signature. + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleRedesignedSignature(); + + // Run all 5 signature types + await new TestDapp(driver).openTestDappPage(); + await personalSignWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV3WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV4WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signPermitWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts index 7f26e02a572c..3e75adb34db8 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts @@ -57,7 +57,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts index 438b3e979d0a..0e1134737c87 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts index 5a8dcd3768f7..138695904e55 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts @@ -80,7 +80,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts index 7ca9518cabc2..589670212be1 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); From fd1fad8c1f3056b2364ef75b1c22a1305cc29203 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 16 Oct 2024 06:10:47 -0700 Subject: [PATCH 07/51] feat: Use requested permissions as default selected values for AmonHenV2 connection flow with case insensitive address comparison (#27517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Previously, the permission approval component for the AmonHenV2 Flow (accounts + permittedChains in one view) did not consider the caveat values of the requested permission as valid defaults. This PR makes the `ConnectPage` component use any caveat values in the permission request as the default selected before falling back to the previous default logic (currently selected account + all non test networks). Also adds case insensitive account address comparison to related flow [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27517?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** `CHAIN_PERMISSIONS=1 yarn start` ``` await window.ethereum.request({ "method": "wallet_requestPermissions", "params": [ { eth_accounts: { caveats: [ { type: 'restrictReturnedAccounts', value: ['0x5bA08AF1bc30f17272178bDcACA1C74e94955cF4'] } ] } } ], }); ``` ``` await window.ethereum.request({ "method": "wallet_requestPermissions", "params": [ { 'endowment:permitted-chains': { caveats: [ { type: 'restrictNetworkSwitching', value: ['0x1'] } ] } } ], }); ``` OR some combination of the above. You should see the accounts/chains in the request as the default is provided. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../edit-accounts-modal.tsx | 9 +- .../site-cell/site-cell.tsx | 5 +- .../__snapshots__/connect-page.test.tsx.snap | 240 ++++++++++++++++++ .../connect-page/connect-page.test.tsx | 37 +++ .../connect-page/connect-page.tsx | 33 ++- 5 files changed, 319 insertions(+), 5 deletions(-) diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx index ba842efc6a11..084596f07afb 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx @@ -35,6 +35,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; type EditAccountsModalProps = { activeTabOrigin: string; @@ -141,8 +142,12 @@ export const EditAccountsModal: React.FC = ({ isPinned={Boolean(account.pinned)} startAccessory={ + isEqualCaseInsensitive( + selectedAccountAddress, + account.address, + ), )} /> } diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index bb3a14a8f5e8..562d3e8c7d2e 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -19,6 +19,7 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../../shared/constants/metametrics'; +import { isEqualCaseInsensitive } from '../../../../../../shared/modules/string-utils'; import { SiteCellTooltip } from './site-cell-tooltip'; import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; @@ -59,7 +60,9 @@ export const SiteCell: React.FC = ({ const [showEditNetworksModal, setShowEditNetworksModal] = useState(false); const selectedAccounts = accounts.filter(({ address }) => - selectedAccountAddresses.includes(address), + selectedAccountAddresses.some((selectedAccountAddress) => + isEqualCaseInsensitive(selectedAccountAddress, address), + ), ); const selectedNetworks = allNetworks.filter(({ chainId }) => selectedChainIds.includes(chainId), diff --git a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap index e416011c1b08..ad53f67a7127 100644 --- a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap +++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap @@ -249,3 +249,243 @@ exports[`ConnectPage should render correctly 1`] = ` `; + +exports[`ConnectPage should render with defaults from the requested permissions 1`] = ` +
+
+
+
+
+

+

+ Connect with MetaMask +

+

+ This site wants to + : +

+

+
+
+
+
+
+
+ +
+
+

+ See your accounts and suggest transactions +

+
+ + Requesting for Test Account + + +
+
+ +
+
+
+ +
+
+

+ Use your enabled networks +

+
+ + Requesting for + +
Alerts"" + data-tooltipped="" + style="display: inline;" + > +
+
+
+
+ Custom Mainnet RPC logo +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+`; diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx index d7c50c6aa501..ef705e474ad9 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -2,6 +2,11 @@ import React from 'react'; import { renderWithProvider } from '../../../../test/jest/rendering'; import mockState from '../../../../test/data/mock-state.json'; import configureStore from '../../../store/store'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; import { ConnectPage, ConnectPageRequest } from './connect-page'; const render = ( @@ -74,4 +79,36 @@ describe('ConnectPage', () => { expect(confirmButton).toBeDefined(); expect(cancelButton).toBeDefined(); }); + + it('should render with defaults from the requested permissions', () => { + const { container } = render({ + request: { + id: '1', + origin: 'https://test.dapp', + permissions: { + [RestrictedMethods.eth_accounts]: { + caveats: [ + { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], + }, + ], + }, + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + permissionsRequestId: '1', + rejectPermissionsRequest: jest.fn(), + approveConnection: jest.fn(), + activeTabOrigin: 'https://test.dapp', + }); + expect(container).toMatchSnapshot(); + }); }); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index a30047fbd38a..45e6c5b1f48f 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -34,10 +34,19 @@ import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { mergeAccounts } from '../../../components/multichain/account-list-menu/account-list-menu'; import { TEST_CHAINS } from '../../../../shared/constants/network'; import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; export type ConnectPageRequest = { id: string; origin: string; + permissions?: Record< + string, + { caveats?: { type: string; value: string[] }[] } + >; }; type ConnectPageProps = { @@ -57,6 +66,20 @@ export const ConnectPage: React.FC = ({ }) => { const t = useI18nContext(); + const ethAccountsPermission = + request?.permissions?.[RestrictedMethods.eth_accounts]; + const requestedAccounts = + ethAccountsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, + )?.value || []; + + const permittedChainsPermission = + request?.permissions?.[EndowmentTypes.permittedChains]; + const requestedChainIds = + permittedChainsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value || []; + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const [nonTestNetworks, testNetworks] = useMemo( () => @@ -70,7 +93,10 @@ export const ConnectPage: React.FC = ({ ), [networkConfigurations], ); - const defaultSelectedChainIds = nonTestNetworks.map(({ chainId }) => chainId); + const defaultSelectedChainIds = + requestedChainIds.length > 0 + ? requestedChainIds + : nonTestNetworks.map(({ chainId }) => chainId); const [selectedChainIds, setSelectedChainIds] = useState( defaultSelectedChainIds, ); @@ -84,7 +110,10 @@ export const ConnectPage: React.FC = ({ }, [accounts, internalAccounts]); const currentAccount = useSelector(getSelectedInternalAccount); - const defaultAccountsAddresses = [currentAccount?.address]; + const defaultAccountsAddresses = + requestedAccounts.length > 0 + ? requestedAccounts + : [currentAccount?.address]; const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( defaultAccountsAddresses, ); From 56ed6930c59322b5275a94be7f75ca76a89e351f Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:00:47 +0100 Subject: [PATCH 08/51] fix: Contract Interaction - cannot read the property `text_signature` (#27686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces a validation to handle cases where the 4byte response results are either undefined or an empty array. Instead of throwing an error, the code now safely handles these cases by returning undefined, preventing the TypeError from occurring. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27686?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27527 ## **Manual testing steps** 1. Go to Remix 2. Deploy the contract below 3. Trigger the triggerMe func 4. See console error ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Params { uint256 public x; uint256 public value; address public addr; bool public flag; string public text; function triggerMe( uint256 _x, uint256 _value, address _addr, bool _flag, string memory _text ) public returns (bool) { x = _x; value = _value; addr = _addr; flag = _flag; text = _text; return true; } receive() external payable { } } ``` ## **Screenshots/Recordings** [4bytes response.webm](https://github.com/user-attachments/assets/0d6d8ba9-5c43-4c65-ad34-7ea416039c6f) ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/lib/four-byte.test.ts | 27 ++++++++++++++++++++++++--- shared/lib/four-byte.ts | 4 ++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/shared/lib/four-byte.test.ts b/shared/lib/four-byte.test.ts index 2867aa2e51b7..77271c4aeba3 100644 --- a/shared/lib/four-byte.test.ts +++ b/shared/lib/four-byte.test.ts @@ -10,12 +10,14 @@ import { getMethodDataAsync, getMethodFrom4Byte } from './four-byte'; const FOUR_BYTE_MOCK = TRANSACTION_DATA_FOUR_BYTE.slice(0, 10); describe('Four Byte', () => { - const fetchMock = jest.fn(); - describe('getMethodFrom4Byte', () => { - it('returns signature with earliest creation date', async () => { + const fetchMock = jest.fn(); + + beforeEach(() => { jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + }); + it('returns signature with earliest creation date', async () => { fetchMock.mockResolvedValue({ ok: true, json: async () => FOUR_BYTE_RESPONSE, @@ -44,6 +46,25 @@ describe('Four Byte', () => { expect(await getMethodFrom4Byte(prefix)).toBeUndefined(); }, ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + ['undefined', { results: undefined }], + ['object', { results: {} }], + ['empty', { results: [] }], + ])( + 'returns `undefined` if fourByteResponse.results is %s', + async (_: string, mockResponse: { results: unknown }) => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await getMethodFrom4Byte('0x913aa952'); + + expect(result).toBeUndefined(); + }, + ); }); describe('getMethodDataAsync', () => { diff --git a/shared/lib/four-byte.ts b/shared/lib/four-byte.ts index e28f4d4c0c5c..c6b9da22e617 100644 --- a/shared/lib/four-byte.ts +++ b/shared/lib/four-byte.ts @@ -34,6 +34,10 @@ export async function getMethodFrom4Byte( functionName: 'getMethodFrom4Byte', })) as FourByteResponse; + if (!fourByteResponse.results?.length) { + return undefined; + } + fourByteResponse.results.sort((a, b) => { return new Date(a.created_at).getTime() < new Date(b.created_at).getTime() ? -1 From bf87d720cb6c2f98487368dd8df81da078a7d2e7 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 16 Oct 2024 20:24:01 +0530 Subject: [PATCH 09/51] feat: Adding typed sign support for NFT permit (#27796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adding support for NFT permit signature request. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27396 ## **Manual testing steps** 1. Submit an NFT permit request 2. Check the confirmation page that appears ## **Screenshots/Recordings** Screenshot 2024-10-11 at 7 43 59 PM ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/data/confirmations/typed_sign.ts | 15 ++++++ .../components/confirm/title/title.test.tsx | 20 ++++++- .../components/confirm/title/title.tsx | 47 ++++++++++++---- ui/pages/confirmations/constants/index.ts | 5 ++ .../hooks/useTypedSignSignatureInfo.test.js | 27 ++++++++++ .../hooks/useTypedSignSignatureInfo.ts | 53 +++++++++++++++++++ 6 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js create mode 100644 ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index f02705a2540b..7be24a1389c6 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -183,6 +183,21 @@ export const permitSignatureMsg = { }, } as SignatureRequestType; +export const permitNFTSignatureMsg = { + id: 'c5067710-87cf-11ef-916c-71f266571322', + status: 'unapproved', + time: 1728651190529, + type: 'eth_signTypedData', + msgParams: { + data: '{"domain":{"name":"Uniswap V3 Positions NFT-V1","version":"1","chainId":1,"verifyingContract":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88"},"types":{"Permit":[{"name":"spender","type":"address"},{"name":"tokenId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","message":{"spender":"0x00000000Ede6d8D217c60f93191C060747324bca","tokenId":"3606393","nonce":"0","deadline":"1734995006"}}', + from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + signatureMethod: 'eth_signTypedData_v4', + requestId: 2874791875, + origin: 'https://metamask.github.io', + }, +} as SignatureRequestType; + export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', securityAlertResponse: { diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index 3c03343c2afb..3d4d6672940d 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -11,7 +11,10 @@ import { getMockTypedSignConfirmStateForRequest, } from '../../../../../../test/data/confirmations/helper'; import { unapprovedPersonalSignMsg } from '../../../../../../test/data/confirmations/personal_sign'; -import { permitSignatureMsg } from '../../../../../../test/data/confirmations/typed_sign'; +import { + permitNFTSignatureMsg, + permitSignatureMsg, +} from '../../../../../../test/data/confirmations/typed_sign'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { tEn } from '../../../../../../test/lib/i18n-helpers'; import { @@ -71,6 +74,21 @@ describe('ConfirmTitle', () => { ).toBeInTheDocument(); }); + it('should render the title and description for a NFT permit signature', () => { + const mockStore = configureMockStore([])( + getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg), + ); + const { getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(getByText('Withdrawal request')).toBeInTheDocument(); + expect( + getByText('This site wants permission to withdraw your NFTs'), + ).toBeInTheDocument(); + }); + it('should render the title and description for typed signature', () => { const mockStore = configureMockStore([])(getMockTypedSignConfirmState()); const { getByText } = renderWithConfirmContextProvider( diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 2645feed8a41..969e9c05518d 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -3,6 +3,8 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; + +import { TokenStandard } from '../../../../../../shared/constants/transaction'; import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; import { Box, Text } from '../../../../../components/component-library'; import { @@ -12,12 +14,11 @@ import { } from '../../../../../helpers/constants/design-system'; import useAlerts from '../../../../../hooks/useAlerts'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { TypedSignSignaturePrimaryTypes } from '../../../constants'; import { useConfirmContext } from '../../../context/confirm'; import { Confirmation, SignatureRequestType } from '../../../types/confirm'; -import { - isPermitSignatureRequest, - isSIWESignatureRequest, -} from '../../../utils'; +import { isSIWESignatureRequest } from '../../../utils'; +import { useTypedSignSignatureInfo } from '../../../hooks/useTypedSignSignatureInfo'; import { useIsNFT } from '../info/approve/hooks/use-is-nft'; import { useDecodedTransactionData } from '../info/hooks/useDecodedTransactionData'; import { getIsRevokeSetApprovalForAll } from '../info/utils'; @@ -51,6 +52,8 @@ function ConfirmBannerAlert({ ownerId }: { ownerId: string }) { type IntlFunction = (str: string) => string; +// todo: getTitle and getDescription can be merged to remove code duplication. + const getTitle = ( t: IntlFunction, confirmation?: Confirmation, @@ -58,6 +61,8 @@ const getTitle = ( customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { if (pending) { return ''; @@ -74,9 +79,13 @@ const getTitle = ( } return t('confirmTitleSignature'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitlePermitTokens') - : t('confirmTitleSignature'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('setApprovalForAllRedesignedTitle'); + } + return t('confirmTitlePermitTokens'); + } + return t('confirmTitleSignature'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleApproveTransaction'); @@ -104,6 +113,8 @@ const getDescription = ( customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { if (pending) { return ''; @@ -120,9 +131,13 @@ const getDescription = ( } return t('confirmTitleDescSign'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitleDescPermitSignature') - : t('confirmTitleDescSign'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('confirmTitleDescApproveTransaction'); + } + return t('confirmTitleDescPermitSignature'); + } + return t('confirmTitleDescSign'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleDescApproveTransaction'); @@ -150,6 +165,10 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); + const { primaryType, tokenStandard } = useTypedSignSignatureInfo( + currentConfirmation as SignatureRequestType, + ); + const { customSpendingCap, pending: spendingCapPending } = useCurrentSpendingCap(currentConfirmation); @@ -175,6 +194,8 @@ const ConfirmTitle: React.FC = memo(() => { customSpendingCap, isRevokeSetApprovalForAll, spendingCapPending || revokePending, + primaryType, + tokenStandard, ), [ currentConfirmation, @@ -183,6 +204,8 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll, spendingCapPending, revokePending, + primaryType, + tokenStandard, ], ); @@ -195,6 +218,8 @@ const ConfirmTitle: React.FC = memo(() => { customSpendingCap, isRevokeSetApprovalForAll, spendingCapPending || revokePending, + primaryType, + tokenStandard, ), [ currentConfirmation, @@ -203,6 +228,8 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll, spendingCapPending, revokePending, + primaryType, + tokenStandard, ], ); diff --git a/ui/pages/confirmations/constants/index.ts b/ui/pages/confirmations/constants/index.ts index 38fd05b714ba..7e26ce5c6d62 100644 --- a/ui/pages/confirmations/constants/index.ts +++ b/ui/pages/confirmations/constants/index.ts @@ -9,3 +9,8 @@ export const TYPED_SIGNATURE_VERSIONS = { }; export const SPENDING_CAP_UNLIMITED_MSG = 'UNLIMITED MESSAGE'; + +export const TypedSignSignaturePrimaryTypes = { + PERMIT: 'Permit', + ORDER: 'Order', +}; diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js new file mode 100644 index 000000000000..38468749782d --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js @@ -0,0 +1,27 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { permitNFTSignatureMsg } from '../../../../test/data/confirmations/typed_sign'; +import { unapprovedPersonalSignMsg } from '../../../../test/data/confirmations/personal_sign'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; +import { useTypedSignSignatureInfo } from './useTypedSignSignatureInfo'; + +describe('useTypedSignSignatureInfo', () => { + it('should return details for primaty type and token standard', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(permitNFTSignatureMsg), + ); + expect(result.current.primaryType).toStrictEqual( + TypedSignSignaturePrimaryTypes.PERMIT, + ); + expect(result.current.tokenStandard).toStrictEqual(TokenStandard.ERC721); + }); + + it('should return empty object if confirmation is not typed sign', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(unapprovedPersonalSignMsg), + ); + expect(result.current.primaryType).toBeUndefined(); + expect(result.current.tokenStandard).toBeUndefined(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts new file mode 100644 index 000000000000..30d4e58f1525 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; + +import { + isOrderSignatureRequest, + isPermitSignatureRequest, + isSignatureTransactionType, +} from '../utils'; +import { SignatureRequestType } from '../types/confirm'; +import { parseTypedDataMessage } from '../../../../shared/modules/transaction.utils'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; + +export const useTypedSignSignatureInfo = ( + confirmation: SignatureRequestType, +) => { + const primaryType = useMemo(() => { + if ( + !confirmation || + !isSignatureTransactionType(confirmation) || + confirmation?.type !== MESSAGE_TYPE.ETH_SIGN_TYPED_DATA + ) { + return undefined; + } + if (isPermitSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.PERMIT; + } else if (isOrderSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.ORDER; + } + return undefined; + }, [confirmation]); + + // here we are using presence of tokenId in typed message data to know if its NFT permit + // we can get contract details for verifyingContract but that is async process taking longer + // and result in confirmation page content loading late + const tokenStandard = useMemo(() => { + if (primaryType !== TypedSignSignaturePrimaryTypes.PERMIT) { + return undefined; + } + const { + message: { tokenId }, + } = parseTypedDataMessage(confirmation?.msgParams?.data as string); + if (tokenId !== undefined) { + return TokenStandard.ERC721; + } + return undefined; + }, [confirmation, primaryType]); + + return { + primaryType: primaryType as keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard, + }; +}; From a1e0b71a15b8458cae05d61cbbecd0f5d22a4fa6 Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Wed, 16 Oct 2024 08:28:24 -0700 Subject: [PATCH 10/51] test: set ENABLE_MV3 automatically (#27748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The stated problem was "automatically set `ENABLE_MV3` based on the browser, in `run-e2e-test`" However, that would only handle the case of Firefox, and miss builds with: - `start:mv2` - `dist:mv2` - `build:test:flask:mv2` - `build:test:mv2` - Any `webpack` build I had previously written code that reads `manifest.json` from Node, so I thought "hey let's just read it!" When running a test, you now never even have to **think** about `ENABLE_MV3`. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27748?quickstart=1) ## **Related issues** Fixes: #27704 ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 4 ++-- README.md | 5 ++++- package.json | 2 +- shared/modules/mv3.utils.js | 9 ++++----- test/e2e/manifest-flag-mocha-hooks.ts | 4 +++- test/e2e/set-manifest-flags.ts | 21 ++++++++++++++++++--- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b2c5ab712973..2bf244b9bf8a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1250,7 +1250,7 @@ jobs: command: mv ./builds-test-flask-mv2 ./builds - run: name: test:e2e:firefox:flask - command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1393,7 +1393,7 @@ jobs: command: mv ./builds-test-mv2 ./builds - run: name: test:e2e:firefox - command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox no_output_timeout: 5m - store_artifacts: path: test-artifacts diff --git a/README.md b/README.md index 4f15e138be56..f3e738a40abc 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,15 @@ If you are using VS Code and are unable to make commits from the source control To start a development build (e.g. with logging and file watching) run `yarn start`. #### Development build with wallet state + You can start a development build with a preloaded wallet state, by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file. Then you have the following options: + 1. Start the wallet with the default fixture flags, by running `yarn start:with-state`. 2. Check the list of available fixture flags, by running `yarn start:with-state --help`. 3. Start the wallet with custom fixture flags, by running `yarn start:with-state --FIXTURE_NAME=VALUE` for example `yarn start:with-state --withAccounts=100`. You can pass as many flags as you want. The rest of the fixtures will take the default values. #### Development build with Webpack + You can also start a development build using the `yarn webpack` command, or `yarn webpack --watch`. This uses an alternative build system that is much faster, but not yet production ready. See the [Webpack README](./development/webpack/README.md) for more information. #### React and Redux DevTools @@ -191,7 +194,7 @@ Different build types have different e2e tests sets. In order to run them look i ```console "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:snaps": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --snaps", - "test:e2e:firefox": "ENABLE_MV3=false SELENIUM_BROWSER=firefox node test/e2e/run-all.js", + "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", ``` #### Note: Running MMI e2e tests diff --git a/package.json b/package.json index 860bad431285..9a77bb273995 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask", - "test:e2e:chrome:webpack": "ENABLE_MV3=false SELENIUM_BROWSER=chrome node test/e2e/run-all.js", + "test:e2e:chrome:webpack": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:api-specs": "SELENIUM_BROWSER=chrome ts-node test/e2e/run-openrpc-api-test-coverage.ts", "test:e2e:mmi:ci": "yarn playwright test --project=mmi --project=mmi.visual", "test:e2e:mmi:all": "yarn playwright test --project=mmi && yarn test:e2e:mmi:visual", diff --git a/shared/modules/mv3.utils.js b/shared/modules/mv3.utils.js index 417484c46de6..24a4bc8880f4 100644 --- a/shared/modules/mv3.utils.js +++ b/shared/modules/mv3.utils.js @@ -6,14 +6,13 @@ const runtimeManifest = /** * A boolean indicating whether the manifest of the current extension is set to manifest version 3. * - * We have found that when this is run early in a service worker process, the runtime manifest is - * unavailable. That's why we have a fallback using the ENABLE_MV3 constant. The fallback is also - * used in unit tests. + * If this function is running in the Extension, it will use the runtime manifest. + * If this function is running in Node doing a build job, it will read process.env.ENABLE_MV3. + * If this function is running in Node doing an E2E test, it will `fs.readFileSync` the manifest.json file. */ const isManifestV3 = runtimeManifest ? runtimeManifest.manifest_version === 3 - : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it will - // always be a string + : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it can be a string process.env.ENABLE_MV3 === true || process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined; diff --git a/test/e2e/manifest-flag-mocha-hooks.ts b/test/e2e/manifest-flag-mocha-hooks.ts index f319984eb067..f5c8d5e8099c 100644 --- a/test/e2e/manifest-flag-mocha-hooks.ts +++ b/test/e2e/manifest-flag-mocha-hooks.ts @@ -14,7 +14,9 @@ */ import fs from 'fs'; import { hasProperty } from '@metamask/utils'; -import { folder } from './set-manifest-flags'; +import { folder, getManifestVersion } from './set-manifest-flags'; + +process.env.ENABLE_MV3 = getManifestVersion() === 3 ? 'true' : 'false'; // Global beforeEach hook to backup the manifest.json file if (typeof beforeEach === 'function' && process.env.SELENIUM_BROWSER) { diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index 290e8b863a9e..75339250506f 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -5,6 +5,9 @@ import { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; export const folder = `dist/${process.env.SELENIUM_BROWSER}`; +type ManifestType = { _flags?: ManifestFlags; manifest_version: string }; +let manifest: ManifestType; + function parseIntOrUndefined(value: string | undefined): number | undefined { return value ? parseInt(value, 10) : undefined; } @@ -113,11 +116,23 @@ export function setManifestFlags(flags: ManifestFlags = {}) { } } - const manifest = JSON.parse( - fs.readFileSync(`${folder}/manifest.json`).toString(), - ); + readManifest(); manifest._flags = flags; fs.writeFileSync(`${folder}/manifest.json`, JSON.stringify(manifest)); } + +export function getManifestVersion(): number { + readManifest(); + + return parseInt(manifest.manifest_version, 10); +} + +function readManifest() { + if (!manifest) { + manifest = JSON.parse( + fs.readFileSync(`${folder}/manifest.json`).toString(), + ); + } +} From 6d9cc1f5b44223bdea3b362ab14c436207e9015e Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:27:02 -0700 Subject: [PATCH 11/51] feat: use asset pickers with network dropdown in cross-chain swaps page (#27522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Changes include * PrepareBridge - cross-chain swaps landing page, accepts user inputs for quote params * useTokensWithFiltering - new hook for sorting and filtering tokens * useLatestBalance - new hook that returns a user's src chain balance on token selection [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26430?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/METABRIDGE-866 ## **Manual testing steps** 1. Set BRIDGE_USE_DEV_APIS=1 and load extension 2. Click Bridge button 3. Verify that PrepareBridgePage appears 4. Change src/dest chains and tokens; verify that token list is updated as a result 5. Change input value 6. Navigate away from Bridge page using Back button 7. Navigate back to Bridge page and verify that input fields are reset ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-07-12 at 3 44 34 PM](https://github.com/user-attachments/assets/9bcb8552-c1e7-4a4e-b9f3-b70327341d68) ### **After** https://github.com/user-attachments/assets/1779a548-92da-4e80-8974-038d984ac0a0 ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 6 + test/e2e/tests/bridge/bridge-test-utils.ts | 4 - ui/ducks/bridge/actions.ts | 10 +- ui/ducks/bridge/bridge.test.ts | 62 ++- ui/ducks/bridge/bridge.ts | 9 + ui/hooks/bridge/useBridging.test.ts | 2 +- ui/hooks/bridge/useBridging.ts | 12 +- ui/hooks/bridge/useLatestBalance.test.ts | 89 ++++ ui/hooks/bridge/useLatestBalance.ts | 66 +++ ui/hooks/useTokensWithFiltering.test.ts | 153 ++++++ ui/hooks/useTokensWithFiltering.ts | 178 +++++++ .../bridge/__snapshots__/index.test.tsx.snap | 56 ++- ui/pages/bridge/index.scss | 28 ++ ui/pages/bridge/index.tsx | 93 ++-- .../bridge-cta-button.test.tsx.snap | 14 + .../prepare-bridge-page.test.tsx.snap | 456 ++++++++++++++++++ .../bridge/prepare/bridge-cta-button.test.tsx | 50 ++ ui/pages/bridge/prepare/bridge-cta-button.tsx | 49 ++ .../bridge/prepare/bridge-input-group.tsx | 157 ++++++ ui/pages/bridge/prepare/index.scss | 170 +++++++ .../prepare/prepare-bridge-page.test.tsx | 118 +++++ .../bridge/prepare/prepare-bridge-page.tsx | 173 ++++++- .../connect-hardware/index.test.tsx | 4 + ui/pages/pages.scss | 1 + ui/pages/routes/routes.component.js | 11 + 25 files changed, 1873 insertions(+), 98 deletions(-) create mode 100644 ui/hooks/bridge/useLatestBalance.test.ts create mode 100644 ui/hooks/bridge/useLatestBalance.ts create mode 100644 ui/hooks/useTokensWithFiltering.test.ts create mode 100644 ui/hooks/useTokensWithFiltering.ts create mode 100644 ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap create mode 100644 ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap create mode 100644 ui/pages/bridge/prepare/bridge-cta-button.test.tsx create mode 100644 ui/pages/bridge/prepare/bridge-cta-button.tsx create mode 100644 ui/pages/bridge/prepare/bridge-input-group.tsx create mode 100644 ui/pages/bridge/prepare/index.scss create mode 100644 ui/pages/bridge/prepare/prepare-bridge-page.test.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 49d48b9c71ac..6e907c35a088 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -854,9 +854,15 @@ "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeFrom": { + "message": "Bridge from" + }, "bridgeSelectNetwork": { "message": "Select network" }, + "bridgeTo": { + "message": "Bridge to" + }, "browserNotSupported": { "message": "Your browser is not supported..." }, diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 40bb8c6bd97f..1f4a3e5cda79 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -84,10 +84,6 @@ export class BridgePage { verifySwapPage = async (expectedHandleCount: number) => { await this.driver.delay(4000); - await this.driver.waitForSelector({ - css: '.bridge__title', - text: 'Bridge', - }); assert.equal( (await this.driver.getAllWindowHandles()).length, IS_FIREFOX || !isManifestV3 diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 5bfbda1e23cf..5e50b004b774 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -18,9 +18,17 @@ const { setFromToken, setToToken, setFromTokenInputValue, + resetInputFields, + switchToAndFromTokens, } = bridgeSlice.actions; -export { setFromToken, setToToken, setFromTokenInputValue }; +export { + setFromToken, + setToToken, + setFromTokenInputValue, + switchToAndFromTokens, + resetInputFields, +}; const callBridgeControllerMethod = ( bridgeAction: BridgeUserAction | BridgeBackgroundAction, diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index b8d2e09eb0ea..f4a566c233b5 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -17,6 +17,8 @@ import { setToChain, setToToken, setFromChain, + resetInputFields, + switchToAndFromTokens, } from './actions'; const middleware = [thunk]; @@ -43,9 +45,9 @@ describe('Ducks - Bridge', () => { // Check redux state const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setToChainId'); + expect(actions[0].type).toStrictEqual('bridge/setToChainId'); const newState = bridgeReducer(state, actions[0]); - expect(newState.toChainId).toBe(actionPayload); + expect(newState.toChainId).toStrictEqual(actionPayload); // Check background state expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); expect(mockSelectDestNetwork).toHaveBeenCalledWith( @@ -61,9 +63,9 @@ describe('Ducks - Bridge', () => { const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; store.dispatch(setFromToken(actionPayload)); const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setFromToken'); + expect(actions[0].type).toStrictEqual('bridge/setFromToken'); const newState = bridgeReducer(state, actions[0]); - expect(newState.fromToken).toBe(actionPayload); + expect(newState.fromToken).toStrictEqual(actionPayload); }); }); @@ -73,9 +75,9 @@ describe('Ducks - Bridge', () => { const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; store.dispatch(setToToken(actionPayload)); const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setToToken'); + expect(actions[0].type).toStrictEqual('bridge/setToToken'); const newState = bridgeReducer(state, actions[0]); - expect(newState.toToken).toBe(actionPayload); + expect(newState.toToken).toStrictEqual(actionPayload); }); }); @@ -85,9 +87,9 @@ describe('Ducks - Bridge', () => { const actionPayload = '10'; store.dispatch(setFromTokenInputValue(actionPayload)); const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setFromTokenInputValue'); + expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue'); const newState = bridgeReducer(state, actions[0]); - expect(newState.fromTokenInputValue).toBe(actionPayload); + expect(newState.fromTokenInputValue).toStrictEqual(actionPayload); }); }); @@ -118,4 +120,48 @@ describe('Ducks - Bridge', () => { ); }); }); + + describe('resetInputFields', () => { + it('resets to initalState', async () => { + const state = store.getState().bridge; + store.dispatch(resetInputFields()); + const actions = store.getActions(); + expect(actions[0].type).toStrictEqual('bridge/resetInputFields'); + const newState = bridgeReducer(state, actions[0]); + expect(newState).toStrictEqual({ + toChainId: null, + fromToken: null, + toToken: null, + fromTokenInputValue: null, + }); + }); + }); + + describe('switchToAndFromTokens', () => { + it('switches to and from input values', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bridgeStore = configureMockStore(middleware)( + createBridgeMockStore( + {}, + { + toChainId: CHAIN_IDS.MAINNET, + fromToken: { symbol: 'WETH', address: '0x13341432' }, + toToken: { symbol: 'USDC', address: '0x13341431' }, + fromTokenInputValue: '10', + }, + ), + ); + const state = bridgeStore.getState().bridge; + bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON)); + const actions = bridgeStore.getActions(); + expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens'); + const newState = bridgeReducer(state, actions[0]); + expect(newState).toStrictEqual({ + toChainId: CHAIN_IDS.POLYGON, + fromToken: { symbol: 'USDC', address: '0x13341431' }, + toToken: { symbol: 'WETH', address: '0x13341432' }, + fromTokenInputValue: null, + }); + }); + }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index f2469d1025f3..9ec744d9e953 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -36,6 +36,15 @@ const bridgeSlice = createSlice({ setFromTokenInputValue: (state, action) => { state.fromTokenInputValue = action.payload; }, + resetInputFields: () => ({ + ...initialState, + }), + switchToAndFromTokens: (state, { payload }) => ({ + toChainId: payload, + fromToken: state.toToken, + toToken: state.fromToken, + fromTokenInputValue: null, + }), }, }); diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index df8bbb940f4e..6e3f3b534e35 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -174,7 +174,7 @@ describe('useBridging', () => { result.current.openBridgeExperience(location, token, urlSuffix); - expect(mockDispatch.mock.calls).toHaveLength(3); + expect(mockDispatch.mock.calls).toHaveLength(2); expect(mockHistoryPush.mock.calls).toHaveLength(1); expect(mockHistoryPush).toHaveBeenCalledWith(expectedUrl); expect(openTabSpy).not.toHaveBeenCalled(); diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index ce4b8c48b89c..a68aeb361bdd 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -1,10 +1,7 @@ import { useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { - setBridgeFeatureFlags, - setFromChain, -} from '../../ducks/bridge/actions'; +import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getCurrentKeyring, @@ -55,13 +52,6 @@ const useBridging = () => { dispatch(setBridgeFeatureFlags()); }, [dispatch, setBridgeFeatureFlags]); - useEffect(() => { - isBridgeChain && - isBridgeSupported && - providerConfig && - dispatch(setFromChain(providerConfig.chainId)); - }, []); - const openBridgeExperience = useCallback( ( location: string, diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts new file mode 100644 index 000000000000..d1186c3eeb91 --- /dev/null +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -0,0 +1,89 @@ +import { BigNumber } from 'ethers'; +import { renderHookWithProvider } from '../../../test/lib/render-helpers'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { zeroAddress } from '../../__mocks__/ethereumjs-util'; +import { createTestProviderTools } from '../../../test/stub/provider'; +import * as tokenutil from '../../../shared/lib/token-util'; +import useLatestBalance from './useLatestBalance'; + +const mockGetBalance = jest.fn(); +jest.mock('@ethersproject/providers', () => { + return { + Web3Provider: jest.fn().mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }), + }; +}); + +const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); +jest.mock('../../../shared/lib/token-util', () => ({ + ...jest.requireActual('../../../shared/lib/token-util'), + fetchTokenBalance: jest.fn(), +})); + +const renderUseLatestBalance = ( + token: { address: string; decimals?: number | string }, + chainId: string, + mockStoreState: object, +) => + renderHookWithProvider( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => useLatestBalance(token as any, chainId as any), + mockStoreState, + ); + +describe('useLatestBalance', () => { + beforeEach(() => { + jest.clearAllMocks(); + const { provider } = createTestProviderTools({ + networkId: 'Ethereum', + chainId: CHAIN_IDS.MAINNET, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + it('returns formattedBalance for native asset in current chain', async () => { + mockGetBalance.mockResolvedValue(BigNumber.from('1000000000000000000')); + + const { result, waitForNextUpdate } = renderUseLatestBalance( + { address: zeroAddress(), decimals: 18 }, + CHAIN_IDS.MAINNET, + createBridgeMockStore(), + ); + + await waitForNextUpdate(); + expect(result.current.formattedBalance).toStrictEqual('1'); + + expect(mockGetBalance).toHaveBeenCalledTimes(1); + expect(mockGetBalance).toHaveBeenCalledWith( + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + ); + expect(mockFetchTokenBalance).toHaveBeenCalledTimes(0); + }); + + it('returns formattedBalance for ERC20 asset in current chain', async () => { + mockFetchTokenBalance.mockResolvedValueOnce(BigNumber.from('15390000')); + + const { result, waitForNextUpdate } = renderUseLatestBalance( + { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: '6' }, + CHAIN_IDS.MAINNET, + createBridgeMockStore(), + ); + + await waitForNextUpdate(); + expect(result.current.formattedBalance).toStrictEqual('15.39'); + + expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1); + expect(mockFetchTokenBalance).toHaveBeenCalledWith( + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + global.ethereumProvider, + ); + expect(mockGetBalance).toHaveBeenCalledTimes(0); + }); +}); diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts new file mode 100644 index 000000000000..dfe868a04090 --- /dev/null +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -0,0 +1,66 @@ +import { useSelector } from 'react-redux'; +import { zeroAddress } from 'ethereumjs-util'; +import { Web3Provider } from '@ethersproject/providers'; +import { Hex } from '@metamask/utils'; +import { BigNumber } from 'ethers'; +import { Numeric } from '../../../shared/modules/Numeric'; +import { DEFAULT_PRECISION } from '../useCurrencyDisplay'; +import { fetchTokenBalance } from '../../../shared/lib/token-util'; +import { + getCurrentChainId, + getSelectedInternalAccount, + SwapsEthToken, +} from '../../selectors'; +import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { useAsyncResult } from '../useAsyncResult'; + +/** + * Custom hook to fetch and format the latest balance of a given token or native asset. + * + * @param token - The token object for which the balance is to be fetched. Can be null. + * @param chainId - The chain ID to be used for fetching the balance. Optional. + * @returns An object containing the formatted balance as a string. + */ +const useLatestBalance = ( + token: SwapsTokenObject | SwapsEthToken | null, + chainId?: Hex, +) => { + const { address: selectedAddress } = useSelector(getSelectedInternalAccount); + const currentChainId = useSelector(getCurrentChainId); + + const { value: latestBalance } = useAsyncResult(async () => { + if (token && chainId && currentChainId === chainId) { + if (!token.address || token.address === zeroAddress()) { + const ethersProvider = new Web3Provider(global.ethereumProvider); + return await ethersProvider.getBalance(selectedAddress); + } + return await fetchTokenBalance( + token.address, + selectedAddress, + global.ethereumProvider, + ); + } + + return undefined; + }, [token, selectedAddress, global.ethereumProvider]); + + if (token && !token.decimals) { + throw new Error( + `Failed to calculate latest balance - ${token.symbol} token is missing "decimals" value`, + ); + } + + const tokenDecimals = token?.decimals ? Number(token.decimals) : 1; + + return { + formattedBalance: + token && latestBalance + ? Numeric.from(latestBalance.toString(), 10) + .shiftedBy(tokenDecimals) + .round(DEFAULT_PRECISION) + .toString() + : undefined, + }; +}; + +export default useLatestBalance; diff --git a/ui/hooks/useTokensWithFiltering.test.ts b/ui/hooks/useTokensWithFiltering.test.ts new file mode 100644 index 000000000000..f5ea05e02b8d --- /dev/null +++ b/ui/hooks/useTokensWithFiltering.test.ts @@ -0,0 +1,153 @@ +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import { createBridgeMockStore } from '../../test/jest/mock-store'; +import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, + TokenBucketPriority, +} from '../../shared/constants/swaps'; +import { useTokensWithFiltering } from './useTokensWithFiltering'; + +const mockUseTokenTracker = jest + .fn() + .mockReturnValue({ tokensWithBalances: [] }); +jest.mock('./useTokenTracker', () => ({ + useTokenTracker: () => mockUseTokenTracker(), +})); + +const TEST_CHAIN_ID = '0x1'; +const NATIVE_TOKEN = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[TEST_CHAIN_ID]; + +const MOCK_TOP_ASSETS = [ + { address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' }, // UNI + { address: NATIVE_TOKEN.address }, + { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, // USDC + { address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, // USDT +]; + +const MOCK_TOKEN_LIST_BY_ADDRESS: Record = { + [NATIVE_TOKEN.address]: NATIVE_TOKEN, + ...STATIC_MAINNET_TOKEN_LIST, +}; + +describe('useTokensWithFiltering should return token list generator', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('when chainId === activeChainId and sorted by topAssets', () => { + const mockStore = createBridgeMockStore(); + const { result } = renderHookWithProvider( + () => + useTokensWithFiltering( + MOCK_TOKEN_LIST_BY_ADDRESS, + MOCK_TOP_ASSETS, + TokenBucketPriority.top, + TEST_CHAIN_ID, + ), + mockStore, + ); + + expect(result.current).toHaveLength(1); + expect(typeof result.current).toStrictEqual('function'); + const tokenGenerator = result.current(() => true); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + balance: undefined, + decimals: 18, + iconUrl: './images/eth_logo.svg', + identiconAddress: null, + image: './images/eth_logo.svg', + name: 'Ether', + primaryLabel: 'ETH', + rawFiat: '', + rightPrimaryLabel: undefined, + rightSecondaryLabel: '', + secondaryLabel: 'Ether', + symbol: 'ETH', + type: 'NATIVE', + }); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2', + aggregators: [], + balance: undefined, + decimals: 18, + erc20: true, + erc721: false, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b3595068778dd592e39a122f4f5a5cf09c90fe2.png', + identiconAddress: null, + image: 'images/contract/sushi.svg', + name: 'SushiSwap', + primaryLabel: 'SUSHI', + rawFiat: '', + rightPrimaryLabel: undefined, + rightSecondaryLabel: '', + secondaryLabel: 'SushiSwap', + symbol: 'SUSHI', + type: 'TOKEN', + }); + }); + + it('when chainId === activeChainId and sorted by balance', () => { + const mockStore = createBridgeMockStore(); + mockUseTokenTracker.mockReturnValue({ + tokensWithBalances: [ + { + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + balance: '0xa', + }, + ], + }); + const { result } = renderHookWithProvider( + () => + useTokensWithFiltering( + MOCK_TOKEN_LIST_BY_ADDRESS, + MOCK_TOP_ASSETS, + TokenBucketPriority.owned, + TEST_CHAIN_ID, + ), + mockStore, + ); + + expect(result.current).toHaveLength(1); + expect(typeof result.current).toStrictEqual('function'); + const tokenGenerator = result.current(() => true); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + balance: '0x0', + decimals: 18, + iconUrl: './images/eth_logo.svg', + identiconAddress: null, + image: './images/eth_logo.svg', + name: 'Ether', + primaryLabel: 'ETH', + rawFiat: '0', + rightPrimaryLabel: '0 ETH', + rightSecondaryLabel: '$0.00 USD', + secondaryLabel: 'Ether', + string: '0', + symbol: 'ETH', + type: 'NATIVE', + }); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + aggregators: [], + balance: '0xa', + decimals: 6, + erc20: true, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png', + identiconAddress: null, + image: 'images/contract/usdt.svg', + name: 'Tether USD', + primaryLabel: 'USDT', + rawFiat: '', + rightPrimaryLabel: undefined, + rightSecondaryLabel: '', + secondaryLabel: 'Tether USD', + symbol: 'USDT', + type: 'TOKEN', + }); + }); +}); diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts new file mode 100644 index 000000000000..a7ff3f2513ac --- /dev/null +++ b/ui/hooks/useTokensWithFiltering.ts @@ -0,0 +1,178 @@ +import { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { isEqual } from 'lodash'; +import { ChainId, hexToBN } from '@metamask/controller-utils'; +import { Hex } from '@metamask/utils'; +import { + getAllTokens, + getCurrentCurrency, + getSelectedInternalAccountWithBalance, + getShouldHideZeroBalanceTokens, + getTokenExchangeRates, +} from '../selectors'; +import { getConversionRate } from '../ducks/metamask/metamask'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, + TokenBucketPriority, +} from '../../shared/constants/swaps'; +import { getValueFromWeiHex } from '../../shared/modules/conversion.utils'; +import { EtherDenomination } from '../../shared/constants/common'; +import { + AssetWithDisplayData, + ERC20Asset, + NativeAsset, + TokenWithBalance, +} from '../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { AssetType } from '../../shared/constants/transaction'; +import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils'; +import { useTokenTracker } from './useTokenTracker'; +import { getRenderableTokenData } from './useTokensToSearch'; + +/* + * Returns a token list generator that filters and sorts tokens based on + * query match, balance/popularity, all other tokens + */ +export const useTokensWithFiltering = ( + tokenList: Record, + topTokens: { address: string }[], + sortOrder: TokenBucketPriority = TokenBucketPriority.owned, + chainId?: ChainId | Hex, +) => { + // Only includes non-native tokens + const allDetectedTokens = useSelector(getAllTokens); + const { address: selectedAddress, balance: balanceOnActiveChain } = + useSelector(getSelectedInternalAccountWithBalance); + + const allDetectedTokensForChainAndAddress = chainId + ? allDetectedTokens?.[chainId]?.[selectedAddress] ?? [] + : []; + + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const { + tokensWithBalances: erc20TokensWithBalances, + }: { tokensWithBalances: TokenWithBalance[] } = useTokenTracker({ + tokens: allDetectedTokensForChainAndAddress, + address: selectedAddress, + hideZeroBalanceTokens: Boolean(shouldHideZeroBalanceTokens), + }); + + const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); + const conversionRate = useSelector(getConversionRate); + const currentCurrency = useSelector(getCurrentCurrency); + + const sortedErc20TokensWithBalances = useMemo( + () => + erc20TokensWithBalances.toSorted( + (a, b) => Number(b.string) - Number(a.string), + ), + [erc20TokensWithBalances], + ); + + const filteredTokenListGenerator = useCallback( + (shouldAddToken: (symbol: string, address?: string) => boolean) => { + const buildTokenData = ( + token: SwapsTokenObject, + ): + | AssetWithDisplayData + | AssetWithDisplayData + | undefined => { + if (chainId && shouldAddToken(token.symbol, token.address)) { + return getRenderableTokenData( + { + ...token, + type: isSwapsDefaultTokenSymbol(token.symbol, chainId) + ? AssetType.native + : AssetType.token, + image: token.iconUrl, + }, + tokenConversionRates, + conversionRate, + currentCurrency, + chainId, + tokenList, + ); + } + return undefined; + }; + + return (function* (): Generator< + AssetWithDisplayData | AssetWithDisplayData + > { + const balance = hexToBN(balanceOnActiveChain); + const srcBalanceFields = + sortOrder === TokenBucketPriority.owned + ? { + balance: balanceOnActiveChain, + string: getValueFromWeiHex({ + value: balance, + numberOfDecimals: 4, + toDenomination: EtherDenomination.ETH, + }), + } + : {}; + const nativeToken = buildTokenData({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ], + ...srcBalanceFields, + }); + if (nativeToken) { + yield nativeToken; + } + + if (sortOrder === TokenBucketPriority.owned) { + for (const tokenWithBalance of sortedErc20TokensWithBalances) { + const cachedTokenData = + tokenWithBalance.address && + tokenList && + (tokenList[tokenWithBalance.address] ?? + tokenList[tokenWithBalance.address.toLowerCase()]); + if (cachedTokenData) { + const combinedTokenData = buildTokenData({ + ...tokenWithBalance, + ...(cachedTokenData ?? {}), + }); + if (combinedTokenData) { + yield combinedTokenData; + } + } + } + } + + for (const topToken of topTokens) { + const tokenListItem = + tokenList?.[topToken.address] ?? + tokenList?.[topToken.address.toLowerCase()]; + if (tokenListItem) { + const tokenWithTokenListData = buildTokenData(tokenListItem); + if (tokenWithTokenListData) { + yield tokenWithTokenListData; + } + } + } + + for (const token of Object.values(tokenList)) { + const tokenWithTokenListData = buildTokenData(token); + if (tokenWithTokenListData) { + yield tokenWithTokenListData; + } + } + })(); + }, + [ + balanceOnActiveChain, + sortedErc20TokensWithBalances, + topTokens, + tokenConversionRates, + conversionRate, + currentCurrency, + chainId, + tokenList, + ], + ); + + return filteredTokenListGenerator; +}; diff --git a/ui/pages/bridge/__snapshots__/index.test.tsx.snap b/ui/pages/bridge/__snapshots__/index.test.tsx.snap index 14514e59987d..cebca14e93bb 100644 --- a/ui/pages/bridge/__snapshots__/index.test.tsx.snap +++ b/ui/pages/bridge/__snapshots__/index.test.tsx.snap @@ -9,27 +9,61 @@ exports[`Bridge renders the component with initial props 1`] = ` class="bridge__container" >
- +
- Bridge +

+ Bridge +

+
+
+
+
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss index 99b8712cc63e..98a3a3ee5c34 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -1 +1,29 @@ @use "design-system"; + +@import 'prepare/index'; + +.bridge { + max-height: 100vh; + width: 360px; + position: relative; + + &__container { + width: 100%; + + .multichain-page-footer { + position: absolute; + width: 100%; + height: 80px; + bottom: 0; + padding: 16px; + display: flex; + + button { + flex: 1; + height: 100%; + font-size: 14px; + font-weight: 500; + } + } + } +} diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index e4b5c0b930d4..e81b20670011 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -1,9 +1,7 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Switch, useHistory } from 'react-router-dom'; -import classnames from 'classnames'; import { I18nContext } from '../../contexts/i18n'; - import { clearSwapsState } from '../../ducks/swaps/swaps'; import { DEFAULT_ROUTE, @@ -11,25 +9,24 @@ import { PREPARE_SWAP_ROUTE, CROSS_CHAIN_SWAP_ROUTE, } from '../../helpers/constants/routes'; - import { resetBackgroundSwapsState } from '../../store/actions'; - import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route'; import { - Box, - Icon, + ButtonIcon, + ButtonIconSize, IconName, - IconSize, } from '../../components/component-library'; -import { - JustifyContent, - IconColor, - Display, - BlockSize, -} from '../../helpers/constants/design-system'; -import { getIsBridgeEnabled } from '../../selectors'; +import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors'; import useBridging from '../../hooks/bridge/useBridging'; -import { PrepareBridgePage } from './prepare/prepare-bridge-page'; +import { + Content, + Footer, + Header, +} from '../../components/multichain/pages/page'; +import { getProviderConfig } from '../../ducks/metamask/metamask'; +import { resetInputFields, setFromChain } from '../../ducks/bridge/actions'; +import PrepareBridgePage from './prepare/prepare-bridge-page'; +import { BridgeCTAButton } from './prepare/bridge-cta-button'; const CrossChainSwap = () => { const t = useContext(I18nContext); @@ -40,6 +37,19 @@ const CrossChainSwap = () => { const dispatch = useDispatch(); const isBridgeEnabled = useSelector(getIsBridgeEnabled); + const providerConfig = useSelector(getProviderConfig); + const isBridgeChain = useSelector(getIsBridgeChain); + + useEffect(() => { + isBridgeChain && + isBridgeEnabled && + providerConfig && + dispatch(setFromChain(providerConfig.chainId)); + + return () => { + dispatch(resetInputFields()); + }; + }, [isBridgeChain, isBridgeEnabled, providerConfig]); const redirectToDefaultRoute = async () => { history.push({ @@ -53,37 +63,27 @@ const CrossChainSwap = () => { return (
-
- ) => { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - - - -
{t('bridge')}
-
-
+ } > + {t('bridge')} + + { }} /> -
+ +
+ +
); diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap new file mode 100644 index 000000000000..f225adec3b6d --- /dev/null +++ b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeCTAButton should render the component's initial state 1`] = ` +
+ +
+`; diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap new file mode 100644 index 000000000000..b406cafe0941 --- /dev/null +++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap @@ -0,0 +1,456 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PrepareBridgePage should render the component, with initial state 1`] = ` +
+
+
+
+
+ +
+
+ +
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+
+
+`; + +exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+
+
+`; diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx new file mode 100644 index 000000000000..5e42823c885b --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { BridgeCTAButton } from './bridge-cta-button'; + +describe('BridgeCTAButton', () => { + it("should render the component's initial state", () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], + }, + { fromTokenInputValue: 1 }, + ); + const { container, getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByText('Select token')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should render the component when tx is submittable', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Bridge')).toBeInTheDocument(); + expect(getByRole('button')).not.toBeDisabled(); + }); +}); diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx new file mode 100644 index 000000000000..fedcf4d4606a --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { Button } from '../../../components/component-library'; +import { + getFromAmount, + getFromChain, + getFromToken, + getToAmount, + getToChain, + getToToken, +} from '../../../ducks/bridge/selectors'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const BridgeCTAButton = () => { + const t = useI18nContext(); + const fromToken = useSelector(getFromToken); + const toToken = useSelector(getToToken); + + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + + const fromAmount = useSelector(getFromAmount); + const toAmount = useSelector(getToAmount); + + const isTxSubmittable = + fromToken && toToken && fromChain && toChain && fromAmount && toAmount; + + const label = useMemo(() => { + if (isTxSubmittable) { + return t('bridge'); + } + + return t('swapSelectToken'); + }, [isTxSubmittable]); + + return ( + + ); +}; diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx new file mode 100644 index 000000000000..811310590c71 --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { Hex } from '@metamask/utils'; +import { SwapsTokenObject } from '../../../../shared/constants/swaps'; +import { + Box, + Text, + TextField, + TextFieldType, +} from '../../../components/component-library'; +import { AssetPicker } from '../../../components/multichain/asset-picker-amount/asset-picker'; +import { TabName } from '../../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs'; +import CurrencyDisplay from '../../../components/ui/currency-display'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; +import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; +import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; +import Tooltip from '../../../components/ui/tooltip'; +import { SwapsEthToken } from '../../../selectors'; +import { + ERC20Asset, + NativeAsset, +} from '../../../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { zeroAddress } from '../../../__mocks__/ethereumjs-util'; +import { AssetType } from '../../../../shared/constants/transaction'; +import { + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, + CHAIN_ID_TOKEN_IMAGE_MAP, +} from '../../../../shared/constants/network'; +import useLatestBalance from '../../../hooks/bridge/useLatestBalance'; + +const generateAssetFromToken = ( + chainId: Hex, + tokenDetails: SwapsTokenObject | SwapsEthToken, +): ERC20Asset | NativeAsset => { + if ('iconUrl' in tokenDetails && tokenDetails.address !== zeroAddress()) { + return { + type: AssetType.token, + image: tokenDetails.iconUrl, + symbol: tokenDetails.symbol, + address: tokenDetails.address, + }; + } + + return { + type: AssetType.native, + image: + CHAIN_ID_TOKEN_IMAGE_MAP[ + chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP + ], + symbol: + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ + chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + ], + }; +}; + +export const BridgeInputGroup = ({ + className, + header, + token, + onAssetChange, + onAmountChange, + networkProps, + customTokenListGenerator, + amountFieldProps = {}, +}: { + className: string; + onAmountChange?: (value: string) => void; + token: SwapsTokenObject | SwapsEthToken | null; + amountFieldProps?: Pick< + React.ComponentProps, + 'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled' + >; +} & Pick< + React.ComponentProps, + 'networkProps' | 'header' | 'customTokenListGenerator' | 'onAssetChange' +>) => { + const t = useI18nContext(); + + const tokenFiatValue = useTokenFiatAmount( + token?.address || undefined, + amountFieldProps?.value?.toString() || '0x0', + token?.symbol, + { + showFiat: true, + }, + true, + ); + const ethFiatValue = useEthFiatAmount( + amountFieldProps?.value?.toString() || '0x0', + { showFiat: true }, + true, + ); + + const { formattedBalance } = useLatestBalance( + token, + networkProps?.network?.chainId, + ); + + return ( + + + + + { + onAmountChange?.(e.target.value); + }} + {...amountFieldProps} + /> + + + + + {formattedBalance ? `${t('balance')}: ${formattedBalance}` : ' '} + + + + + ); +}; diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss new file mode 100644 index 000000000000..1fa416df727f --- /dev/null +++ b/ui/pages/bridge/prepare/index.scss @@ -0,0 +1,170 @@ +@use "design-system"; + +.tokens-main-view-modal { + .multichain-asset-picker-list-item .mm-badge-wrapper__badge-container { + display: none; + } +} + +.prepare-bridge-page { + display: flex; + flex-flow: column; + flex: 1; + width: 100%; + + &__content { + display: flex; + flex-direction: column; + padding: 16px 0 16px 0; + border-radius: 8px; + border: 1px solid var(--color-border-muted); + } + + &__from, + &__to { + display: flex; + flex-direction: column; + gap: 4px; + justify-content: center; + padding: 8px 16px 8px 16px; + } + + &__input-row { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 298px; + gap: 16px; + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + .mm-text-field { + background-color: inherit; + + &--focused { + outline: none; + } + } + } + + &__amounts-row { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 298px; + height: 22px; + + p, + span { + color: var(--color-text-alternative); + font-size: 12px; + } + } + + .asset-picker { + border: 1px solid var(--color-border-muted); + height: 40px; + min-width: fit-content; + max-width: fit-content; + background-color: inherit; + + p { + font-size: 14px; + font-weight: 500; + } + + .mm-avatar-token { + height: 24px; + width: 24px; + border: 1px solid var(--color-border-muted); + } + + .mm-badge-wrapper__badge-container .mm-avatar-base { + height: 10px; + width: 10px; + border: none; + } + } + + .amount-input { + border: none; + + input { + text-align: right; + padding-right: 0; + font-size: 24px; + font-weight: 700; + + &:focus, + &:focus-visible { + outline: none; + } + } + + .mm-text-field--focused { + outline: none; + } + } + + &__switch-tokens { + display: flex; + justify-content: center; + align-items: center; + + &::before, + &::after { + content: ''; + border-top: 1px solid var(--color-border-muted); + flex-grow: 1; + } + + button { + border-radius: 50%; + padding: 10px; + border: 1px solid var(--color-border-muted); + transition: all 0.3s ease-in-out; + cursor: pointer; + width: 40px; + height: 40px; + + &:hover:enabled { + background: var(--color-background-default-hover); + + .mm-icon { + color: var(--color-icon-default); + } + } + + &:active { + background: var(--color-background-default-pressed); + + .mm-icon { + color: var(--color-icon-default); + } + } + + .rotate { + transform: rotate(360deg); + } + } + + .mm-icon { + color: var(--color-icon-alternative); + transition: all 0.3s ease-in-out; + } + + &:disabled { + cursor: not-allowed; + } + } +} diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx new file mode 100644 index 000000000000..82441aad218d --- /dev/null +++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { act } from '@testing-library/react'; +import { fireEvent, renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { createTestProviderTools } from '../../../../test/stub/provider'; +import PrepareBridgePage from './prepare-bridge-page'; + +describe('PrepareBridgePage', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'Ethereum', + chainId: CHAIN_IDS.MAINNET, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + it('should render the component, with initial state', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], + }, + {}, + ); + const { container, getByRole, getByTestId } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument(); + expect(getByRole('button', { name: /Select token/u })).toBeInTheDocument(); + + expect(getByTestId('from-amount')).toBeInTheDocument(); + expect(getByTestId('from-amount').closest('input')).not.toBeDisabled(); + await act(() => { + fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } }); + }); + expect(getByTestId('from-amount').closest('input')).toHaveValue(2); + + expect(getByTestId('to-amount')).toBeInTheDocument(); + expect(getByTestId('to-amount').closest('input')).toBeDisabled(); + + expect(getByTestId('switch-tokens').closest('button')).toBeDisabled(); + }); + + it('should render the component, with inputs set', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: { address: '0x3103910', decimals: 6 }, + toToken: { + iconUrl: 'http://url', + symbol: 'UNI', + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 6, + }, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { container, getByRole, getByTestId } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument(); + expect(getByRole('button', { name: /UNI/u })).toBeInTheDocument(); + + expect(getByTestId('from-amount')).toBeInTheDocument(); + expect(getByTestId('from-amount').closest('input')).not.toBeDisabled(); + expect(getByTestId('from-amount').closest('input')).toHaveValue(1); + await act(() => { + fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } }); + }); + expect(getByTestId('from-amount').closest('input')).toHaveValue(2); + + expect(getByTestId('to-amount')).toBeInTheDocument(); + expect(getByTestId('to-amount').closest('input')).toBeDisabled(); + + expect(getByTestId('switch-tokens').closest('button')).not.toBeDisabled(); + }); + + it('should throw an error if token decimals are not defined', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: { address: '0x3103910' }, + toToken: { + iconUrl: 'http://url', + symbol: 'UNI', + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + }, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + + expect(() => + renderWithProvider(, configureStore(mockStore)), + ).toThrow(); + }); +}); diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 6c66499ac9b9..2fdb11289c5b 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -1,24 +1,163 @@ -import React from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import { isEqual, shuffle } from 'lodash'; -import PrepareSwapPage from '../../swaps/prepare-swap-page/prepare-swap-page'; -import { getSelectedAccount, getTokenList } from '../../../selectors'; +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import classnames from 'classnames'; +import { + setFromChain, + setFromToken, + setFromTokenInputValue, + setToChain, + setToToken, + switchToAndFromTokens, +} from '../../../ducks/bridge/actions'; +import { + getFromAmount, + getFromChain, + getFromChains, + getFromToken, + getFromTokens, + getFromTopAssets, + getToAmount, + getToChain, + getToChains, + getToToken, + getToTokens, + getToTopAssets, +} from '../../../ducks/bridge/selectors'; +import { + Box, + ButtonIcon, + IconName, +} from '../../../components/component-library'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { TokenBucketPriority } from '../../../../shared/constants/swaps'; +import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering'; +import { setActiveNetwork } from '../../../store/actions'; +import { BlockSize } from '../../../helpers/constants/design-system'; +import { BridgeInputGroup } from './bridge-input-group'; -export const PrepareBridgePage = () => { - const selectedAccount = useSelector(getSelectedAccount, shallowEqual); - const { balance: ethBalance, address: selectedAccountAddress } = - selectedAccount; +const PrepareBridgePage = () => { + const dispatch = useDispatch(); - const tokenList = useSelector(getTokenList, isEqual); - const shuffledTokensList = shuffle(Object.values(tokenList)); + const t = useI18nContext(); + + const fromToken = useSelector(getFromToken); + const fromTokens = useSelector(getFromTokens); + const fromTopAssets = useSelector(getFromTopAssets); + + const toToken = useSelector(getToToken); + const toTokens = useSelector(getToTokens); + const toTopAssets = useSelector(getToTopAssets); + + const fromChains = useSelector(getFromChains); + const toChains = useSelector(getToChains); + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + + const fromAmount = useSelector(getFromAmount); + const toAmount = useSelector(getToAmount); + + const fromTokenListGenerator = useTokensWithFiltering( + fromTokens, + fromTopAssets, + TokenBucketPriority.owned, + fromChain?.chainId, + ); + const toTokenListGenerator = useTokensWithFiltering( + toTokens, + toTopAssets, + TokenBucketPriority.top, + toChain?.chainId, + ); + + const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); return ( -
- +
+ + { + dispatch(setFromTokenInputValue(e)); + }} + onAssetChange={(token) => dispatch(setFromToken(token))} + networkProps={{ + network: fromChain, + networks: fromChains, + onNetworkChange: (networkConfig) => { + dispatch( + setActiveNetwork( + networkConfig.rpcEndpoints[ + networkConfig.defaultRpcEndpointIndex + ].networkClientId, + ), + ); + dispatch(setFromChain(networkConfig.chainId)); + }, + }} + customTokenListGenerator={ + fromTokens && fromTopAssets ? fromTokenListGenerator : undefined + } + amountFieldProps={{ + testId: 'from-amount', + autoFocus: true, + value: fromAmount || undefined, + }} + /> + + + { + setRotateSwitchTokens(!rotateSwitchTokens); + const toChainClientId = + toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints + ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex] + .networkClientId + : undefined; + toChainClientId && dispatch(setActiveNetwork(toChainClientId)); + dispatch(switchToAndFromTokens({ fromChain })); + }} + /> + + + dispatch(setToToken(token))} + networkProps={{ + network: toChain, + networks: toChains, + onNetworkChange: (networkConfig) => { + dispatch(setToChain(networkConfig.chainId)); + }, + }} + customTokenListGenerator={ + toChain && toTokens && toTopAssets + ? toTokenListGenerator + : fromTokenListGenerator + } + amountFieldProps={{ + testId: 'to-amount', + readOnly: true, + disabled: true, + value: toAmount, + }} + /> +
); }; + +export default PrepareBridgePage; diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx index 6e0d2627d4aa..0b8585fd0b5c 100644 --- a/ui/pages/create-account/connect-hardware/index.test.tsx +++ b/ui/pages/create-account/connect-hardware/index.test.tsx @@ -30,6 +30,10 @@ jest.mock('../../../selectors', () => ({ }, })); +jest.mock('../../../ducks/bridge/selectors', () => ({ + getAllBridgeableNetworks: () => [], +})); + const MOCK_RECENT_PAGE = '/home'; jest.mock('../../../ducks/history/history', () => ({ getMostRecentOverviewPage: jest diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 9f86ad6f6e5a..378622a3994a 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -28,4 +28,5 @@ @import 'create-snap-account/index'; @import 'remove-snap-account/index'; @import 'swaps/index'; +@import 'bridge/index'; @import 'unlock-page/index'; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index a27d59e3b33b..d59c4b29e0c1 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -500,6 +500,17 @@ export default class Routes extends Component { hideAppHeader() { const { location } = this.props; + const isCrossChainSwapsPage = Boolean( + matchPath(location.pathname, { + path: `${CROSS_CHAIN_SWAP_ROUTE}`, + exact: false, + }), + ); + + if (isCrossChainSwapsPage) { + return true; + } + const isNotificationsPage = Boolean( matchPath(location.pathname, { path: `${NOTIFICATIONS_ROUTE}`, From 526d3ecd02528fb3978c6b113170c64d2dd2f632 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Wed, 16 Oct 2024 17:40:18 +0100 Subject: [PATCH 12/51] fix: updated edit modals (#27623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to update edit modals followed by design QA ## **Description** Following changes have been made in this PR: 1. The update CTA is now fixed/pinned to the bottom 2. The warning message while clicking on disconnect has been updated: - Simple the copy to This will disconnect you from this site - Increase Icon size from 12px to 16px - Center align warning message with the button NOTE: Add new accounts will be handled in a separate PR Including RPC URL is a new change proposed so I will update that in a different PR as well ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/3390](https://github.com/MetaMask/MetaMask-planning/issues/3390) ## **Manual testing steps** 1. Run extension with yarn start 2. Connect to dapp 3. Click on All Permissions and the go to individual permission page 4. Click on Edit Accounts Modal and observe the above UI changes ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/8d3c95e8-5f49-486d-9d70-69e50339fe43 ### **After** https://github.com/user-attachments/assets/9f48b909-8c3a-4435-9a38-99b71e05e5d2 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 +- .../edit-accounts-modal.test.tsx | 1 - .../edit-accounts-modal.tsx | 281 +++++++++--------- .../multichain/edit-accounts-modal/index.scss | 5 + .../edit-networks-modal.js | 96 +++--- .../multichain/edit-networks-modal/index.scss | 5 + .../multichain/multichain-components.scss | 2 + .../review-permissions-page.tsx | 1 - .../site-cell/site-cell.tsx | 4 - .../connect-page/connect-page.tsx | 2 - 10 files changed, 198 insertions(+), 202 deletions(-) create mode 100644 ui/components/multichain/edit-accounts-modal/index.scss create mode 100644 ui/components/multichain/edit-networks-modal/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6e907c35a088..bb10d6f579a0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1646,8 +1646,7 @@ "message": "Snaps" }, "disconnectMessage": { - "message": "This will disconnect you from $1", - "description": "$1 is the name of the dapp" + "message": "This will disconnect you from this site" }, "disconnectPrompt": { "message": "Disconnect $1" diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx index b00e3dc39c3e..65d29bfec036 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.test.tsx @@ -44,7 +44,6 @@ const render = ( , store, diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx index 084596f07afb..ddc2749e2ee7 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx @@ -27,8 +27,8 @@ import { IconColor, FlexDirection, AlignItems, + BlockSize, } from '../../../helpers/constants/design-system'; -import { getURLHost } from '../../../helpers/utils/util'; import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { MetaMetricsEventCategory, @@ -38,7 +38,6 @@ import { MetaMetricsContext } from '../../../contexts/metametrics'; import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; type EditAccountsModalProps = { - activeTabOrigin: string; accounts: MergedInternalAccount[]; defaultSelectedAccountAddresses: string[]; onClose: () => void; @@ -46,7 +45,6 @@ type EditAccountsModalProps = { }; export const EditAccountsModal: React.FC = ({ - activeTabOrigin, accounts, defaultSelectedAccountAddresses, onClose, @@ -56,7 +54,6 @@ export const EditAccountsModal: React.FC = ({ const trackEvent = useContext(MetaMetricsContext); const [showAddNewAccounts, setShowAddNewAccounts] = useState(false); - const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( defaultSelectedAccountAddresses, ); @@ -84,155 +81,151 @@ export const EditAccountsModal: React.FC = ({ } }; - const allAreSelected = () => { - return accounts.length === selectedAccountAddresses.length; - }; - + const allAreSelected = () => + accounts.length === selectedAccountAddresses.length; const checked = allAreSelected(); const isIndeterminate = !checked && selectedAccountAddresses.length > 0; - const hostName = getURLHost(activeTabOrigin); - const defaultSet = new Set(defaultSelectedAccountAddresses); const selectedSet = new Set(selectedAccountAddresses); return ( - <> - - - - {t('editAccounts')} - - {showAddNewAccounts ? ( - - setShowAddNewAccounts(false)} + + + + {t('editAccounts')} + + {showAddNewAccounts ? ( + + setShowAddNewAccounts(false)} + /> + + ) : ( + <> + + + allAreSelected() ? deselectAll() : selectAll() + } + isIndeterminate={isIndeterminate} /> + setShowAddNewAccounts(true)}> + {t('newAccount')} + - ) : ( - <> - - - allAreSelected() ? deselectAll() : selectAll() - } - isIndeterminate={isIndeterminate} - /> - setShowAddNewAccounts(true)}> - {t('newAccount')} - - - {accounts.map((account) => ( - handleAccountClick(account.address)} - account={account} - key={account.address} - isPinned={Boolean(account.pinned)} - startAccessory={ - - isEqualCaseInsensitive( - selectedAccountAddress, - account.address, - ), - )} - /> - } - selected={false} - /> - ))} - - - {selectedAccountAddresses.length === 0 ? ( - - - - - {t('disconnectMessage', [hostName])} - - - { - onSubmit([]); - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - danger - > - {t('disconnect')} - - - ) : ( - { - // Get accounts that are in `selectedAccountAddresses` but not in `defaultSelectedAccountAddresses` - const addedAccounts = selectedAccountAddresses.filter( - (address) => !defaultSet.has(address), - ); - // Get accounts that are in `defaultSelectedAccountAddresses` but not in `selectedAccountAddresses` - const removedAccounts = - defaultSelectedAccountAddresses.filter( - (address) => !selectedSet.has(address), - ); - - onSubmit(selectedAccountAddresses); - trackEvent({ - category: MetaMetricsEventCategory.Permissions, - event: - MetaMetricsEventName.UpdatePermissionedAccounts, - properties: { - addedAccounts: addedAccounts.length, - removedAccounts: removedAccounts.length, - location: 'Edit Accounts Modal', - }, - }); - - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - > - {t('update')} - - )} - - - )} - - - - + {accounts.map((account) => ( + handleAccountClick(account.address)} + account={account} + key={account.address} + isPinned={Boolean(account.pinned)} + startAccessory={ + + isEqualCaseInsensitive( + selectedAccountAddress, + account.address, + ), + )} + /> + } + selected={false} + /> + ))} + + )} + + + + {selectedAccountAddresses.length === 0 ? ( + + + + + {t('disconnectMessage')} + + + { + onSubmit([]); + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + danger + > + {t('disconnect')} + + + ) : ( + { + const addedAccounts = selectedAccountAddresses.filter( + (address) => !defaultSet.has(address), + ); + const removedAccounts = defaultSelectedAccountAddresses.filter( + (address) => !selectedSet.has(address), + ); + + onSubmit(selectedAccountAddresses); + trackEvent({ + category: MetaMetricsEventCategory.Permissions, + event: MetaMetricsEventName.UpdatePermissionedAccounts, + properties: { + addedAccounts: addedAccounts.length, + removedAccounts: removedAccounts.length, + location: 'Edit Accounts Modal', + }, + }); + + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + > + {t('update')} + + )} + + + ); }; diff --git a/ui/components/multichain/edit-accounts-modal/index.scss b/ui/components/multichain/edit-accounts-modal/index.scss new file mode 100644 index 000000000000..887b8afb8183 --- /dev/null +++ b/ui/components/multichain/edit-accounts-modal/index.scss @@ -0,0 +1,5 @@ +.edit-accounts-modal { + &__body { + overflow: auto; + } +} diff --git a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js index e4a7c391b4df..0b86716af50a 100644 --- a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js +++ b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js @@ -2,9 +2,11 @@ import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { AlignItems, + BlockSize, Display, FlexDirection, IconColor, + JustifyContent, TextColor, TextVariant, } from '../../../helpers/constants/design-system'; @@ -26,7 +28,6 @@ import { IconSize, } from '../../component-library'; import { NetworkListItem } from '..'; -import { getURLHost } from '../../../helpers/utils/util'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; import { MetaMetricsEventCategory, @@ -35,7 +36,6 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; export const EditNetworksModal = ({ - activeTabOrigin, nonTestNetworks, testNetworks, defaultSelectedChainIds, @@ -80,8 +80,6 @@ export const EditNetworksModal = ({ const checked = allAreSelected(); const isIndeterminate = !checked && selectedChainIds.length > 0; - const hostName = getURLHost(activeTabOrigin); - const defaultChainIdsSet = new Set(defaultSelectedChainIds); const selectedChainIdsSet = new Set(selectedChainIds); @@ -102,7 +100,11 @@ export const EditNetworksModal = ({ > {t('editNetworksTitle')} - + ))} - - {selectedChainIds.length === 0 ? ( + + + {selectedChainIds.length === 0 ? ( + - - - - {t('disconnectMessage', [hostName])} - - - { - onSubmit([]); - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - danger + + - {t('disconnect')} - + {t('disconnectMessage')} + - ) : ( { onSubmit(selectedChainIds); // Get networks that are in `selectedChainIds` but not in `defaultSelectedChainIds` @@ -211,23 +203,31 @@ export const EditNetworksModal = ({ }} size={ButtonPrimarySize.Lg} block + danger > - {t('update')} + {t('disconnect')} - )} - - + + ) : ( + { + onSubmit(selectedChainIds); + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + > + {t('update')} + + )} + ); }; EditNetworksModal.propTypes = { - /** - * Origin for the active tab. - */ - activeTabOrigin: PropTypes.string, - /** * Array of network objects representing available non-test networks to choose from. */ diff --git a/ui/components/multichain/edit-networks-modal/index.scss b/ui/components/multichain/edit-networks-modal/index.scss new file mode 100644 index 000000000000..113351b8cd2e --- /dev/null +++ b/ui/components/multichain/edit-networks-modal/index.scss @@ -0,0 +1,5 @@ +.edit-networks-modal { + &__body { + overflow: auto; + } +} diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index 2d2d6b3fdef0..bf3191c7e994 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -19,6 +19,8 @@ @import 'connected-site-menu'; @import 'create-named-snap-account'; @import 'dropdown-editor'; +@import "edit-accounts-modal"; +@import "edit-networks-modal"; @import 'token-list-item'; @import 'network-list-item'; @import 'network-list-item-menu'; diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx index b12fea776c65..66e7cadd7546 100644 --- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx @@ -195,7 +195,6 @@ export const ReviewPermissions = () => { onSelectChainIds={handleSelectChainIds} selectedAccountAddresses={connectedAccountAddresses} selectedChainIds={connectedChainIds} - activeTabOrigin={activeTabOrigin} /> ) : ( diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index 562d3e8c7d2e..ae7a93283ead 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -37,7 +37,6 @@ type SiteCellProps = { onSelectChainIds: (chainIds: Hex[]) => void; selectedAccountAddresses: string[]; selectedChainIds: string[]; - activeTabOrigin: string; isConnectFlow?: boolean; }; @@ -49,7 +48,6 @@ export const SiteCell: React.FC = ({ onSelectChainIds, selectedAccountAddresses, selectedChainIds, - activeTabOrigin, isConnectFlow, }) => { const t = useI18nContext(); @@ -148,7 +146,6 @@ export const SiteCell: React.FC = ({ {showEditAccountsModal && ( setShowEditAccountsModal(false)} @@ -158,7 +155,6 @@ export const SiteCell: React.FC = ({ {showEditNetworksModal && ( = ({ permissionsRequestId, rejectPermissionsRequest, approveConnection, - activeTabOrigin, }) => { const t = useI18nContext(); @@ -146,7 +145,6 @@ export const ConnectPage: React.FC = ({ onSelectChainIds={setSelectedChainIds} selectedAccountAddresses={selectedAccountAddresses} selectedChainIds={selectedChainIds} - activeTabOrigin={activeTabOrigin} isConnectFlow /> From 601b5fabea723404b6043a19b0176be681d5ca05 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 16 Oct 2024 11:45:57 -0500 Subject: [PATCH 13/51] fix: Onboarding: Code style nits (#27767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Just doing some code style nits for the onboarding flow [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27767?quickstart=1) ## **Related issues** Fixes: N/A ## **Manual testing steps** 1. Get to the end of onboarding 2. Try out the advance configuration 3. Ensure all screens toggle as they should. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../privacy-settings/privacy-settings.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index ee11f63caf2a..aed08f196957 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -1,6 +1,7 @@ import React, { useContext, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import classnames from 'classnames'; import { ButtonVariant } from '@metamask/snaps-sdk'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -78,6 +79,8 @@ import { } from '../../../../shared/constants/network'; import { Setting } from './setting'; +const ANIMATION_TIME = 500; + /** * Profile Syncing Setting props * @@ -232,14 +235,14 @@ export default function PrivacySettings() { setTimeout(() => { setHiddenClass(false); - }, 500); + }, ANIMATION_TIME); }; const handleBack = () => { setShowDetail(false); setTimeout(() => { setHiddenClass(true); - }, 500); + }, ANIMATION_TIME); }; const items = [ @@ -252,7 +255,10 @@ export default function PrivacySettings() { <>
- {selectedItem && selectedItem.title} + {selectedItem?.title} @@ -397,7 +403,7 @@ export default function PrivacySettings() { className="privacy-settings__settings" data-testid="privacy-settings-settings" > - {selectedItem && selectedItem.id === 1 ? ( + {selectedItem?.id === 1 ? ( <> ) : null} - {selectedItem && selectedItem.id === 2 ? ( + {selectedItem?.id === 2 ? ( <> ) : null} - {selectedItem && selectedItem.id === 3 ? ( + {selectedItem?.id === 3 ? ( <> Date: Wed, 16 Oct 2024 19:16:34 +0200 Subject: [PATCH 14/51] chore: update @metamask/bitcoin-wallet-snap to 0.7.0 (#27730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Bump the BTC Snap to version 0.7.0. - This new release uses a new node provider (QuickNode) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26701?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- privacy-snapshot.json | 1 + test/e2e/constants.ts | 3 ++ .../flask/btc/btc-account-overview.spec.ts | 21 ++++++++++ test/e2e/flask/btc/common-btc.ts | 38 ++++++++++--------- test/e2e/mock-e2e.js | 1 + yarn.lock | 10 ++--- 7 files changed, 52 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 9a77bb273995..fe2f43f63c9a 100644 --- a/package.json +++ b/package.json @@ -303,7 +303,7 @@ "@metamask/approval-controller": "^7.0.0", "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", - "@metamask/bitcoin-wallet-snap": "^0.6.1", + "@metamask/bitcoin-wallet-snap": "^0.7.0", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 37a05025382d..41b04a9b5210 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -1,4 +1,5 @@ [ + "*.btc*.quiknode.pro", "accounts.api.cx.metamask.io", "acl.execution.metamask.io", "api.blockchair.com", diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 7e92a28cf463..c3957cb6fbbf 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -46,3 +46,6 @@ export const DAPP_ONE_URL = 'http://127.0.0.1:8081'; /* Default BTC address created using test SRP */ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; + +/* Default (mocked) BTC balance used by the Bitcoin RPC provider */ +export const DEFAULT_BTC_BALANCE = 1; // BTC diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts index 5f0277c191de..24eedb60b6a2 100644 --- a/test/e2e/flask/btc/btc-account-overview.spec.ts +++ b/test/e2e/flask/btc/btc-account-overview.spec.ts @@ -1,4 +1,6 @@ +import { strict as assert } from 'assert'; import { Suite } from 'mocha'; +import { DEFAULT_BTC_BALANCE } from '../../constants'; import { withBtcAccountSnap } from './common-btc'; describe('BTC Account - Overview', function (this: Suite) { @@ -39,4 +41,23 @@ describe('BTC Account - Overview', function (this: Suite) { }, ); }); + + it('has balance', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + // Wait for the balance to load up + await driver.delay(2000); + + const balanceElement = await driver.findElement( + '.coin-overview__balance', + ); + const balanceText = await balanceElement.getText(); + + const [balance, unit] = balanceText.split('\n'); + assert(Number(balance) === DEFAULT_BTC_BALANCE); + assert(unit === 'BTC'); + }, + ); + }); }); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index a33ab1241a1c..15bf7d49eb0b 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -1,34 +1,36 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures, unlockWallet } from '../../helpers'; -import { DEFAULT_BTC_ACCOUNT } from '../../constants'; +import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { Driver } from '../../webdriver/driver'; import { createBtcAccount } from '../../accounts/common'; -const GENERATE_MOCK_BTC_BALANCE_CALL = ( - address: string = DEFAULT_BTC_ACCOUNT, -): { data: { [address: string]: number } } => { - return { - data: { - [address]: 9999, - }, - }; -}; - export async function mockBtcBalanceQuote( mockServer: Mockttp, address: string = DEFAULT_BTC_ACCOUNT, ) { return await mockServer - .forGet(/https:\/\/api\.blockchair\.com\/bitcoin\/addresses\/balances/u) - .withQuery({ - addresses: address, + .forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u) + .withJsonBodyIncluding({ + method: 'bb_getaddress', }) - .thenCallback(() => ({ - statusCode: 200, - json: GENERATE_MOCK_BTC_BALANCE_CALL(address), - })); + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: { + address, + balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats + totalReceived: '0', + totalSent: '0', + unconfirmedBalance: '0', + unconfirmedTxs: 0, + txs: 0, + }, + }, + }; + }); } export async function mockRampsDynamicFeatureFlag( diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 1d0783b82624..12d0fb293e15 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -76,6 +76,7 @@ const browserAPIRequestDomains = */ const privateHostMatchers = [ // { pattern: RegExp, host: string } + { pattern: /^.*\.btc.*\.quiknode.pro$/iu, host: '*.btc*.quiknode.pro' }, ]; /** diff --git a/yarn.lock b/yarn.lock index 94ecd006df3e..5b2708b2df0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4981,10 +4981,10 @@ __metadata: languageName: node linkType: hard -"@metamask/bitcoin-wallet-snap@npm:^0.6.1": - version: 0.6.1 - resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.1" - checksum: 10/9c595e328cd63efe62cdda4194efe44ab3da4a54a89007f485280924aa9e8ee37042bda0a07751f3ce01c2c3e4740b16cd130f07558aa84cd57b20a8d5f1d3a7 +"@metamask/bitcoin-wallet-snap@npm:^0.7.0": + version: 0.7.0 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.7.0" + checksum: 10/be4eceef1715c5e6d33d095d5b4aaa974656d945ff0ed0304fdc1244eb8940eb8978f304378367642aa8fd60d6b375eecc2a4653c38ba62ec306c03955c96682 languageName: node linkType: hard @@ -26129,7 +26129,7 @@ __metadata: "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" - "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" + "@metamask/bitcoin-wallet-snap": "npm:^0.7.0" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" From d1d469eef346e0901cd14835a013174b7b755d22 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 16 Oct 2024 21:11:49 +0200 Subject: [PATCH 15/51] chore: Bump Snaps packages (#27376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This bumps all Snaps packages to the latest versions. Summary of changes in the snaps deps: - Allow updating `context` in `snap_updateInterface` - Add `snap_getCurrencyRate` RPC method - Add `Avatar` component - Add `min`, `max` and `step` props to `Input` component - Add `size` prop to `Heading` component - Add support for `metamask:` schemed URLs in Snap UI links - Pass full URLs to PhishingController - Ignore Snap insight response if transaction or signature has already been signed - Allow `Link` in `Row` and `Address` in `Link` Closes https://github.com/MetaMask/snaps/issues/2776 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27376?quickstart=1) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Guillaume Roux Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Co-authored-by: Hassan Malik Co-authored-by: Frederik Bolding --- ...ask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch | 120 ------------------ .../controllers/permissions/specifications.js | 1 + app/scripts/metamask-controller.js | 14 ++ builds.yml | 8 +- package.json | 17 ++- test/e2e/snaps/enums.js | 2 +- .../safe-component-list.js | 2 + .../snaps/snap-ui-address/snap-ui-address.tsx | 37 ++---- .../app/snaps/snap-ui-avatar/index.ts | 1 + .../snaps/snap-ui-avatar/snap-ui-avatar.tsx | 44 +++++++ .../app/snaps/snap-ui-link/snap-ui-link.js | 23 +++- .../snap-ui-renderer/components/address.ts | 2 +- .../snap-ui-renderer/components/avatar.ts | 9 ++ .../snap-ui-renderer/components/field.ts | 5 +- .../snap-ui-renderer/components/heading.ts | 15 ++- .../snap-ui-renderer/components/index.ts | 2 + .../snap-ui-renderer/components/input.ts | 58 +++++++-- ui/hooks/snaps/useSnapNavigation.ts | 22 ++++ yarn.lock | 111 ++++++---------- 19 files changed, 246 insertions(+), 247 deletions(-) delete mode 100644 .yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch create mode 100644 ui/components/app/snaps/snap-ui-avatar/index.ts create mode 100644 ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx create mode 100644 ui/components/app/snaps/snap-ui-renderer/components/avatar.ts create mode 100644 ui/hooks/snaps/useSnapNavigation.ts diff --git a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch deleted file mode 100644 index 3361025d4860..000000000000 --- a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch +++ /dev/null @@ -1,120 +0,0 @@ -diff --git a/dist/ui.cjs b/dist/ui.cjs -index 300fe9e97bba85945e3c2d200e736987453f8268..d6fa322e2b3629f41d653b91db52c3db85064276 100644 ---- a/dist/ui.cjs -+++ b/dist/ui.cjs -@@ -200,13 +200,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - (0, utils_1.assert)(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- (0, utils_1.assert)(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ (0, utils_1.assert)(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ (0, utils_1.assert)(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ (0, utils_1.assert)(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.cjs.map b/dist/ui.cjs.map -index 71b5ecb9eb8bc8bdf919daccf24b25737ee69819..6d6e56cd7fea85e4d477c0399506a03d465ca740 100644 ---- a/dist/ui.cjs.map -+++ b/dist/ui.cjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";;;;AACA,mDAA+C;AAa/C,iDAiBiC;AACjC,2CAKyB;AACzB,mCAA2C;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,uBAAC,UAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,uBAAC,UAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,uBAAC,YAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,IAAA,cAAK,EAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,IAAA,mBAAU,EAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAhCD,0CAgCC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAA,cAAM,EACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,oBAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,uBAAC,YAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,oBAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,uBAAC,cAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,uBAAC,UAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,oBAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,uBAAC,WAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,oBAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,uBAAC,WAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,uBAAC,WAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,oBAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,uBAAC,SAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,GAAG;gBACf,OAAO,CACL,uBAAC,SAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,uBAAC,UAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,IAAA,wBAAgB,EAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAxFD,gEAwFC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,IAAA,cAAK,EAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,IAAA,mBAAU,EAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAA,cAAM,EACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;QAEzE,IAAA,cAAM,EAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAtBD,oCAsBC;AAED;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AATD,8CASC;AAED;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAXD,4CAWC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,oBAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,oBAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,oBAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AArBD,gDAqBC;AAED;;;;;GAKG;AACH,SAAgB,WAAW,CACzB,OAAgB;IAIhB,OAAO,IAAA,mBAAW,EAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAND,kCAMC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAdD,wCAcC;AAED;;;;;;;GAOG;AACH,SAAgB,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,IAAA,mBAAW,EAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,CAAC;QACzB,IAAA,mBAAW,EAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,IAAA,qBAAa,EAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAtCD,0BAsCC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,IAAA,mBAAW,EAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC;AAzBD,oCAyBC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";;;;AACA,mDAA+C;AAa/C,iDAiBiC;AACjC,2CAKyB;AACzB,mCAA2C;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,uBAAC,UAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,uBAAC,UAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,uBAAC,YAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,IAAA,cAAK,EAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,IAAA,mBAAU,EAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAhCD,0CAgCC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAA,cAAM,EACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,oBAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,uBAAC,YAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,oBAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,uBAAC,cAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,uBAAC,UAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,oBAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,uBAAC,WAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,oBAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,uBAAC,WAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,uBAAC,WAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,oBAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,uBAAC,SAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,GAAG;gBACf,OAAO,CACL,uBAAC,SAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,uBAAC,UAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,IAAA,wBAAgB,EAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAxFD,gEAwFC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,IAAA,cAAK,EAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,IAAA,mBAAU,EAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAA,cAAM,EACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAA,cAAM,EAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;gBAChC,MAAM,IAAI,GAAG,WAAW,QAAQ,EAAE,CAAC;gBACnC,IAAA,cAAM,EAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;YACvE,CAAC;YAED,OAAO;QACT,CAAC;QAED,IAAA,cAAM,EAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AA/BD,oCA+BC;AAED;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AATD,8CASC;AAED;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAXD,4CAWC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,oBAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,oBAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,oBAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AArBD,gDAqBC;AAED;;;;;GAKG;AACH,SAAgB,WAAW,CACzB,OAAgB;IAIhB,OAAO,IAAA,mBAAW,EAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAND,kCAMC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAdD,wCAcC;AAED;;;;;;;GAOG;AACH,SAAgB,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,IAAA,mBAAW,EAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,CAAC;QACzB,IAAA,mBAAW,EAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,IAAA,qBAAa,EAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAtCD,0BAsCC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,IAAA,mBAAW,EAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC;AAzBD,oCAyBC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -diff --git a/dist/ui.d.cts b/dist/ui.d.cts -index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644 ---- a/dist/ui.d.cts -+++ b/dist/ui.d.cts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map -index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644 ---- a/dist/ui.d.cts.map -+++ b/dist/ui.d.cts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.d.mts b/dist/ui.d.mts -index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644 ---- a/dist/ui.d.mts -+++ b/dist/ui.d.mts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map -index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644 ---- a/dist/ui.d.mts.map -+++ b/dist/ui.d.mts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.mjs b/dist/ui.mjs -index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644 ---- a/dist/ui.mjs -+++ b/dist/ui.mjs -@@ -195,13 +195,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ assert(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ assert(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map -index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644 ---- a/dist/ui.mjs.map -+++ b/dist/ui.mjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,4BAA4B;AAa/C,OAAO,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,GAAG,EACH,MAAM,EACN,OAAO,EACR,gCAAgC;AACjC,OAAO,EACL,MAAM,EACN,gBAAgB,EAChB,WAAW,EACX,aAAa,EACd,wBAAwB;AACzB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,KAAC,IAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,KAAC,MAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,UAAU,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,CACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,QAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,QAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,KAAC,QAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,QAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,KAAC,KAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,QAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,KAAC,KAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,QAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,KAAC,GAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,QAAQ,CAAC,GAAG;gBACf,OAAO,CACL,KAAC,GAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,KAAC,IAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,UAAU,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,CACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;QAEzE,MAAM,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,QAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,QAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,OAAgB;IAIhB,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,4BAA4B;AAa/C,OAAO,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,GAAG,EACH,MAAM,EACN,OAAO,EACR,gCAAgC;AACjC,OAAO,EACL,MAAM,EACN,gBAAgB,EAChB,WAAW,EACX,aAAa,EACd,wBAAwB;AACzB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,KAAC,IAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,KAAC,MAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,UAAU,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,CACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,QAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,QAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,KAAC,QAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,QAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,KAAC,KAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,QAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,KAAC,KAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,QAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,KAAC,GAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,QAAQ,CAAC,GAAG;gBACf,OAAO,CACL,KAAC,GAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,KAAC,IAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,UAAU,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,CACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;gBAChC,MAAM,IAAI,GAAG,WAAW,QAAQ,EAAE,CAAC;gBACnC,MAAM,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;YACvE,CAAC;YAED,OAAO;QACT,CAAC;QAED,MAAM,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,QAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,QAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,OAAgB;IAIhB,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 8a40082d4d80..fffc9ae44f49 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -413,6 +413,7 @@ export const unrestrictedMethods = Object.freeze([ 'snap_updateInterface', 'snap_getInterfaceState', 'snap_resolveInterface', + 'snap_getCurrencyRate', ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 'metamaskinstitutional_authenticate', 'metamaskinstitutional_reauthenticate', diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5b3693960113..0c692703f242 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1479,6 +1479,7 @@ export default class MetamaskController extends EventEmitter { `${this.phishingController.name}:testOrigin`, `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, + `${this.snapController.name}:get`, ], }); @@ -5975,6 +5976,19 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getAll', ), + getCurrencyRate: (currency) => { + const rate = this.multichainRatesController.state.rates[currency]; + const { fiatCurrency } = this.multichainRatesController.state; + + if (!rate) { + return undefined; + } + + return { + ...rate, + currency: fiatCurrency, + }; + }, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hasPermission: this.permissionController.hasPermission.bind( this.permissionController, diff --git a/builds.yml b/builds.yml index a69bf611a322..bcd035b56bc1 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -67,7 +67,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -90,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io diff --git a/package.json b/package.json index fe2f43f63c9a..652b8d4b3afb 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.5.1", + "@metamask/snaps-sdk": "^6.9.0", "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -264,8 +264,7 @@ "@metamask/network-controller@npm:^17.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^20.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", - "path-to-regexp": "1.9.0", - "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "path-to-regexp": "1.9.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -342,7 +341,7 @@ "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", - "@metamask/preinstalled-example-snap": "^0.1.0", + "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", @@ -353,11 +352,11 @@ "@metamask/selected-network-controller": "^18.0.1", "@metamask/signature-controller": "^19.1.0", "@metamask/smart-transactions-controller": "^13.0.0", - "@metamask/snaps-controllers": "^9.7.0", - "@metamask/snaps-execution-environments": "^6.7.2", - "@metamask/snaps-rpc-methods": "^11.1.1", - "@metamask/snaps-sdk": "^6.5.1", - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch", + "@metamask/snaps-controllers": "^9.11.1", + "@metamask/snaps-execution-environments": "^6.9.1", + "@metamask/snaps-rpc-methods": "^11.5.0", + "@metamask/snaps-sdk": "^6.9.0", + "@metamask/snaps-utils": "^8.4.1", "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 2b1a6bc6532d..7fdbf5e1fd24 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.1/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.2', }; diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index 14abe8eceea7..a41b92c9f463 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -43,6 +43,7 @@ import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; import { SnapUICard } from '../snaps/snap-ui-card'; import { SnapUIAddress } from '../snaps/snap-ui-address'; +import { SnapUIAvatar } from '../snaps/snap-ui-avatar'; import { SnapUISelector } from '../snaps/snap-ui-selector'; import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -106,6 +107,7 @@ export const safeComponentList = { SnapUICard, SnapUISelector, SnapUIAddress, + SnapUIAvatar, SnapUIFooterButton, FormTextField, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index 669f7dd30799..539548622135 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { CaipAccountId, isHexString, @@ -11,32 +10,35 @@ import { Display, TextColor, } from '../../../../helpers/constants/design-system'; -import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon'; -import Jazzicon from '../../../ui/jazzicon'; -import { getUseBlockie } from '../../../../selectors'; import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; +import { SnapUIAvatar } from '../snap-ui-avatar'; export type SnapUIAddressProps = { // The address must be a CAIP-10 string. address: string; - diameter?: number; + // This is not currently exposed to Snaps. + avatarSize?: 'xs' | 'sm' | 'md' | 'lg'; }; export const SnapUIAddress: React.FunctionComponent = ({ address, - diameter = 32, + avatarSize = 'md', }) => { - const parsed = useMemo(() => { + const caipIdentifier = useMemo(() => { if (isHexString(address)) { // For legacy address inputs we assume them to be Ethereum addresses. // NOTE: This means the chain ID is not gonna be reliable. - return parseCaipAccountId(`eip155:1:${address}`); + return `eip155:1:${address}`; } - return parseCaipAccountId(address as CaipAccountId); + return address; }, [address]); - const useBlockie = useSelector(getUseBlockie); + + const parsed = useMemo( + () => parseCaipAccountId(caipIdentifier as CaipAccountId), + [caipIdentifier], + ); // For EVM addresses, we make sure they are checksummed. const transformedAddress = @@ -47,20 +49,7 @@ export const SnapUIAddress: React.FunctionComponent = ({ return ( - {useBlockie ? ( - - ) : ( - - )} + {shortenedAddress} ); diff --git a/ui/components/app/snaps/snap-ui-avatar/index.ts b/ui/components/app/snaps/snap-ui-avatar/index.ts new file mode 100644 index 000000000000..44fc129d6b39 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-avatar/index.ts @@ -0,0 +1 @@ +export * from './snap-ui-avatar'; diff --git a/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx b/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx new file mode 100644 index 000000000000..7e6de5f3370b --- /dev/null +++ b/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx @@ -0,0 +1,44 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { CaipAccountId, parseCaipAccountId } from '@metamask/utils'; +import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon'; +import Jazzicon from '../../../ui/jazzicon'; +import { getUseBlockie } from '../../../../selectors'; + +export const DIAMETERS: Record = { + xs: 16, + sm: 24, + md: 32, + lg: 40, +}; + +export type SnapUIAvatarProps = { + // The address must be a CAIP-10 string. + address: string; + size?: 'xs' | 'sm' | 'md' | 'lg'; +}; + +export const SnapUIAvatar: React.FunctionComponent = ({ + address, + size = 'md', +}) => { + const parsed = useMemo(() => { + return parseCaipAccountId(address as CaipAccountId); + }, [address]); + const useBlockie = useSelector(getUseBlockie); + + return useBlockie ? ( + + ) : ( + + ); +}; diff --git a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js index 46439c523a68..a1289543fd45 100644 --- a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js +++ b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js @@ -9,18 +9,39 @@ import { IconSize, } from '../../../component-library'; import SnapLinkWarning from '../snap-link-warning'; +import useSnapNavigation from '../../../../hooks/snaps/useSnapNavigation'; export const SnapUILink = ({ href, children }) => { const [isOpen, setIsOpen] = useState(false); + const isMetaMaskUrl = href.startsWith('metamask:'); + const { navigate } = useSnapNavigation(); + const handleLinkClick = () => { - setIsOpen(true); + if (isMetaMaskUrl) { + navigate(href); + } else { + setIsOpen(true); + } }; const handleModalClose = () => { setIsOpen(false); }; + if (isMetaMaskUrl) { + return ( + + {children} + + ); + } + return ( <> diff --git a/ui/components/app/snaps/snap-ui-renderer/components/address.ts b/ui/components/app/snaps/snap-ui-renderer/components/address.ts index 108ff37f33a5..1e39966df760 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/address.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/address.ts @@ -5,6 +5,6 @@ export const address: UIComponentFactory = ({ element }) => ({ element: 'SnapUIAddress', props: { address: element.props.address, - diameter: 16, + avatarSize: 'xs', }, }); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts new file mode 100644 index 000000000000..9572516383b6 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts @@ -0,0 +1,9 @@ +import { AvatarElement } from '@metamask/snaps-sdk/jsx'; +import { UIComponentFactory } from './types'; + +export const avatar: UIComponentFactory = ({ element }) => ({ + element: 'SnapUIAvatar', + props: { + address: element.props.address, + }, +}); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/field.ts b/ui/components/app/snaps/snap-ui-renderer/components/field.ts index 0bafec17b2bf..169619b9a561 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/field.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/field.ts @@ -14,6 +14,7 @@ import { radioGroup as radioGroupFn } from './radioGroup'; import { checkbox as checkboxFn } from './checkbox'; import { selector as selectorFn } from './selector'; import { UIComponentFactory, UIComponentParams } from './types'; +import { constructInputProps } from './input'; export const field: UIComponentFactory = ({ element, @@ -79,9 +80,7 @@ export const field: UIComponentFactory = ({ id: input.props.name, placeholder: input.props.placeholder, label: element.props.label, - textFieldProps: { - type: input.props.type, - }, + ...constructInputProps(input.props), name: input.props.name, form, error: element.props.error !== undefined, diff --git a/ui/components/app/snaps/snap-ui-renderer/components/heading.ts b/ui/components/app/snaps/snap-ui-renderer/components/heading.ts index 709868fd4a6e..f0d6dee396d1 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/heading.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/heading.ts @@ -5,11 +5,24 @@ import { } from '../../../../../helpers/constants/design-system'; import { UIComponentFactory } from './types'; +export const generateSize = (size: HeadingElement['props']['size']) => { + switch (size) { + case 'sm': + return TextVariant.headingSm; + case 'md': + return TextVariant.headingMd; + case 'lg': + return TextVariant.headingLg; + default: + return TextVariant.headingSm; + } +}; + export const heading: UIComponentFactory = ({ element }) => ({ element: 'Text', children: element.props.children, props: { - variant: TextVariant.headingSm, + variant: generateSize(element.props.size), overflowWrap: OverflowWrap.Anywhere, }, }); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/index.ts b/ui/components/app/snaps/snap-ui-renderer/components/index.ts index 5d3b8fa16789..17a9b6aa37c1 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts @@ -26,6 +26,7 @@ import { container } from './container'; import { selector } from './selector'; import { icon } from './icon'; import { section } from './section'; +import { avatar } from './avatar'; export const COMPONENT_MAPPING = { Box: box, @@ -38,6 +39,7 @@ export const COMPONENT_MAPPING = { Copyable: copyable, Row: row, Address: address, + Avatar: avatar, Button: button, FileInput: fileInput, Form: form, diff --git a/ui/components/app/snaps/snap-ui-renderer/components/input.ts b/ui/components/app/snaps/snap-ui-renderer/components/input.ts index 9cc565d5d7f5..beda6c5ba4cc 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/input.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/input.ts @@ -1,16 +1,50 @@ -import { InputElement } from '@metamask/snaps-sdk/jsx'; +import { InputElement, NumberInputProps } from '@metamask/snaps-sdk/jsx'; +import { hasProperty } from '@metamask/utils'; import { UIComponentFactory } from './types'; -export const input: UIComponentFactory = ({ element, form }) => ({ - element: 'SnapUIInput', - props: { - id: element.props.name, - placeholder: element.props.placeholder, - textFieldProps: { - type: element.props.type, +export const constructInputProps = (props: InputElement['props']) => { + if (!hasProperty(props, 'type')) { + return { + textFieldProps: { + type: 'text', + }, + }; + } + + switch (props.type) { + case 'number': { + const { step, min, max, type } = props as NumberInputProps; + + return { + textFieldProps: { + type, + inputProps: { + step: step?.toString(), + min: min?.toString(), + max: max?.toString(), + }, + }, + }; + } + default: + return { + textFieldProps: { + type: props.type, + }, + }; + } +}; + +export const input: UIComponentFactory = ({ element, form }) => { + return { + element: 'SnapUIInput', + props: { + id: element.props.name, + placeholder: element.props.placeholder, + ...constructInputProps(element.props), + name: element.props.name, + form, }, - name: element.props.name, - form, - }, -}); + }; +}; diff --git a/ui/hooks/snaps/useSnapNavigation.ts b/ui/hooks/snaps/useSnapNavigation.ts new file mode 100644 index 000000000000..047b6fb13d36 --- /dev/null +++ b/ui/hooks/snaps/useSnapNavigation.ts @@ -0,0 +1,22 @@ +import { useHistory } from 'react-router-dom'; +import { parseMetaMaskUrl } from '@metamask/snaps-utils'; +import { getSnapRoute } from '../../helpers/utils/util'; + +const useSnapNavigation = () => { + const history = useHistory(); + const navigate = (url: string) => { + let path; + const linkData = parseMetaMaskUrl(url); + if (linkData.snapId) { + path = getSnapRoute(linkData.snapId); + } else { + path = linkData.path; + } + history.push(path); + }; + return { + navigate, + }; +}; + +export default useSnapNavigation; diff --git a/yarn.lock b/yarn.lock index 5b2708b2df0d..6d5582a99ee1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6069,12 +6069,12 @@ __metadata: languageName: node linkType: hard -"@metamask/preinstalled-example-snap@npm:^0.1.0": - version: 0.1.0 - resolution: "@metamask/preinstalled-example-snap@npm:0.1.0" +"@metamask/preinstalled-example-snap@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask/preinstalled-example-snap@npm:0.2.0" dependencies: - "@metamask/snaps-sdk": "npm:^6.5.0" - checksum: 10/0540aa6c20b17171f3a3bcf9ea2a7be551d6abbf16de9bd55dce038c5602c62a3921c7e840b82a325b0db00f26b96f54568854bdcd091558bd3b8fa8c6188023 + "@metamask/snaps-sdk": "npm:^6.9.0" + checksum: 10/f8ad6f42c9bd7ce3b7fc9b45eecda6191320ff762b48c482ba4944a6d7a228682b833c15e56058f26ac7bb10417dfe9de340af1c8eb9bbe5dc03c665426ccb13 languageName: node linkType: hard @@ -6272,9 +6272,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.7.0": - version: 9.7.0 - resolution: "@metamask/snaps-controllers@npm:9.7.0" +"@metamask/snaps-controllers@npm:^9.11.1, @metamask/snaps-controllers@npm:^9.7.0": + version: 9.11.1 + resolution: "@metamask/snaps-controllers@npm:9.11.1" dependencies: "@metamask/approval-controller": "npm:^7.0.2" "@metamask/base-controller": "npm:^6.0.2" @@ -6286,9 +6286,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-rpc-methods": "npm:^11.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/utils": "npm:^9.2.1" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6301,30 +6301,30 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.7.1 + "@metamask/snaps-execution-environments": ^6.9.1 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/8a353819e60330ef3e338a40b1115d4c830b92b1cc0c92afb2b34bf46fbc906e6da5f905654e1d486cacd40b7025ec74d3cd01cb935090035ce9f1021ce5469f + checksum: 10/e9d47b62c39cf331d26a9e35dcf5c0452aff70980db31b42b56b11165d8d1dc7e3b5ad6b495644baa0276b18a7d9681bfb059388c4f2fb1b07c6bbc8b8da799b languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.7.2": - version: 6.7.2 - resolution: "@metamask/snaps-execution-environments@npm:6.7.2" +"@metamask/snaps-execution-environments@npm:^6.9.1": + version: 6.9.1 + resolution: "@metamask/snaps-execution-environments@npm:6.9.1" dependencies: "@metamask/json-rpc-engine": "npm:^9.0.2" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.8.0" + "@metamask/snaps-utils": "npm:^8.4.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/4b8ec4c0f6e628feeffd92fe4378fd204d2ed78012a1ed5282b24b00c78cebc3b6d7cb1306903b045a2ca887ecc0adafb2c96da4a19f2730a268f4912b36bec3 + checksum: 10/87fb63e89780ebeb9083c93988167e671ceb3d1c77980a2cd32801f83d285669859bfd248197d3a2d683119b87554f1f835965549ad04587c8c2fa2f01fa1f18 languageName: node linkType: hard @@ -6340,63 +6340,32 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.1.1": - version: 11.1.1 - resolution: "@metamask/snaps-rpc-methods@npm:11.1.1" +"@metamask/snaps-rpc-methods@npm:^11.5.0": + version: 11.5.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.5.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/permission-controller": "npm:^11.0.0" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" - checksum: 10/e23279dabc6f4ffe2c6c4a7003a624cd5e79b558d7981ec12c23e54a5da25cb7be9bc7bddfa8b2ce84af28a89b42076a2c14ab004b7a976a4426bf1e1de71b5b + checksum: 10/a89b79926d5204a70369cd70e5174290805e8f9ede8057a49e347bd0e680d88de40ddfc25b3e54f53a16c3080a736ab73b50ffe50623264564af13f8709a23d3 languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.5.1": - version: 6.5.1 - resolution: "@metamask/snaps-sdk@npm:6.5.1" +"@metamask/snaps-sdk@npm:^6.9.0": + version: 6.9.0 + resolution: "@metamask/snaps-sdk@npm:6.9.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - checksum: 10/7831fb2ca61a32ad43e971de9307b221f6bd2f65c84a3286f350cfdd2396166c58db6cd2fac9711654a211c8dc2049e591a79ab720b3f5ad562e434f75e95d32 - languageName: node - linkType: hard - -"@metamask/snaps-utils@npm:8.1.1": - version: 8.1.1 - resolution: "@metamask/snaps-utils@npm:8.1.1" - dependencies: - "@babel/core": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/key-tree": "npm:^9.1.2" - "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/slip44": "npm:^4.0.0" - "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.1" - chalk: "npm:^4.1.2" - cron-parser: "npm:^4.5.0" - fast-deep-equal: "npm:^3.1.3" - fast-json-stable-stringify: "npm:^2.1.0" - fast-xml-parser: "npm:^4.4.1" - marked: "npm:^12.0.1" - rfdc: "npm:^1.3.0" - semver: "npm:^7.5.4" - ses: "npm:^1.1.0" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f + checksum: 10/ea2c34c4451f671acc6c3c0ad0d46e770e8b7d0741c1d78a30bc36b883f09a10e9a428b8b564ecd0171da95fdf78bb8ac0de261423a1b35de5d22852300a24ee languageName: node linkType: hard @@ -6431,9 +6400,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch": - version: 8.1.1 - resolution: "@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch::version=8.1.1&hash=d09097" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1": + version: 8.4.1 + resolution: "@metamask/snaps-utils@npm:8.4.1" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6443,7 +6412,7 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" @@ -6458,7 +6427,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/6b1d3d70c5ebee684d5b76bf911c66ebd122a0607cefcfc9fffd4bf6882a7acfca655d97be87c0f7f47e59a981b58234578ed8a123e554a36e6c48ff87492655 + checksum: 10/c68a2fe69dc835c2b996d621fd4698435475d419a85aa557aa000aae0ab7ebb68d2a52f0b28bbab94fff895ece9a94077e3910a21b16d904cff3b9419ca575b6 languageName: node linkType: hard @@ -26178,7 +26147,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" "@metamask/preferences-controller": "npm:^13.0.2" - "@metamask/preinstalled-example-snap": "npm:^0.1.0" + "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" @@ -26189,11 +26158,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^18.0.1" "@metamask/signature-controller": "npm:^19.1.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.7.0" - "@metamask/snaps-execution-environments": "npm:^6.7.2" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "@metamask/snaps-controllers": "npm:^9.11.1" + "@metamask/snaps-execution-environments": "npm:^6.9.1" + "@metamask/snaps-rpc-methods": "npm:^11.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" "@metamask/transaction-controller": "npm:^37.2.0" From aebd94a5a3e98623c8e4443f30160380b9654064 Mon Sep 17 00:00:00 2001 From: Ethan Wessel Date: Wed, 16 Oct 2024 15:07:36 -0700 Subject: [PATCH 16/51] fix: swapQuotesError as a property in the reported metric (#27712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27712?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/constants/metametrics.ts | 3 +- .../send/components/quote-card/index.tsx | 29 ++++++++++++++++++- ui/components/multichain/pages/send/send.js | 3 ++ ui/ducks/swaps/swaps.js | 4 +-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 544d24ce1271..8107a1040127 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -742,7 +742,8 @@ export enum MetaMetricsEventName { sendFlowExited = 'Send Flow Exited', sendRecipientSelected = 'Send Recipient Selected', sendSwapQuoteError = 'Send Swap Quote Error', - sendSwapQuoteFetched = 'Send Swap Quote Fetched', + sendSwapQuoteRequested = 'Send Swap Quote Requested', + sendSwapQuoteReceived = 'Send Swap Quote Received', sendTokenModalOpened = 'Send Token Modal Opened', } diff --git a/ui/components/multichain/pages/send/components/quote-card/index.tsx b/ui/components/multichain/pages/send/components/quote-card/index.tsx index 99b84e42a29b..9ac05dd6617f 100644 --- a/ui/components/multichain/pages/send/components/quote-card/index.tsx +++ b/ui/components/multichain/pages/send/components/quote-card/index.tsx @@ -87,7 +87,7 @@ export function QuoteCard({ scrollRef }: QuoteCardProps) { if (bestQuote) { trackEvent( { - event: MetaMetricsEventName.sendSwapQuoteFetched, + event: MetaMetricsEventName.sendSwapQuoteReceived, category: MetaMetricsEventCategory.Send, properties: { is_first_fetch: isQuoteJustLoaded, @@ -118,6 +118,33 @@ export function QuoteCard({ scrollRef }: QuoteCardProps) { return () => clearTimeout(timeout); }, [timeLeft]); + // use to track when a quote is requested and received + useEffect(() => { + if (isSwapQuoteLoading) { + trackEvent( + { + event: MetaMetricsEventName.sendSwapQuoteRequested, + category: MetaMetricsEventCategory.Send, + sensitiveProperties: { + ...sendAnalytics, + }, + }, + { excludeMetaMetricsId: false }, + ); + } else if (bestQuote) { + trackEvent( + { + event: MetaMetricsEventName.sendSwapQuoteReceived, + category: MetaMetricsEventCategory.Send, + sensitiveProperties: { + ...sendAnalytics, + }, + }, + { excludeMetaMetricsId: false }, + ); + } + }, [isSwapQuoteLoading]); + const infoText = useMemo(() => { if (isSwapQuoteLoading) { return t('swapFetchingQuotes'); diff --git a/ui/components/multichain/pages/send/send.js b/ui/components/multichain/pages/send/send.js index 3fe8ef3652eb..b77a5e7f5810 100644 --- a/ui/components/multichain/pages/send/send.js +++ b/ui/components/multichain/pages/send/send.js @@ -260,6 +260,9 @@ export const SendPage = () => { { event: MetaMetricsEventName.sendSwapQuoteError, category: MetaMetricsEventCategory.Send, + properties: { + error: swapQuotesError, + }, sensitiveProperties: { ...sendAnalytics, }, diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index ece4292fdf14..91ed081eb719 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -751,7 +751,7 @@ export const fetchQuotesAndSetQuoteState = ( const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(state); trackEvent({ - event: 'Quotes Requested', + event: MetaMetricsEventName.QuotesRequested, category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { token_from: fromTokenSymbol, @@ -839,7 +839,7 @@ export const fetchQuotesAndSetQuoteState = ( const tokenToAmountToString = tokenToAmountBN.toString(10); trackEvent({ - event: 'Quotes Received', + event: MetaMetricsEventName.QuotesReceived, category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { token_from: fromTokenSymbol, From 70e2c0874986b3d050db4adb9c035f9eb69428f5 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:21:12 +0000 Subject: [PATCH 17/51] fix(deps): update from eth-rpc-errors to @metamask/rpc-errors (cause edition) (#24496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Upgrade from obsolete `eth-rpc-errors` to `@metamask/rpc-errors` - This introduce handling of error causes See [here](https://github.com/MetaMask/rpc-errors/pull/140) for some context. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24496?quickstart=1) ## **Related issues** - #22871 #### Blocked by - [x] https://github.com/MetaMask/rpc-errors/pull/158 - [x] https://github.com/MetaMask/rpc-errors/pull/144 - [x] https://github.com/MetaMask/rpc-errors/pull/140 #### Blocking - #22875 ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/background.js | 4 +- app/scripts/lib/createMetaRPCHandler.js | 4 +- app/scripts/lib/createMetaRPCHandler.test.js | 1 + .../lib/createRPCMethodTrackingMiddleware.js | 2 +- .../createRPCMethodTrackingMiddleware.test.js | 2 +- app/scripts/lib/metaRPCClientFactory.js | 4 +- .../createMethodMiddleware.js | 4 +- .../createUnsupportedMethodMiddleware.ts | 4 +- .../handlers/add-ethereum-chain.js | 6 +- .../handlers/add-ethereum-chain.test.js | 8 +- .../handlers/ethereum-chain-utils.js | 28 +- .../mmi-set-account-and-network.js | 4 +- .../handlers/request-accounts.js | 6 +- .../handlers/send-metadata.js | 4 +- .../handlers/switch-ethereum-chain.js | 4 +- .../handlers/watch-asset.js | 4 +- .../handlers/watch-asset.test.js | 4 +- app/scripts/metamask-controller.js | 10 +- docs/confirmations.md | 4 +- lavamoat/browserify/beta/policy.json | 289 +++++++++++++++--- lavamoat/browserify/flask/policy.json | 289 +++++++++++++++--- lavamoat/browserify/main/policy.json | 289 +++++++++++++++--- lavamoat/browserify/mmi/policy.json | 289 +++++++++++++++--- lavamoat/build-system/policy.json | 2 +- package.json | 3 +- shared/modules/error.test.ts | 2 +- shared/modules/error.ts | 39 ++- .../dapp-interactions/provider-api.spec.js | 2 +- .../qr-hardware-popover.js | 4 +- .../import-account/import-account.js | 6 +- .../import-nfts-modal/import-nfts-modal.js | 3 +- ui/ducks/send/helpers.js | 8 +- ui/ducks/send/send.js | 10 +- .../confirm-add-suggested-nft.js | 6 +- .../confirm-add-suggested-token.js | 4 +- .../components/confirm/footer/footer.tsx | 4 +- .../components/confirm/nav/nav.tsx | 4 +- .../signature-request-original.component.js | 6 +- .../signature-request-original.container.js | 3 +- .../signature-request-siwe.js | 4 +- .../signature-request/signature-request.js | 4 +- .../templates/add-ethereum-chain.js | 4 +- .../templates/switch-ethereum-chain.js | 4 +- ui/pages/error/error.component.js | 6 +- ui/pages/keychains/reveal-seed.js | 3 +- .../permissions-connect.component.js | 6 +- ui/store/actions.ts | 52 ++-- .../institutional/institution-background.ts | 17 +- yarn.lock | 19 +- 49 files changed, 1198 insertions(+), 290 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index b6fe63b9aff1..9f203b35661d 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -18,7 +18,7 @@ import { isObject } from '@metamask/utils'; import { ApprovalType } from '@metamask/controller-utils'; import PortStream from 'extension-port-stream'; -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import { NotificationServicesController } from '@metamask/notification-services-controller'; @@ -1159,7 +1159,7 @@ export function setupController( default: controller.approvalController.reject( id, - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ); break; } diff --git a/app/scripts/lib/createMetaRPCHandler.js b/app/scripts/lib/createMetaRPCHandler.js index 77f86d23fe02..9d72620b4013 100644 --- a/app/scripts/lib/createMetaRPCHandler.js +++ b/app/scripts/lib/createMetaRPCHandler.js @@ -1,4 +1,4 @@ -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { rpcErrors, serializeError } from '@metamask/rpc-errors'; import { isStreamWritable } from './stream-utils'; const createMetaRPCHandler = (api, outStream) => { @@ -9,7 +9,7 @@ const createMetaRPCHandler = (api, outStream) => { if (!api[data.method]) { outStream.write({ jsonrpc: '2.0', - error: ethErrors.rpc.methodNotFound({ + error: rpcErrors.methodNotFound({ message: `${data.method} not found`, }), id: data.id, diff --git a/app/scripts/lib/createMetaRPCHandler.test.js b/app/scripts/lib/createMetaRPCHandler.test.js index 842af632e830..873366d53443 100644 --- a/app/scripts/lib/createMetaRPCHandler.test.js +++ b/app/scripts/lib/createMetaRPCHandler.test.js @@ -71,6 +71,7 @@ describe('createMetaRPCHandler', () => { }); streamTest.on('data', (data) => { expect(data.error.message).toStrictEqual('foo-error'); + expect(data.error.data.cause.message).toStrictEqual('foo-error'); streamTest.end(); }); }); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index b50905b8cb65..a5f12687f89e 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,5 +1,5 @@ import { ApprovalType, detectSIWE } from '@metamask/controller-utils'; -import { errorCodes } from 'eth-rpc-errors'; +import { errorCodes } from '@metamask/rpc-errors'; import { isValidAddress } from 'ethereumjs-util'; import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; import { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index b96c708be2d3..01daaf2974a4 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -1,4 +1,4 @@ -import { errorCodes } from 'eth-rpc-errors'; +import { errorCodes } from '@metamask/rpc-errors'; import { detectSIWE } from '@metamask/controller-utils'; import MetaMetricsController from '../controllers/metametrics'; diff --git a/app/scripts/lib/metaRPCClientFactory.js b/app/scripts/lib/metaRPCClientFactory.js index 3aae9962dbdb..2451189836e9 100644 --- a/app/scripts/lib/metaRPCClientFactory.js +++ b/app/scripts/lib/metaRPCClientFactory.js @@ -1,4 +1,4 @@ -import { EthereumRpcError } from 'eth-rpc-errors'; +import { JsonRpcError } from '@metamask/rpc-errors'; import SafeEventEmitter from '@metamask/safe-event-emitter'; import createRandomId from '../../../shared/modules/random-id'; import { TEN_SECONDS_IN_MILLISECONDS } from '../../../shared/lib/transactions-controller-utils'; @@ -77,7 +77,7 @@ class MetaRPCClient { } if (error) { - const e = new EthereumRpcError(error.code, error.message, error.data); + const e = new JsonRpcError(error.code, error.message, error.data); // preserve the stack from serializeError e.stack = error.stack; if (cb) { diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index e4b436163fc6..cee4e7763255 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -1,7 +1,7 @@ import { permissionRpcMethods } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; import { selectHooks } from '@metamask/snaps-rpc-methods'; import { hasProperty } from '@metamask/utils'; -import { ethErrors } from 'eth-rpc-errors'; import { handlers as localHandlers, legacyHandlers } from './handlers'; const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers]; @@ -67,7 +67,7 @@ function makeMethodMiddlewareMaker(handlers) { return end( error instanceof Error ? error - : ethErrors.rpc.internal({ data: error }), + : rpcErrors.internal({ data: error }), ); } } diff --git a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts index 193cc54b5a38..12abc82d4b21 100644 --- a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts +++ b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { JsonRpcMiddleware } from 'json-rpc-engine'; import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; @@ -12,7 +12,7 @@ export function createUnsupportedMethodMiddleware(): JsonRpcMiddleware< > { return async function unsupportedMethodMiddleware(req, _res, next, end) { if ((UNSUPPORTED_RPC_METHODS as Set).has(req.method)) { - return end(ethErrors.rpc.methodNotSupported()); + return end(rpcErrors.methodNotSupported()); } return next(); }; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index 2f4727fdab36..afcc2e167043 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -1,7 +1,7 @@ -import { ApprovalType } from '@metamask/controller-utils'; import * as URI from 'uri-js'; +import { ApprovalType } from '@metamask/controller-utils'; import { RpcEndpointType } from '@metamask/network-controller'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { cloneDeep } from 'lodash'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { @@ -73,7 +73,7 @@ async function addEthereumChainHandler( existingNetwork.nativeCurrency !== ticker ) { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\n${ticker}`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js index 945953cff562..ee0c9d3f732b 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; import addEthereumChain from './add-ethereum-chain'; @@ -350,7 +350,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\ninvalid_chain_id`, }), ); @@ -573,7 +573,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${unexpectedParam}`, }), ); @@ -657,7 +657,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\nWRONG`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js index 080fef549564..10973e052715 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js @@ -1,4 +1,4 @@ -import { errorCodes, ethErrors } from 'eth-rpc-errors'; +import { errorCodes, rpcErrors } from '@metamask/rpc-errors'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -11,13 +11,13 @@ import { getValidUrl } from '../../util'; export function validateChainId(chainId) { const _chainId = typeof chainId === 'string' && chainId.toLowerCase(); if (!isPrefixedFormattedHexString(_chainId)) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`, }); } if (!isSafeChainId(parseInt(_chainId, 16))) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`, }); } @@ -27,7 +27,7 @@ export function validateChainId(chainId) { export function validateSwitchEthereumChainParams(req, end) { if (!req.params?.[0] || typeof req.params[0] !== 'object') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( req.params, )}`, @@ -36,7 +36,7 @@ export function validateSwitchEthereumChainParams(req, end) { const { chainId, ...otherParams } = req.params[0]; if (Object.keys(otherParams).length > 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${Object.keys( otherParams, )}`, @@ -48,7 +48,7 @@ export function validateSwitchEthereumChainParams(req, end) { export function validateAddEthereumChainParams(params, end) { if (!params || typeof params !== 'object') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( params, )}`, @@ -70,14 +70,14 @@ export function validateAddEthereumChainParams(params, end) { ); if (otherKeys.length > 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${otherKeys}`, }); } const _chainId = validateChainId(chainId, end); if (!rpcUrls || !Array.isArray(rpcUrls) || rpcUrls.length === 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, }); } @@ -100,13 +100,13 @@ export function validateAddEthereumChainParams(params, end) { : null; if (!firstValidRPCUrl) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, }); } if (typeof chainName !== 'string' || !chainName) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected non-empty string 'chainName'. Received:\n${chainName}`, }); } @@ -116,18 +116,18 @@ export function validateAddEthereumChainParams(params, end) { if (nativeCurrency !== null) { if (typeof nativeCurrency !== 'object' || Array.isArray(nativeCurrency)) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected null or object 'nativeCurrency'. Received:\n${nativeCurrency}`, }); } if (nativeCurrency.decimals !== 18) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected the number 18 for 'nativeCurrency.decimals' when 'nativeCurrency' is provided. Received: ${nativeCurrency.decimals}`, }); } if (!nativeCurrency.symbol || typeof nativeCurrency.symbol !== 'string') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected a string 'nativeCurrency.symbol'. Received: ${nativeCurrency.symbol}`, }); } @@ -138,7 +138,7 @@ export function validateAddEthereumChainParams(params, end) { ticker !== UNKNOWN_TICKER_SYMBOL && (typeof ticker !== 'string' || ticker.length < 1 || ticker.length > 6) ) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected 1-6 character string 'nativeCurrency.symbol'. Received:\n${ticker}`, }); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js index 9dca692601a3..6c3dc41da9d2 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js @@ -1,5 +1,5 @@ -import { ethErrors } from 'eth-rpc-errors'; import { isAllowedRPCOrigin } from '@metamask-institutional/rpc-allowlist'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../../shared/constants/app'; const mmiSetAccountAndNetwork = { @@ -46,7 +46,7 @@ async function mmiSetAccountAndNetworkHandler( if (!req.params?.[0] || typeof req.params[0] !== 'object') { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( req.params, )}`, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js index f90fb5bd0d42..04977fe465d9 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { MetaMetricsEventName, @@ -71,7 +71,7 @@ async function requestEthereumAccountsHandler( }, ) { if (locks.has(origin)) { - res.error = ethErrors.rpc.resourceUnavailable( + res.error = rpcErrors.resourceUnavailable( `Already processing ${MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS}. Please wait.`, ); return end(); @@ -132,7 +132,7 @@ async function requestEthereumAccountsHandler( } else { // This should never happen, because it should be caught in the // above catch clause - res.error = ethErrors.rpc.internal( + res.error = rpcErrors.internal( 'Accounts unexpectedly unavailable. Please report this bug.', ); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js index a32fa497f248..35ec117a1f63 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; /** @@ -50,7 +50,7 @@ function sendMetadataHandler( origin, }); } else { - return end(ethErrors.rpc.invalidParams({ data: params })); + return end(rpcErrors.invalidParams({ data: params })); } res.result = true; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js index f43973e4ba57..5f907bef4d4b 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { validateSwitchEthereumChainParams, @@ -57,7 +57,7 @@ async function switchEthereumChainHandler( if (!networkClientIdToSwitchTo) { return end( - ethErrors.provider.custom({ + providerErrors.custom({ code: 4902, message: `Unrecognized chain ID "${chainId}". Try adding the chain using ${MESSAGE_TYPE.ADD_ETHEREUM_CHAIN} first.`, }), diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index 6bce7d7dca18..fdfacb373c77 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -1,5 +1,5 @@ import { ERC1155, ERC721 } from '@metamask/controller-utils'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; const watchAsset = { @@ -51,7 +51,7 @@ async function watchAssetHandler( typeof tokenId !== 'string' ) { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected parameter 'tokenId' to be type 'string'. Received type '${typeof tokenId}'`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js index eebe8a470ead..73efdd2a2798 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js @@ -1,5 +1,5 @@ import { ERC20, ERC721 } from '@metamask/controller-utils'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import watchAssetHandler from './watch-asset'; describe('watchAssetHandler', () => { @@ -95,7 +95,7 @@ describe('watchAssetHandler', () => { }); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected parameter 'tokenId' to be type 'string'. Received type 'number'`, }), ); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0c692703f242..87f7570f2d19 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -27,9 +27,9 @@ import createFilterMiddleware from '@metamask/eth-json-rpc-filters'; import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; import { errorCodes as rpcErrorCodes, - EthereumRpcError, - ethErrors, -} from 'eth-rpc-errors'; + JsonRpcError, + providerErrors, +} from '@metamask/rpc-errors'; import { Mutex } from 'await-semaphore'; import log from 'loglevel'; @@ -471,7 +471,7 @@ export default class MetamaskController extends EventEmitter { this.encryptionPublicKeyController.clearUnapproved(); this.decryptMessageController.clearUnapproved(); this.signatureController.clearUnapproved(); - this.approvalController.clear(ethErrors.provider.userRejectedRequest()); + this.approvalController.clear(providerErrors.userRejectedRequest()); }; this.queuedRequestController = new QueuedRequestController({ @@ -6726,7 +6726,7 @@ export default class MetamaskController extends EventEmitter { try { this.approvalController.reject( id, - new EthereumRpcError(error.code, error.message, error.data), + new JsonRpcError(error.code, error.message, error.data), ); } catch (exp) { if (!(exp instanceof ApprovalRequestNotFoundError)) { diff --git a/docs/confirmations.md b/docs/confirmations.md index 7af838d2053e..86577fc8f691 100644 --- a/docs/confirmations.md +++ b/docs/confirmations.md @@ -168,7 +168,7 @@ function getValues(pendingApproval, t, actions, _history) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: true, }; @@ -401,4 +401,4 @@ When an approval flow is created, this is reflected in the state and the UI will ### Custom Success Approval -[](assets/confirmation.png) \ No newline at end of file +[](assets/confirmation.png) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index d7522783f9fc..880b542673ea 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -699,13 +705,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -727,6 +733,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -851,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -886,14 +919,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1505,9 +1544,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1551,8 +1590,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1564,11 +1603,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1595,7 +1655,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1618,12 +1678,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1832,9 +1904,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1848,6 +1920,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1938,9 +2031,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -1971,6 +2064,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2096,8 +2210,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2110,6 +2224,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2131,8 +2266,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2143,6 +2278,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2160,8 +2316,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2310,11 +2466,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2364,6 +2520,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2449,11 +2611,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2486,8 +2648,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2508,15 +2676,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2574,8 +2748,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2590,10 +2764,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2612,12 +2786,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2631,6 +2811,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2647,10 +2833,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2680,10 +2866,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2694,6 +2880,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2766,8 +2958,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2794,6 +2986,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2803,9 +3001,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2822,6 +3020,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4067,11 +4286,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4525,8 +4739,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4537,6 +4751,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index d7522783f9fc..880b542673ea 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -699,13 +705,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -727,6 +733,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -851,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -886,14 +919,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1505,9 +1544,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1551,8 +1590,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1564,11 +1603,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1595,7 +1655,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1618,12 +1678,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1832,9 +1904,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1848,6 +1920,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1938,9 +2031,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -1971,6 +2064,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2096,8 +2210,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2110,6 +2224,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2131,8 +2266,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2143,6 +2278,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2160,8 +2316,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2310,11 +2466,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2364,6 +2520,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2449,11 +2611,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2486,8 +2648,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2508,15 +2676,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2574,8 +2748,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2590,10 +2764,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2612,12 +2786,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2631,6 +2811,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2647,10 +2833,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2680,10 +2866,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2694,6 +2880,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2766,8 +2958,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2794,6 +2986,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2803,9 +3001,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2822,6 +3020,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4067,11 +4286,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4525,8 +4739,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4537,6 +4751,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index d7522783f9fc..880b542673ea 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -699,13 +705,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -727,6 +733,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -851,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -886,14 +919,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1505,9 +1544,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1551,8 +1590,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1564,11 +1603,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1595,7 +1655,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1618,12 +1678,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1832,9 +1904,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1848,6 +1920,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1938,9 +2031,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -1971,6 +2064,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2096,8 +2210,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2110,6 +2224,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2131,8 +2266,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2143,6 +2278,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2160,8 +2316,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2310,11 +2466,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2364,6 +2520,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2449,11 +2611,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2486,8 +2648,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2508,15 +2676,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2574,8 +2748,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2590,10 +2764,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2612,12 +2786,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2631,6 +2811,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2647,10 +2833,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2680,10 +2866,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2694,6 +2880,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2766,8 +2958,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2794,6 +2986,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2803,9 +3001,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2822,6 +3020,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4067,11 +4286,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4525,8 +4739,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4537,6 +4751,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 3df824f29c78..25756f84ccc4 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -754,8 +754,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -766,6 +766,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -791,13 +797,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -819,6 +825,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -943,11 +955,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -978,14 +1011,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1597,9 +1636,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1643,8 +1682,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1656,11 +1695,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1687,7 +1747,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1710,12 +1770,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1924,9 +1996,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1940,6 +2012,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2030,9 +2123,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -2063,6 +2156,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2188,8 +2302,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2202,6 +2316,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2223,8 +2358,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2235,6 +2370,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2252,8 +2408,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2402,11 +2558,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2456,6 +2612,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2541,11 +2703,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2578,8 +2740,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2600,15 +2768,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2666,8 +2840,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2682,10 +2856,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2704,12 +2878,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2723,6 +2903,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2739,10 +2925,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2772,10 +2958,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2786,6 +2972,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2858,8 +3050,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2886,6 +3078,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2895,9 +3093,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2914,6 +3112,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4159,11 +4378,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4617,8 +4831,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4629,6 +4843,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 6e3b319da1e8..e7ce64ceec23 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1995,7 +1995,7 @@ "Buffer.isBuffer": true }, "packages": { - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true } }, "browserify>string_decoder": { diff --git a/package.json b/package.json index 652b8d4b3afb..7efae54424ff 100644 --- a/package.json +++ b/package.json @@ -346,7 +346,7 @@ "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", - "@metamask/rpc-errors": "^6.2.1", + "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^18.0.1", @@ -390,7 +390,6 @@ "eth-ens-namehash": "^2.0.8", "eth-lattice-keyring": "^0.12.4", "eth-method-registry": "^4.0.0", - "eth-rpc-errors": "^4.0.2", "ethereumjs-util": "^7.0.10", "extension-port-stream": "^3.0.0", "fast-json-patch": "^3.1.1", diff --git a/shared/modules/error.test.ts b/shared/modules/error.test.ts index 247ef302d09e..7fab2ee4e2e4 100644 --- a/shared/modules/error.test.ts +++ b/shared/modules/error.test.ts @@ -24,7 +24,7 @@ describe('error module', () => { expect(log.error).toHaveBeenCalledWith('test'); }); - it('calls loglevel.error with the parameter passed in when parameter is not an instance of Error', () => { + it('calls loglevel.error with string representation of parameter passed in when parameter is not an instance of Error', () => { logErrorWithMessage({ test: 'test' }); expect(log.error).toHaveBeenCalledWith({ test: 'test' }); }); diff --git a/shared/modules/error.ts b/shared/modules/error.ts index fa212365570f..04b754625257 100644 --- a/shared/modules/error.ts +++ b/shared/modules/error.ts @@ -1,24 +1,33 @@ import log from 'loglevel'; +import { + getErrorMessage as _getErrorMessage, + hasProperty, + isObject, + isErrorWithMessage, +} from '@metamask/utils'; + +export { isErrorWithMessage } from '@metamask/utils'; /** - * Type guard for determining whether the given value is an error object with a - * `message` property, such as an instance of Error. - * - * TODO: Remove once this becomes available at @metamask/utils + * Attempts to obtain the message from a possible error object, defaulting to an + * empty string if it is impossible to do so. * - * @param error - The object to check. - * @returns True or false, depending on the result. + * @param error - The possible error to get the message from. + * @returns The message if `error` is an object with a `message` property; + * the string version of `error` if it is not `undefined` or `null`; otherwise + * an empty string. */ -export function isErrorWithMessage( - error: unknown, -): error is { message: string } { - return typeof error === 'object' && error !== null && 'message' in error; +// TODO: Remove completely once changes implemented in @metamask/utils +export function getErrorMessage(error: unknown): string { + return isErrorWithMessage(error) && + hasProperty(error, 'cause') && + isObject(error.cause) && + hasProperty(error.cause, 'message') && + typeof error.cause.message === 'string' + ? error.cause.message + : _getErrorMessage(error); } export function logErrorWithMessage(error: unknown) { - if (isErrorWithMessage(error)) { - log.error(error.message); - } else { - log.error(error); - } + log.error(isErrorWithMessage(error) ? getErrorMessage(error) : error); } diff --git a/test/e2e/tests/dapp-interactions/provider-api.spec.js b/test/e2e/tests/dapp-interactions/provider-api.spec.js index 80cca60afb95..1c20b9fb2f6e 100644 --- a/test/e2e/tests/dapp-interactions/provider-api.spec.js +++ b/test/e2e/tests/dapp-interactions/provider-api.spec.js @@ -1,5 +1,5 @@ const { strict: assert } = require('assert'); -const { errorCodes } = require('eth-rpc-errors'); +const { errorCodes } = require('@metamask/rpc-errors'); const { defaultGanacheOptions, withFixtures, diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js index 40d577380f49..ad0dff033ae9 100644 --- a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js +++ b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { getCurrentQRHardwareState } from '../../../selectors'; import Popover from '../../ui/popover'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -44,7 +44,7 @@ const QRHardwarePopover = () => { dispatch( rejectPendingApproval( _txData.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); dispatch(cancelTx(_txData)); diff --git a/ui/components/multichain/import-account/import-account.js b/ui/components/multichain/import-account/import-account.js index cf5d13494e07..a37958074003 100644 --- a/ui/components/multichain/import-account/import-account.js +++ b/ui/components/multichain/import-account/import-account.js @@ -1,6 +1,7 @@ import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; +import { getErrorMessage } from '../../../../shared/modules/error'; import { MetaMetricsEventAccountImportType, MetaMetricsEventAccountType, @@ -50,8 +51,9 @@ export const ImportAccount = ({ onActionComplete }) => { return false; } } catch (error) { - trackImportEvent(strategy, error.message); - translateWarning(error.message); + const message = getErrorMessage(error); + trackImportEvent(strategy, message); + translateWarning(message); return false; } diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js index 3d40209ba62b..8b85e5cf1a4b 100644 --- a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js +++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, { useContext, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { getErrorMessage } from '../../../../shared/modules/error'; import { MetaMetricsEventName, MetaMetricsTokenEventSource, @@ -95,7 +96,7 @@ export const ImportNftsModal = ({ onClose }) => { dispatch(updateNftDropDownState(newNftDropdownState)); } catch (error) { - const { message } = error; + const message = getErrorMessage(error); dispatch(setNewNftAddedMessage(message)); setNftAddFailed(true); return; diff --git a/ui/ducks/send/helpers.js b/ui/ducks/send/helpers.js index 43582c6e3504..b48e05034f59 100644 --- a/ui/ducks/send/helpers.js +++ b/ui/ducks/send/helpers.js @@ -2,6 +2,7 @@ import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util'; import abi from 'human-standard-token-abi'; import BigNumber from 'bignumber.js'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { getErrorMessage } from '../../../shared/modules/error'; import { GAS_LIMITS, MIN_GAS_LIMIT_HEX } from '../../../shared/constants/gas'; import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; @@ -157,13 +158,14 @@ export async function estimateGasLimitForSend({ ); return addHexPrefix(estimateWithBuffer); } catch (error) { + const errorMessage = getErrorMessage(error); const simulationFailed = - error.message.includes('Transaction execution error.') || - error.message.includes( + errorMessage.includes('Transaction execution error.') || + errorMessage.includes( 'gas required exceeds allowance or always failing transaction', ) || (CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId] && - error.message.includes('gas required exceeds allowance')); + errorMessage.includes('gas required exceeds allowance')); if (simulationFailed) { const estimateWithBuffer = addGasBuffer( paramsForGasEstimate?.gas ?? gasLimit, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index cdbe7d2daa86..700fd466004d 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -7,11 +7,12 @@ import BigNumber from 'bignumber.js'; import { addHexPrefix, zeroAddress } from 'ethereumjs-util'; import { cloneDeep, debounce } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; +import { providerErrors } from '@metamask/rpc-errors'; import { TransactionEnvelopeType, TransactionType, } from '@metamask/transaction-controller'; -import { ethErrors } from 'eth-rpc-errors'; +import { getErrorMessage } from '../../../shared/modules/error'; import { decimalToHex, hexToDecimal, @@ -2702,12 +2703,13 @@ export function updateSendAsset( details.tokenId, ); } catch (err) { - if (err.message.includes('Unable to verify ownership.')) { + const message = getErrorMessage(err); + if (message.includes('Unable to verify ownership.')) { // this would indicate that either our attempts to verify ownership failed because of network issues, // or, somehow a token has been added to NFTs state with an incorrect chainId. } else { // Any other error is unexpected and should be surfaced. - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); } } @@ -2965,7 +2967,7 @@ export function signTransaction(history) { await dispatch( rejectPendingApproval( unapprovedSendTx.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), ); } diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js index 822db143d29a..dda856d64abd 100644 --- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { getTokenTrackerLink } from '@metamask/etherscan-link'; import classnames from 'classnames'; import { PageContainerFooter } from '../../components/ui/page-container'; @@ -125,7 +125,7 @@ const ConfirmAddSuggestedNFT = () => { return dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); }), @@ -421,7 +421,7 @@ const ConfirmAddSuggestedNFT = () => { rejectPendingApproval( id, serializeError( - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ), ), ); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js index c3e1a3f73bf0..f099ea80bfd5 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { BannerAlert, Button, @@ -147,7 +147,7 @@ const ConfirmAddSuggestedToken = () => { dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ), ), diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index cc9b39609030..a37812899ec9 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -1,5 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ConfirmAlertModal } from '../../../../../components/app/alert-system/confirm-alert-modal'; @@ -201,7 +201,7 @@ const Footer = () => { return; } - const error = ethErrors.provider.userRejectedRequest(); + const error = providerErrors.userRejectedRequest(); error.data = { location }; dispatch( diff --git a/ui/pages/confirmations/components/confirm/nav/nav.tsx b/ui/pages/confirmations/components/confirm/nav/nav.tsx index 6546b882b784..de0637a9f641 100644 --- a/ui/pages/confirmations/components/confirm/nav/nav.tsx +++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx @@ -1,4 +1,4 @@ -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -78,7 +78,7 @@ const Nav = () => { dispatch( rejectPendingApproval( conf.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); }); diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js index f9c9dbe9c0a1..026135a52685 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ObjectInspector } from 'react-inspector'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; import LedgerInstructionField from '../ledger-instruction-field'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; @@ -275,7 +275,7 @@ export default class SignatureRequestOriginal extends Component { await rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); @@ -304,7 +304,7 @@ export default class SignatureRequestOriginal extends Component { onCancel={async () => { await rejectPendingApproval( txData.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js index 817f9f8699d4..0ac6b877fa72 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js @@ -11,6 +11,7 @@ import { } from '../../../../store/actions'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) // eslint-disable-next-line import/order +import { getErrorMessage } from '../../../../../shared/modules/error'; import { mmiActionsFactory, setPersonalMessageInProgress, @@ -173,7 +174,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { } catch (err) { await dispatchProps.setWaitForConfirmDeepLinkDialog(true); await dispatchProps.showTransactionsFailedModal({ - errorMessage: err.message, + errorMessage: getErrorMessage(err), closeNotification: true, operationFailed: true, }); diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js index 1ade6dd1a630..e1effe5c8ff3 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js @@ -4,7 +4,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import log from 'loglevel'; import { isValidSIWEOrigin } from '@metamask/controller-utils'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { BannerAlert, Text } from '../../../../components/component-library'; import Popover from '../../../../components/ui/popover'; import Checkbox from '../../../../components/ui/check-box'; @@ -102,7 +102,7 @@ export default function SignatureRequestSIWE({ txData, warnings }) { await dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); } catch (e) { diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js index f15c7045e2d7..ce6967b50f70 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.js @@ -8,7 +8,7 @@ import { } from 'react-redux'; import PropTypes from 'prop-types'; import { memoize } from 'lodash'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { resolvePendingApproval, completedTx, @@ -176,7 +176,7 @@ const SignatureRequest = ({ txData, warnings }) => { await dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); trackEvent({ diff --git a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js index d14048897e39..998751c6e3a7 100644 --- a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js +++ b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import React from 'react'; import { RpcEndpointType } from '@metamask/network-controller'; @@ -564,7 +564,7 @@ function getValues(pendingApproval, t, actions, history, data) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: !originIsMetaMask, }; diff --git a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js index c03d03d3891a..7dbdb8c9c757 100644 --- a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js +++ b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { JustifyContent, SEVERITIES, @@ -85,7 +85,7 @@ function getValues(pendingApproval, t, actions) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: true, }; diff --git a/ui/pages/error/error.component.js b/ui/pages/error/error.component.js index 57a8e40c6473..f7ab9c593d40 100644 --- a/ui/pages/error/error.component.js +++ b/ui/pages/error/error.component.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; +import { getErrorMessage } from '../../../shared/modules/error'; import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; import { MetaMetricsContextProp, @@ -72,6 +73,7 @@ class ErrorPage extends PureComponent { const message = isPopup ? t('errorPagePopupMessage', [supportLink]) : t('errorPageMessage', [supportLink]); + const errorMessage = getErrorMessage(error); return (
@@ -81,8 +83,8 @@ class ErrorPage extends PureComponent {
{t('errorDetails')}
    - {error.message - ? this.renderErrorDetail(t('errorMessage', [error.message])) + {errorMessage + ? this.renderErrorDetail(t('errorMessage', [errorMessage])) : null} {error.code ? this.renderErrorDetail(t('errorCode', [error.code])) diff --git a/ui/pages/keychains/reveal-seed.js b/ui/pages/keychains/reveal-seed.js index 492f37545138..cf3e285eba64 100644 --- a/ui/pages/keychains/reveal-seed.js +++ b/ui/pages/keychains/reveal-seed.js @@ -2,6 +2,7 @@ import qrCode from 'qrcode-generator'; import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { getErrorMessage } from '../../../shared/modules/error'; import { MetaMetricsEventCategory, MetaMetricsEventKeyType, @@ -97,7 +98,7 @@ export default function RevealSeedPage() { reason: e.message, // 'incorrect_password', }, }); - setError(e.message); + setError(getErrorMessage(e)); }); }; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 417a82777b36..e32f85609406 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -443,7 +443,7 @@ export default class PermissionConnect extends Component { rejectSnapInstall={(requestId) => { rejectPendingApproval( requestId, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); this.setState({ permissionsApproved: true }); }} @@ -469,7 +469,7 @@ export default class PermissionConnect extends Component { rejectSnapUpdate={(requestId) => { rejectPendingApproval( requestId, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); this.setState({ permissionsApproved: false }); }} diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 9c5ab7ebb45e..43c7fb189822 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -9,7 +9,8 @@ import { captureException } from '@sentry/browser'; import { capitalize, isEqual } from 'lodash'; import { ThunkAction } from 'redux-thunk'; import { Action, AnyAction } from 'redux'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; +import type { DataWithOptionalCause } from '@metamask/rpc-errors'; import type { Hex, Json } from '@metamask/utils'; import { AssetsContractController, @@ -111,6 +112,7 @@ import { import { decimalToHex } from '../../shared/modules/conversion.utils'; import { TxGasFees, PriorityLevels } from '../../shared/constants/gas'; import { + getErrorMessage, isErrorWithMessage, logErrorWithMessage, } from '../../shared/modules/error'; @@ -228,7 +230,7 @@ export function createNewVaultAndRestore( dispatch(hideLoadingIndication()); }) .catch((err) => { - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); dispatch(hideLoadingIndication()); return Promise.reject(err); }); @@ -248,7 +250,7 @@ export function createNewVaultAndGetSeedPhrase( } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -272,7 +274,7 @@ export function unlockAndGetSeedPhrase( } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -375,7 +377,7 @@ export function resetAccount(): ThunkAction< dispatch(hideLoadingIndication()); if (err) { if (isErrorWithMessage(err)) { - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); } reject(err); return; @@ -575,11 +577,12 @@ export function connectHardware( ); } catch (error) { logErrorWithMessage(error); + const message = getErrorMessage(error); if ( deviceName === HardwareDeviceNames.ledger && ledgerTransportType === LedgerTransportTypes.webhid && isErrorWithMessage(error) && - error.message.match('Failed to open the device') + message.match('Failed to open the device') ) { dispatch(displayWarning(t('ledgerDeviceOpenFailureMessage'))); throw new Error(t('ledgerDeviceOpenFailureMessage')); @@ -1378,10 +1381,7 @@ export function cancelTx( return new Promise((resolve, reject) => { callBackgroundMethod( 'rejectPendingApproval', - [ - String(txMeta.id), - ethErrors.provider.userRejectedRequest().serialize(), - ], + [String(txMeta.id), providerErrors.userRejectedRequest().serialize()], (error) => { if (error) { reject(error); @@ -1427,10 +1427,7 @@ export function cancelTxs( new Promise((resolve, reject) => { callBackgroundMethod( 'rejectPendingApproval', - [ - String(id), - ethErrors.provider.userRejectedRequest().serialize(), - ], + [String(id), providerErrors.userRejectedRequest().serialize()], (err) => { if (err) { reject(err); @@ -1666,7 +1663,7 @@ export function lockMetamask(): ThunkAction< return backgroundSetLocked() .then(() => forceUpdateMetamaskState(dispatch)) .catch((error) => { - dispatch(displayWarning(error.message)); + dispatch(displayWarning(getErrorMessage(error))); return Promise.reject(error); }) .then(() => { @@ -2059,15 +2056,17 @@ export function addNftVerifyOwnership( tokenID, ]); } catch (error) { - if ( - isErrorWithMessage(error) && - (error.message.includes('This NFT is not owned by the user') || - error.message.includes('Unable to verify ownership')) - ) { - throw error; - } else { - logErrorWithMessage(error); - dispatch(displayWarning(error)); + if (isErrorWithMessage(error)) { + const message = getErrorMessage(error); + if ( + message.includes('This NFT is not owned by the user') || + message.includes('Unable to verify ownership') + ) { + throw error; + } else { + logErrorWithMessage(error); + dispatch(displayWarning(error)); + } } } finally { await forceUpdateMetamaskState(dispatch); @@ -2810,7 +2809,8 @@ export function displayWarning(payload: unknown): PayloadAction { if (isErrorWithMessage(payload)) { return { type: actionConstants.DISPLAY_WARNING, - payload: payload.message, + payload: + (payload as DataWithOptionalCause)?.cause?.message || payload.message, }; } else if (typeof payload === 'string') { return { @@ -4061,7 +4061,7 @@ export function rejectAllMessages( ): ThunkAction { return async (dispatch: MetaMaskReduxDispatch) => { const userRejectionError = serializeError( - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ); await Promise.all( messageList.map( diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts index 1604953f9b99..c1d2cfa062a5 100644 --- a/ui/store/institutional/institution-background.ts +++ b/ui/store/institutional/institution-background.ts @@ -12,7 +12,10 @@ import { submitRequestToBackground, } from '../background-connection'; import { MetaMaskReduxDispatch, MetaMaskReduxState } from '../store'; -import { isErrorWithMessage } from '../../../shared/modules/error'; +import { + isErrorWithMessage, + getErrorMessage, +} from '../../../shared/modules/error'; import { ConnectionRequest } from '../../../shared/constants/mmi-controller'; export function showInteractiveReplacementTokenBanner({ @@ -34,8 +37,8 @@ export function showInteractiveReplacementTokenBanner({ // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { if (err) { - dispatch(displayWarning(err.message)); - throw new Error(err.message); + dispatch(displayWarning(err)); + throw new Error(getErrorMessage(err)); } } }; @@ -80,7 +83,7 @@ export function setTypedMessageInProgress(msgId: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { log.error(error); - dispatch(displayWarning(error.message)); + dispatch(displayWarning(error)); } finally { await forceUpdateMetamaskState(dispatch); dispatch(hideLoadingIndication()); @@ -97,7 +100,7 @@ export function setPersonalMessageInProgress(msgId: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { log.error(error); - dispatch(displayWarning(error.message)); + dispatch(displayWarning(error)); } finally { await forceUpdateMetamaskState(dispatch); dispatch(hideLoadingIndication()); @@ -135,7 +138,7 @@ export function mmiActionsFactory() { } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -157,7 +160,7 @@ export function mmiActionsFactory() { return () => { callBackgroundMethod(name, [payload], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; diff --git a/yarn.lock b/yarn.lock index 6d5582a99ee1..825c8bf06d8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6170,12 +6170,22 @@ __metadata: linkType: hard "@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1": - version: 6.3.1 - resolution: "@metamask/rpc-errors@npm:6.3.1" + version: 6.4.0 + resolution: "@metamask/rpc-errors@npm:6.4.0" + dependencies: + "@metamask/utils": "npm:^9.0.0" + fast-safe-stringify: "npm:^2.0.6" + checksum: 10/9a17525aa8ce9ac142a94c04000dba7f0635e8e155c6c045f57eca36cc78c255318cca2fad4571719a427dfd2df64b70bc6442989523a8de555480668d666ad5 + languageName: node + linkType: hard + +"@metamask/rpc-errors@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/rpc-errors@npm:7.0.0" dependencies: "@metamask/utils": "npm:^9.0.0" fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f968fb490b13b632c2ad4770a144d67cecdff8d539cb8b489c732b08dab7a62fae65d7a2908ce8c5b77260317aa618948a52463f093fa8d9f84aee1c5f6f5daf + checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd languageName: node linkType: hard @@ -26152,7 +26162,7 @@ __metadata: "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^18.0.1" @@ -26306,7 +26316,6 @@ __metadata: eth-ens-namehash: "npm:^2.0.8" eth-lattice-keyring: "npm:^0.12.4" eth-method-registry: "npm:^4.0.0" - eth-rpc-errors: "npm:^4.0.2" ethereumjs-util: "npm:^7.0.10" ethers: "npm:5.7.0" extension-port-stream: "npm:^3.0.0" From 55d09729f2bdc2359094c5524b6b43c5e00cde94 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 17 Oct 2024 07:29:50 +0400 Subject: [PATCH 18/51] fix: SonarCloud for forks (#27700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27700?quickstart=1) This PR fixes SonarCloud for forks. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27135 ## **Manual testing steps** 1. SonarCloud analysis is successfully reported from a fork ## **Screenshots/Recordings** Not applicable ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com> Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Mark Stacey --- .github/workflows/main.yml | 9 -------- .github/workflows/sonarcloud.yml | 36 ++++++++++++++++++++++++++++---- sonar-project.properties | 4 ++++ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5d1b4d73bdab..f3cc68bebcec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,21 +32,12 @@ jobs: name: Run tests uses: ./.github/workflows/run-tests.yml - sonarcloud: - name: SonarCloud - uses: ./.github/workflows/sonarcloud.yml - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - needs: - - run-tests - all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest needs: - check-workflows - run-tests - - sonarcloud outputs: PASSED: ${{ steps.set-output.outputs.PASSED }} steps: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 460d5c140462..9ca9f02e2ae5 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,19 +1,33 @@ +# This GitHub action will checkout and scan third party code. +# Please ensure that any changes to this action do not perform +# actions that may result in code from that branch being executed +# such as installing dependencies or running build scripts. + name: SonarCloud on: - workflow_call: - secrets: - SONAR_TOKEN: - required: true + workflow_run: + workflows: + - Run tests + types: + - completed + +permissions: + actions: read jobs: sonarcloud: + # Only scan code from non-forked repositories that have passed the tests + # This will skip scanning the code for forks, but will run for the main repository on PRs from forks + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.repository.fork == false }} name: SonarCloud runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} # Use the repository that triggered the workflow + ref: ${{ github.event.workflow_run.head_branch }} # Use the branch that triggered the workflow fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: Download artifacts @@ -21,6 +35,20 @@ jobs: with: name: lcov.info path: coverage + github-token: ${{ github.token }} # This is required when downloading artifacts from a different repository or from a different workflow run. + run-id: ${{ github.event.workflow_run.id }} # Use the workflow id that triggered the workflow + + - name: Download sonar-project.properties + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: MetaMask/metamask-extension + run: | + sonar_project_properties=$(gh api -H "Accept: application/vnd.github.raw" "repos/$REPOSITORY/contents/sonar-project.properties") + if [ -z "$sonar_project_properties" ]; then + echo "::error::sonar-project.properties not found in $REPOSITORY. Please make sure this file exists on the default branch." + exit 1 + fi + echo "$sonar_project_properties" > sonar-project.properties - name: SonarCloud Scan # This is SonarSource/sonarcloud-github-action@v2.0.0 diff --git a/sonar-project.properties b/sonar-project.properties index ad18a60d6fc7..4362539a94ff 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,7 @@ +# Note: Updating this file on feature branches or forks will not reflect changes in the SonarCloud scan results. +# The SonarCloud scan workflow always uses the latest version from the default branch. +# This means any changes made to this file in a feature branch will not be considered until they are merged. + sonar.projectKey=metamask-extension sonar.organization=consensys From 01ea106de1f6bcd2b122d3b9b8c3fc862591f35a Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Thu, 17 Oct 2024 04:16:11 +0000 Subject: [PATCH 19/51] fix: fall back to bundled chainlist (#23392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The list of known chains is fetched at runtime from `https://chainid.network/chains.json` and cached. There are some issues with the way this works: - MetaMask will not have a list of chains until online (if ever) - When the 24h cache timeout expires, the chainlist becomes unavailable This PR addresses this by: - Refactoring out `https://chainid.network/chains.json` into constant `CHAIN_SPEC_URL` - Add new optional option `allowStale` to `fetchWithCache`. If set to `true`, it will falling back to return any entry instead of throwing an error when a request fail. - Set `allowStale` to `true` for all requests to `CHAIN_SPEC_URL` - Seed the fetch cache for `CHAIN_SPEC_URL` with [`eth-chainlist`](https://www.npmjs.com/package/eth-chainlist), which is the same data exposed via a published npm package. - Open for suggestions on if this should be bundled differently - maybe we want our own equivalent mirror? While an improvement, this could still be further improved. - The bundled result could be used immediately in all cases without waiting for response - The cached data could be updated asynchronously in the background, without being prompted by user action I currently consider these out-of-scope for this PR. Or put more generally: Decoupling the fetching of the data from its use would be even better. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/PR?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've clearly explained what problem this PR is solving and how it is solved. - [ ] I've linked related issues - [x] I've included manual testing steps - [x] I've included screenshots/recordings if applicable - [ ] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [x] I’ve properly set the pull request status: - [x] In case it's not yet "ready for review", I've set it to "draft". - [x] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 25 +++++++++++++++++++ package.json | 1 + shared/constants/network.ts | 1 + shared/lib/fetch-with-cache.ts | 7 ++++++ ui/hooks/useIsOriginalNativeTokenSymbol.js | 4 ++- .../confirmation/confirmation.js | 4 ++- .../networks-form/use-safe-chains.ts | 4 ++- yarn.lock | 8 ++++++ 8 files changed, 51 insertions(+), 3 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 87f7570f2d19..93f8007d4d2f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -42,6 +42,7 @@ import { LedgerIframeBridge, } from '@metamask/eth-ledger-bridge-keyring'; import LatticeKeyring from 'eth-lattice-keyring'; +import { rawChainData } from 'eth-chainlist'; import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; import EthQuery from '@metamask/eth-query'; import EthJSQuery from '@metamask/ethjs-query'; @@ -169,6 +170,7 @@ import { } from '../../shared/constants/swaps'; import { CHAIN_IDS, + CHAIN_SPEC_URL, NETWORK_TYPES, NetworkStatus, MAINNET_DISPLAY_NAME, @@ -200,6 +202,10 @@ import { } from '../../shared/constants/metametrics'; import { LOG_EVENT } from '../../shared/constants/logs'; +import { + getStorageItem, + setStorageItem, +} from '../../shared/lib/storage-helpers'; import { getTokenIdParam, fetchTokenBalance, @@ -413,6 +419,8 @@ export default class MetamaskController extends EventEmitter { this.getRequestAccountTabIds = opts.getRequestAccountTabIds; this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds; + this.initializeChainlist(); + this.controllerMessenger = new ControllerMessenger(); this.loggingController = new LoggingController({ @@ -6304,6 +6312,23 @@ export default class MetamaskController extends EventEmitter { }); } + /** + * The chain list is fetched live at runtime, falling back to a cache. + * This preseeds the cache at startup with a static list provided at build. + */ + async initializeChainlist() { + const cacheKey = `cachedFetch:${CHAIN_SPEC_URL}`; + const { cachedResponse } = (await getStorageItem(cacheKey)) || {}; + if (cachedResponse) { + return; + } + await setStorageItem(cacheKey, { + cachedResponse: rawChainData(), + // Cached value is immediately invalidated + cachedTime: 0, + }); + } + /** * Returns the nonce that will be associated with a transaction once approved * diff --git a/package.json b/package.json index 7efae54424ff..404dbc8e50df 100644 --- a/package.json +++ b/package.json @@ -387,6 +387,7 @@ "currency-formatter": "^1.4.2", "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", + "eth-chainlist": "~0.0.498", "eth-ens-namehash": "^2.0.8", "eth-lattice-keyring": "^0.12.4", "eth-method-registry": "^4.0.0", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index a98417794d81..9ed2e26150a9 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -96,6 +96,7 @@ export const NETWORK_NAMES = { HOMESTEAD: 'homestead', }; +export const CHAIN_SPEC_URL = 'https://chainid.network/chains.json'; /** * An object containing all of the chain ids for networks both built in and * those that we have added custom code to support our feature set. diff --git a/shared/lib/fetch-with-cache.ts b/shared/lib/fetch-with-cache.ts index 969fba9f869f..66610ec925b8 100644 --- a/shared/lib/fetch-with-cache.ts +++ b/shared/lib/fetch-with-cache.ts @@ -7,6 +7,7 @@ const fetchWithCache = async ({ fetchOptions = {}, cacheOptions: { cacheRefreshTime = MINUTE * 6, timeout = SECOND * 30 } = {}, functionName = '', + allowStale = false, }: { url: string; // TODO: Replace `any` with type @@ -16,6 +17,7 @@ const fetchWithCache = async ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any cacheOptions?: Record; functionName: string; + allowStale?: boolean; }) => { if ( fetchOptions.body || @@ -49,6 +51,11 @@ const fetchWithCache = async ({ ...fetchOptions, }); if (!response.ok) { + const message = `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`; + if (allowStale) { + console.debug(`${message}. Returning cached result`); + return cachedResponse; + } throw new Error( `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`, ); diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.js b/ui/hooks/useIsOriginalNativeTokenSymbol.js index 9a546dba8305..65811c4d656c 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.js @@ -4,6 +4,7 @@ import fetchWithCache from '../../shared/lib/fetch-with-cache'; import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, CHAIN_ID_TO_CURRENCY_SYMBOL_MAP_NETWORK_COLLISION, + CHAIN_SPEC_URL, } from '../../shared/constants/network'; import { DAY } from '../../shared/constants/time'; import { useSafeChainsListValidationSelector } from '../selectors'; @@ -78,7 +79,8 @@ export function useIsOriginalNativeTokenSymbol( } const safeChainsList = await fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, functionName: 'getSafeChainsList', }); diff --git a/ui/pages/confirmations/confirmation/confirmation.js b/ui/pages/confirmations/confirmation/confirmation.js index 12b2af503f7f..4bb1f4f7d203 100644 --- a/ui/pages/confirmations/confirmation/confirmation.js +++ b/ui/pages/confirmations/confirmation/confirmation.js @@ -14,6 +14,7 @@ import { produce } from 'immer'; import log from 'loglevel'; import { ApprovalType } from '@metamask/controller-utils'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; +import { CHAIN_SPEC_URL } from '../../../../shared/constants/network'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { MetaMetricsEventCategory, @@ -372,7 +373,8 @@ export default function ConfirmationPage({ try { if (useSafeChainsListValidation) { const response = await fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, functionName: 'getSafeChainsList', }); diff --git a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts index 56b237e9fce4..3556c2196b31 100644 --- a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts +++ b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { useSafeChainsListValidationSelector } from '../../../../selectors'; import fetchWithCache from '../../../../../shared/lib/fetch-with-cache'; +import { CHAIN_SPEC_URL } from '../../../../../shared/constants/network'; import { DAY } from '../../../../../shared/constants/time'; export type SafeChain = { @@ -25,8 +26,9 @@ export const useSafeChains = () => { if (useSafeChainsListValidation) { useEffect(() => { fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, functionName: 'getSafeChainsList', + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, }) .then((response) => { diff --git a/yarn.lock b/yarn.lock index 825c8bf06d8a..4d604200c496 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18431,6 +18431,13 @@ __metadata: languageName: node linkType: hard +"eth-chainlist@npm:~0.0.498": + version: 0.0.498 + resolution: "eth-chainlist@npm:0.0.498" + checksum: 10/a414c0e1f0a877f9ab8bf1cf775556308ddbb66618e368666d4dea9a0b949febedf8ca5440cf57419413404e7661f1e3d040802faf532d0e1618c40ecd334cbf + languageName: node + linkType: hard + "eth-eip712-util-browser@npm:^0.0.3": version: 0.0.3 resolution: "eth-eip712-util-browser@npm:0.0.3" @@ -26313,6 +26320,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.2.0" eslint-plugin-storybook: "npm:^0.6.15" eta: "npm:^3.2.0" + eth-chainlist: "npm:~0.0.498" eth-ens-namehash: "npm:^2.0.8" eth-lattice-keyring: "npm:^0.12.4" eth-method-registry: "npm:^4.0.0" From 935ad43bc68da8044a83f079eea22203f929ced1 Mon Sep 17 00:00:00 2001 From: Priya Date: Thu, 17 Oct 2024 08:52:01 +0200 Subject: [PATCH 20/51] test: Update test-dapp to verison 8.7.0 (#27816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrade the test-dapp version from 8.4.0 to 8.7.0 Update failing permit tests due to the upgrade [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27816?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- test/e2e/tests/confirmations/signatures/permit.spec.ts | 8 ++++---- yarn.lock | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 404dbc8e50df..4951a4106e47 100644 --- a/package.json +++ b/package.json @@ -478,7 +478,7 @@ "@metamask/phishing-warning": "^4.0.0", "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", - "@metamask/test-dapp": "^8.4.0", + "@metamask/test-dapp": "8.7.0", "@octokit/core": "^3.6.0", "@open-rpc/meta-schema": "^1.14.6", "@open-rpc/mock-server": "^1.7.5", diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index c9c4ca9399f4..5c52d1f029ee 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -152,22 +152,22 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.waitForSelector({ css: '#signPermitResult', - text: '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', + text: '0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d730103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea491b', }); await driver.waitForSelector({ css: '#signPermitResultR', - text: 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', + text: 'r: 0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d73', }); await driver.waitForSelector({ css: '#signPermitResultS', - text: 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', + text: 's: 0x0103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea49', }); await driver.waitForSelector({ css: '#signPermitResultV', - text: 'v: 28', + text: 'v: 27', }); await driver.waitForSelector({ css: '#signPermitVerifyResult', diff --git a/yarn.lock b/yarn.lock index 4d604200c496..9f547f225d9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6478,10 +6478,10 @@ __metadata: languageName: node linkType: hard -"@metamask/test-dapp@npm:^8.4.0": - version: 8.4.0 - resolution: "@metamask/test-dapp@npm:8.4.0" - checksum: 10/9d9c4df11c2b18c72b52e8743435ed0bd18815dd7a7aed43cf3a2cce1b9ef8926909890d00b4b624446f73b88c15e95bc0190c5437b9dad437a0e345a6b430ba +"@metamask/test-dapp@npm:8.7.0": + version: 8.7.0 + resolution: "@metamask/test-dapp@npm:8.7.0" + checksum: 10/c2559179d3372e5fc8d67a60c1e4056fad9809486eaff6a2aa9c351a2a613eeecc15885a5fd9b71b8f4139058fe168abeac06bd6bdb6d4a47fe0b9b4146923ab languageName: node linkType: hard @@ -26181,7 +26181,7 @@ __metadata: "@metamask/snaps-sdk": "npm:^6.9.0" "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/test-bundler": "npm:^1.0.0" - "@metamask/test-dapp": "npm:^8.4.0" + "@metamask/test-dapp": "npm:8.7.0" "@metamask/transaction-controller": "npm:^37.2.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^9.3.0" From 2afe52e29850b3afa351c29e34c8f71a887186b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Oliv=C3=A9?= Date: Thu, 17 Oct 2024 11:55:27 +0200 Subject: [PATCH 21/51] feat(logging): add extension request logging and retrieval (#27655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** feat: Implement improved API request logging for MMI - Add API request logging in MMI controller - Implement logAndStoreApiRequest method in MetaMask controller - Update UI to use new logging mechanism - Add types and interfaces for API call log entries - Fixed MMI e2e tests ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMI-5436 ## **Manual testing steps** 1. Open the extension in your browser. 2. Click on the three dots (menu icon) and select Settings. 3. Go to the Advanced section and search for State Logs. 4. Click Download Logs. 5. Open the downloaded file and look for API request logs to review the necessary data. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- .../controllers/mmi-controller.test.ts | 99 +++++++++++++ app/scripts/controllers/mmi-controller.ts | 24 +++- app/scripts/metamask-controller.js | 3 + package.json | 14 +- .../mmi/pageObjects/mmi-dummyApp-page.ts | 15 +- .../interactive-replacement-token-modal.tsx | 40 +++--- .../institution-background.test.js | 72 ++++++++++ .../institutional/institution-background.ts | 7 + yarn.lock | 135 +++++++++--------- 9 files changed, 305 insertions(+), 104 deletions(-) diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index dbef190a5573..0c4aa2d5d874 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -24,6 +24,7 @@ import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../test/stub/networks'; +import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ handleMmiPortfolio: jest.fn(), @@ -353,6 +354,33 @@ describe('MMIController', function () { mmiController.mmiConfigurationController.storeConfiguration, ).toHaveBeenCalled(); }); + + it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => { + const mockKeyring = { + on: jest.fn(), + getAccounts: jest.fn().mockResolvedValue([]), + getSupportedChains: jest.fn().mockResolvedValue({}), + }; + + mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring); + mmiController.custodyController.getAllCustodyTypes = jest.fn().mockReturnValue(['mock-custody-type']); + mmiController.logAndStoreApiRequest = jest.fn(); + + await mmiController.onSubmitPassword(); + + expect(mockKeyring.on).toHaveBeenCalledWith( + API_REQUEST_LOG_EVENT, + expect.any(Function) + ); + + const mockLogData = { someKey: 'someValue' }; + const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find( + call => call[0] === API_REQUEST_LOG_EVENT + )[1]; + apiRequestLogEventHandler(mockLogData); + + expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData); + }); }); describe('connectCustodyAddresses', () => { @@ -408,6 +436,54 @@ describe('MMIController', function () { ).toHaveBeenCalled(); expect(result).toEqual(['0x1']); }); + + it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => { + const custodianType = 'mock-custodian-type'; + const custodianName = 'mock-custodian-name'; + const accounts = { + '0x1': { + name: 'Account 1', + custodianDetails: {}, + labels: [], + token: 'token', + chainId: 1, + }, + }; + CUSTODIAN_TYPES['MOCK-CUSTODIAN-TYPE'] = { + keyringClass: { type: 'mock-keyring-class' }, + }; + + const mockKeyring = { + on: jest.fn(), + setSelectedAddresses: jest.fn(), + addAccounts: jest.fn(), + getStatusMap: jest.fn(), + }; + + mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring); + mmiController.keyringController.getAccounts = jest.fn().mockResolvedValue(['0x2']); + mmiController.keyringController.addNewAccountForKeyring = jest.fn().mockResolvedValue('0x3'); + mmiController.custodyController.setAccountDetails = jest.fn(); + mmiController.accountTrackerController.syncWithAddresses = jest.fn(); + mmiController.storeCustodianSupportedChains = jest.fn(); + mmiController.custodyController.storeCustodyStatusMap = jest.fn(); + mmiController.logAndStoreApiRequest = jest.fn(); + + await mmiController.connectCustodyAddresses(custodianType, custodianName, accounts); + + expect(mockKeyring.on).toHaveBeenCalledWith( + API_REQUEST_LOG_EVENT, + expect.any(Function) + ); + + const mockLogData = { someKey: 'someValue' }; + const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find( + call => call[0] === API_REQUEST_LOG_EVENT + )[1]; + apiRequestLogEventHandler(mockLogData); + + expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData); + }); }); describe('getCustodianAccounts', () => { @@ -783,4 +859,27 @@ describe('MMIController', function () { ).toHaveBeenCalledWith('/new-account/connect'); }); }); + + describe('logAndStoreApiRequest', () => { + it('should call custodyController.sanitizeAndLogApiCall with the provided log data', async () => { + const mockLogData = { someKey: 'someValue' }; + const mockSanitizedLogs = { sanitizedKey: 'sanitizedValue' }; + + mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockResolvedValue(mockSanitizedLogs); + + const result = await mmiController.logAndStoreApiRequest(mockLogData); + + expect(mmiController.custodyController.sanitizeAndLogApiCall).toHaveBeenCalledWith(mockLogData); + expect(result).toEqual(mockSanitizedLogs); + }); + + it('should handle errors and throw them', async () => { + const mockLogData = { someKey: 'someValue' }; + const mockError = new Error('Sanitize error'); + + mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockRejectedValue(mockError); + + await expect(mmiController.logAndStoreApiRequest(mockLogData)).rejects.toThrow('Sanitize error'); + }); + }); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 2373484d4a6e..8c5f1ee4b49b 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -13,12 +13,14 @@ import { import { REFRESH_TOKEN_CHANGE_EVENT, INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, + API_REQUEST_LOG_EVENT, } from '@metamask-institutional/sdk'; import { handleMmiPortfolio } from '@metamask-institutional/portfolio-dashboard'; -import { TransactionMeta } from '@metamask/transaction-controller'; -import { KeyringTypes } from '@metamask/keyring-controller'; import { CustodyController } from '@metamask-institutional/custody-controller'; +import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; import { SignatureController } from '@metamask/signature-controller'; import { OriginalRequest, @@ -304,6 +306,10 @@ export default class MMIController extends EventEmitter { }, ); + keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => { + this.logAndStoreApiRequest(logData); + }); + // store the supported chains for this custodian type const accounts = await keyring.getAccounts(); addresses = addresses.concat(...accounts); @@ -419,6 +425,10 @@ export default class MMIController extends EventEmitter { }, ); + keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => { + this.logAndStoreApiRequest(logData); + }); + if (!keyring) { throw new Error('Unable to get keyring'); } @@ -884,4 +894,14 @@ export default class MMIController extends EventEmitter { this.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE); return true; } + + async logAndStoreApiRequest(logData: IApiCallLogEntry) { + try { + const logs = await this.custodyController.sanitizeAndLogApiCall(logData); + return logs; + } catch (error) { + log.error('Error fetching extension request logs:', error); + throw error; + } + } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 93f8007d4d2f..176c7aea10e5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3776,6 +3776,9 @@ export default class MetamaskController extends EventEmitter { appStateController.setCustodianDeepLink.bind(appStateController), setNoteToTraderMessage: appStateController.setNoteToTraderMessage.bind(appStateController), + logAndStoreApiRequest: this.mmiController.logAndStoreApiRequest.bind( + this.mmiController, + ), ///: END:ONLY_INCLUDE_IF // snaps diff --git a/package.json b/package.json index 4951a4106e47..57dc9224b765 100644 --- a/package.json +++ b/package.json @@ -285,15 +285,15 @@ "@lavamoat/lavadome-react": "0.0.17", "@lavamoat/snow": "^2.0.2", "@material-ui/core": "^4.11.0", - "@metamask-institutional/custody-controller": "^0.2.31", - "@metamask-institutional/custody-keyring": "^2.0.3", - "@metamask-institutional/extension": "^0.3.27", - "@metamask-institutional/institutional-features": "^1.3.5", + "@metamask-institutional/custody-controller": "^0.3.0", + "@metamask-institutional/custody-keyring": "^2.1.0", + "@metamask-institutional/extension": "^0.3.28", + "@metamask-institutional/institutional-features": "^1.3.6", "@metamask-institutional/portfolio-dashboard": "^1.4.1", "@metamask-institutional/rpc-allowlist": "^1.0.3", - "@metamask-institutional/sdk": "^0.1.30", - "@metamask-institutional/transaction-update": "^0.2.5", - "@metamask-institutional/types": "^1.1.0", + "@metamask-institutional/sdk": "^0.2.0", + "@metamask-institutional/transaction-update": "^0.2.6", + "@metamask-institutional/types": "^1.2.0", "@metamask/abi-utils": "^2.0.2", "@metamask/account-watcher": "^4.1.1", "@metamask/accounts-controller": "^18.2.2", diff --git a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts index a9a175e82971..cf455dc0a7e0 100644 --- a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts +++ b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts @@ -34,10 +34,11 @@ export class DummyAppPage { this.connectBtn.click(), ]); await popup1.waitForLoadState(); - // Check which account is selected and select if required - await popup1.locator('.check-box__indeterminate'); - await popup1.locator('button:has-text("Next")').click(); - await popup1.locator('button:has-text("Confirm")').click(); + await popup1.getByTestId('edit').nth(1).click(); + await popup1.getByText('Select all').click(); + await popup1.getByTestId('Sepolia').click(); + await popup1.getByTestId('connect-more-chains-button').click(); + await popup1.getByTestId('confirm-btn').click(); await popup1.close(); } @@ -60,11 +61,7 @@ export class DummyAppPage { if (isSign) { await popup.click('button:has-text("Confirm")'); } else { - await popup.getByTestId('page-container-footer-next').click(); - - if (buttonId === 'approveTokens') { - await popup.getByTestId('page-container-footer-next').click(); - } + await popup.getByTestId('confirm-footer-button').click(); await popup .getByTestId('custody-confirm-link__btn') diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx index 4526595fa455..06fe1336ca7e 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx @@ -85,30 +85,26 @@ const InteractiveReplacementTokenModal: React.FC = () => { {t('custodyRefreshTokenModalTitle')} - { - // @ts-expect-error: todo: Merge MetaMask Institutional PR 778 to fix this - custodian.iconUrl ? ( - - - {custodian.displayName} - - - ) : ( + {custodian.iconUrl ? ( + - {custodian.displayName} + {custodian.displayName} - ) - } + + ) : ( + + {custodian.displayName} + + )} ({ @@ -173,4 +174,75 @@ describe('Institution Actions', () => { ); }); }); + + describe('#logAndStoreApiRequest', () => { + it('should call submitRequestToBackground with correct parameters', async () => { + const mockLogData = { + id: '123', + method: 'GET', + request: { + url: 'https://api.example.com/data', + headers: { 'Content-Type': 'application/json' }, + }, + response: { + status: 200, + body: '{"success": true}', + }, + timestamp: 1234567890, + }; + + await logAndStoreApiRequest(mockLogData); + + expect(submitRequestToBackground).toHaveBeenCalledWith( + 'logAndStoreApiRequest', + [mockLogData], + ); + }); + + it('should return the result from submitRequestToBackground', async () => { + const mockLogData = { + id: '456', + method: 'POST', + request: { + url: 'https://api.example.com/submit', + headers: { 'Content-Type': 'application/json' }, + body: '{"data": "test"}', + }, + response: { + status: 201, + body: '{"id": "789"}', + }, + timestamp: 1234567890, + }; + + submitRequestToBackground.mockResolvedValue('success'); + + const result = await logAndStoreApiRequest(mockLogData); + + expect(result).toBe('success'); + }); + + it('should throw an error if submitRequestToBackground fails', async () => { + const mockLogData = { + id: '789', + method: 'GET', + request: { + url: 'https://api.example.com/error', + headers: { 'Content-Type': 'application/json' }, + }, + response: { + status: 500, + body: '{"error": "Internal Server Error"}', + }, + timestamp: 1234567890, + }; + + const mockError = new Error('Background request failed'); + submitRequestToBackground.mockRejectedValue(mockError); + + await expect(logAndStoreApiRequest(mockLogData)).rejects.toThrow( + 'Background request failed', + ); + }); + }); }); diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts index c1d2cfa062a5..fd42d069b8b7 100644 --- a/ui/store/institutional/institution-background.ts +++ b/ui/store/institutional/institution-background.ts @@ -1,6 +1,7 @@ import log from 'loglevel'; import { ThunkAction } from 'redux-thunk'; import { AnyAction } from 'redux'; +import { IApiCallLogEntry } from '@metamask-institutional/types'; import { forceUpdateMetamaskState, displayWarning, @@ -108,6 +109,12 @@ export function setPersonalMessageInProgress(msgId: string) { }; } +export async function logAndStoreApiRequest( + logData: IApiCallLogEntry, +): Promise { + return await submitRequestToBackground('logAndStoreApiRequest', [logData]); +} + /** * A factory that contains all MMI actions ready to use * Example usage: diff --git a/yarn.lock b/yarn.lock index 9f547f225d9a..a0f9df5f4706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4627,60 +4627,60 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/custody-controller@npm:^0.2.30, @metamask-institutional/custody-controller@npm:^0.2.31": - version: 0.2.31 - resolution: "@metamask-institutional/custody-controller@npm:0.2.31" +"@metamask-institutional/custody-controller@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask-institutional/custody-controller@npm:0.3.0" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/f856c98db42a21639d9ec5d1c835bc302b5a1b3fb821aae8641f63a9400f8303b8fa578368a2f2d2a1ec0c148c070f809b8c0fa46fa3fd2fa29f80e0ec1da207 + checksum: 10/572e96d4b23566fb8dbf06ab0117c68c2d1db901deea69eee48d08f41ea3e1dbbbb3090c83cce6ff240ed8061e84df1b61befaf57da764b495eb0978d45fac42 languageName: node linkType: hard -"@metamask-institutional/custody-keyring@npm:^2.0.3": - version: 2.0.3 - resolution: "@metamask-institutional/custody-keyring@npm:2.0.3" +"@metamask-institutional/custody-keyring@npm:^2.1.0": + version: 2.1.0 + resolution: "@metamask-institutional/custody-keyring@npm:2.1.0" dependencies: "@ethereumjs/tx": "npm:^4.1.1" "@ethereumjs/util": "npm:^8.0.5" "@metamask-institutional/configuration-client": "npm:^2.0.1" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/obs-store": "npm:^9.0.0" crypto: "npm:^1.0.1" lodash.clonedeep: "npm:^4.5.0" - checksum: 10/987beeeed67fb92a436eb1318f48ec2cc0ceb1ae944b7f5b2e492dcdc28a4298c5a8d25a520022ac52f87a411f7341961100be47a9626fbb1674aed349d98737 + checksum: 10/78421e38fed4ad88412593a307fc13f220b0e5a83dee76de0032c835a7896bf23bb76030e4bb7d69bfa604db7a31faa6312ac64b05cc135d8afb723fb3660920 languageName: node linkType: hard -"@metamask-institutional/extension@npm:^0.3.27": - version: 0.3.27 - resolution: "@metamask-institutional/extension@npm:0.3.27" +"@metamask-institutional/extension@npm:^0.3.28": + version: 0.3.28 + resolution: "@metamask-institutional/extension@npm:0.3.28" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-controller": "npm:^0.2.30" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" + "@metamask-institutional/custody-controller": "npm:^0.3.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.1" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/transaction-update": "npm:^0.2.5" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/transaction-update": "npm:^0.2.6" + "@metamask-institutional/types": "npm:^1.2.0" jest-create-mock-instance: "npm:^2.0.0" jest-fetch-mock: "npm:3.0.3" lodash.clonedeep: "npm:^4.5.0" - checksum: 10/dc9eefe8045607cd415b9db4a8df833c9a523e9d06a3a0e49e4c6e85063924db1f117725a91c926f19ce26d0701fc175ea4ad38fb13a8a3b092434bcd7fd7882 + checksum: 10/a1f73c5281282ab1315ee19dd363330504300c036586ff64c98c176da8ac23046de8e8051956b4e15184faf0720bf324b81c406a1bf85295691c24f191b8f747 languageName: node linkType: hard -"@metamask-institutional/institutional-features@npm:^1.3.5": - version: 1.3.5 - resolution: "@metamask-institutional/institutional-features@npm:1.3.5" +"@metamask-institutional/institutional-features@npm:^1.3.6": + version: 1.3.6 + resolution: "@metamask-institutional/institutional-features@npm:1.3.6" dependencies: - "@metamask-institutional/custody-keyring": "npm:^2.0.3" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/1a154dbbfc71c9fee43d755d901423e3ea17ad149679225481fdc2d73ae95960e1805a792dbe660dd778703614ea5fd7390314bd7099c8ede510db1d23bc08ab + checksum: 10/a6b53f1b0ba8554595498153cbc0d32bb1a2d8374ad6ff9b617fea4e10872120000d14d9916b48ff9bafbac5da954ada99dca5f88f3ba21d4fbb80590804444c languageName: node linkType: hard @@ -4698,17 +4698,17 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/sdk@npm:^0.1.30": - version: 0.1.30 - resolution: "@metamask-institutional/sdk@npm:0.1.30" +"@metamask-institutional/sdk@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask-institutional/sdk@npm:0.2.0" dependencies: "@metamask-institutional/simplecache": "npm:^1.1.0" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/types": "npm:^1.2.0" "@types/jsonwebtoken": "npm:^9.0.1" - "@types/node": "npm:^20.11.17" + "@types/node": "npm:^20.14.9" bignumber.js: "npm:^9.1.1" jsonwebtoken: "npm:^9.0.0" - checksum: 10/3f36925fa9399a0ea06e2a64ea89accfb34f0a17581ab69652b4f325a948db10e88faebcca4f7c2d9f5f1f1c7f98bd8f970b7a489218dfd1be8cebc669a2f67e + checksum: 10/59f8b5eff176746ef3c9c406edda340ab04b37df1799d9b56e26fcede95441461d73d4be8b33f1dc3153cddea6baa876eba1232ca538da8f732a29801531a2f8 languageName: node linkType: hard @@ -4719,36 +4719,36 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/transaction-update@npm:^0.2.5": - version: 0.2.5 - resolution: "@metamask-institutional/transaction-update@npm:0.2.5" +"@metamask-institutional/transaction-update@npm:^0.2.6": + version: 0.2.6 + resolution: "@metamask-institutional/transaction-update@npm:0.2.6" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" - "@metamask-institutional/websocket-client": "npm:^0.2.5" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" + "@metamask-institutional/websocket-client": "npm:^0.2.6" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/9dbcf7c38a03becf61ab013f78df225da1f6de12976f328e7809c0edda5ab9e1aeee2b4d5b9430c15d5dc9f7040fa703c560c58073d601110895388c1c15d7a8 + checksum: 10/815c6faaaed9af25ed21d1339790e82622bef81f3c578269afde908dc95d36cc64a549c58164e24f20d9941e8c05e883d02c8886b741e50e3cf83960a8cb00d2 languageName: node linkType: hard -"@metamask-institutional/types@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask-institutional/types@npm:1.1.0" - checksum: 10/76f3c8529e4fe549bcabe60c39a66dd1a526aa7ea16fe7949e960a884d2c9e5e2e65db4d1123e23eeaae46f88b10aafe365cc693f5f632ef1a8e407373fe2fdf +"@metamask-institutional/types@npm:^1.2.0": + version: 1.2.0 + resolution: "@metamask-institutional/types@npm:1.2.0" + checksum: 10/3e28224c12f1ad955f114de919dbf4abbef19bd19cca3a4544898061d79518a94baa14121ebf6e5c6972dd6b1d1ec8071ebc50a77480ad944c26a2be53af5290 languageName: node linkType: hard -"@metamask-institutional/websocket-client@npm:^0.2.5": - version: 0.2.5 - resolution: "@metamask-institutional/websocket-client@npm:0.2.5" +"@metamask-institutional/websocket-client@npm:^0.2.6": + version: 0.2.6 + resolution: "@metamask-institutional/websocket-client@npm:0.2.6" dependencies: - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" mock-socket: "npm:^9.2.1" - checksum: 10/4743ccbb3a92a5b7ddccfd9f72741910bb93cc769023c8b9ee7944bb82f79938e45b10af5f7754b2898dc218c0e3874cb38aa628f96685fc69d956900723755d + checksum: 10/ba59b6d776fdc9d681ac0a294cd3eab961ba9d06d1ebd6a59fbe379cf640c421fdaaf53f6b6ab187ea3f1993b251292deb3c9d1fff8b6717fbd14f2512105190 languageName: node linkType: hard @@ -10699,12 +10699,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.11.17": - version: 20.12.7 - resolution: "@types/node@npm:20.12.7" +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.14.9": + version: 20.16.11 + resolution: "@types/node@npm:20.16.11" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/b4a28a3b593a9bdca5650880b6a9acef46911d58cf7cfa57268f048e9a7157a7c3196421b96cea576850ddb732e3b54bc982c8eb5e1e5ef0635d4424c2fce801 + undici-types: "npm:~6.19.2" + checksum: 10/6d2f92b7b320c32ba0c2bc54d21651bd21690998a2e27f00d15019d4db3e0ec30fce85332efed5e37d4cda078ff93ea86ee3e92b76b7a25a9b92a52a039b60b2 languageName: node linkType: hard @@ -26096,15 +26096,15 @@ __metadata: "@lgbot/madge": "npm:^6.2.0" "@lydell/node-pty": "npm:^1.0.1" "@material-ui/core": "npm:^4.11.0" - "@metamask-institutional/custody-controller": "npm:^0.2.31" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/extension": "npm:^0.3.27" - "@metamask-institutional/institutional-features": "npm:^1.3.5" + "@metamask-institutional/custody-controller": "npm:^0.3.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/extension": "npm:^0.3.28" + "@metamask-institutional/institutional-features": "npm:^1.3.6" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.1" "@metamask-institutional/rpc-allowlist": "npm:^1.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/transaction-update": "npm:^0.2.5" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/transaction-update": "npm:^0.2.6" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/abi-utils": "npm:^2.0.2" "@metamask/account-watcher": "npm:^4.1.1" "@metamask/accounts-controller": "npm:^18.2.2" @@ -35488,6 +35488,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "undici@npm:5.28.4": version: 5.28.4 resolution: "undici@npm:5.28.4" From bbba7c5c8e82150a08e2d1a07539112e5655212d Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Thu, 17 Oct 2024 12:55:06 +0200 Subject: [PATCH 22/51] fix: flaky tests `Add existing token using search renders the balance for the chosen token` (#27853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes a couple of race conditions on the test `Add existing token using search renders the balance for the chosen token` and the rest of the specs in the Add token test file. The changes have also uncovered a production bug in mmi build (see [here](https://github.com/MetaMask/metamask-extension/issues/27854)). So now, this PR is blocked until the fix for mmi is done. [Edit] The fix has now been merged here, so the PR is ready https://github.com/MetaMask/metamask-extension/pull/27855 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27853?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27703 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: sahar-fehri --- test/e2e/tests/tokens/add-hide-token.spec.js | 55 +++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index 535948ba1c9b..5eb60d3db17b 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -8,6 +8,7 @@ const { clickNestedButton, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); const { CHAIN_IDS } = require('../../../../shared/constants/network'); describe('Add hide token', function () { @@ -126,16 +127,16 @@ describe('Add existing token using search', function () { tag: 'p', }); await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement( + await driver.clickElementAndWaitToDisappear( '[data-testid="import-tokens-modal-import-button"]', ); await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); - const [, tkn] = await driver.findElements( - '[data-testid="multichain-token-list-button"]', - ); - await tkn.click(); + await driver.clickElement({ + tag: 'span', + text: 'Basic Attention Token', + }); await driver.waitForSelector({ css: '[data-testid="multichain-token-list-item-value"]', @@ -147,6 +148,8 @@ describe('Add existing token using search', function () { }); describe('Add token using wallet_watchAsset', function () { + const smartContract = SMART_CONTRACTS.HST; + it('opens a notification that adds a token when wallet_watchAsset is executed, then approves', async function () { await withFixtures( { @@ -155,9 +158,13 @@ describe('Add token using wallet_watchAsset', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, + smartContract, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, contractRegistry }) => { + const contractAddress = await contractRegistry.getContractAddress( + smartContract, + ); await unlockWallet(driver); await driver.openNewPage('http://127.0.0.1:8080/'); @@ -168,7 +175,7 @@ describe('Add token using wallet_watchAsset', function () { params: { type: 'ERC20', options: { - address: '0x86002be4cdd922de1ccb831582bf99284b99ac12', + address: '${contractAddress}', symbol: 'TST', decimals: 4 }, @@ -176,19 +183,16 @@ describe('Add token using wallet_watchAsset', function () { }) `); - const windowHandles = await driver.waitUntilXWindowHandles(3); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ tag: 'button', text: 'Add token', }); - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.waitForSelector({ css: '[data-testid="multichain-token-list-item-value"]', @@ -206,9 +210,13 @@ describe('Add token using wallet_watchAsset', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, + smartContract, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, contractRegistry }) => { + const contractAddress = await contractRegistry.getContractAddress( + smartContract, + ); await unlockWallet(driver); await driver.openNewPage('http://127.0.0.1:8080/'); @@ -219,7 +227,7 @@ describe('Add token using wallet_watchAsset', function () { params: { type: 'ERC20', options: { - address: '0x86002be4cdd922de1ccb831582bf99284b99ac12', + address: '${contractAddress}', symbol: 'TST', decimals: 4 }, @@ -227,19 +235,16 @@ describe('Add token using wallet_watchAsset', function () { }) `); - const windowHandles = await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ tag: 'button', text: 'Cancel', }); - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); const assetListItems = await driver.findElements( '.multichain-token-list-item', From 043ea3fc29a4b3d943b69bf1e66b80662d8228e0 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 17 Oct 2024 13:45:55 +0200 Subject: [PATCH 23/51] chore: bump `@metamask/eth-snap-keyring` to version 4.4.0 (#27864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Bumping the Snap bridge. This new version will now sanitize the redirect URL passed by a Snap. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27864?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/585 ## **Manual testing steps** 1. `yarn start:flask` 2. Use the SSK Snap with the following branch: `test/keyring-snap-bridge-70` 3. Run the SSK dapp + Snap using `yarn start`, go to http://localhost:8000/ 4. Install the Snap 5. Make sure that "Use Synchronous Approval" on the SSK dapp is **disabled** 6. Create an SSK account 7. Go to https://metamask.github.io/test-dapp/ 8. Connect your SSK account 9. Use the personal sign test 10. You should see a sanitized URL `https://ioi.com?fake=1` on the Snap dialog ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-11 at 11 19 52](https://github.com/user-attachments/assets/60661c21-18cd-4570-b642-a47650258556) ### **After** ![Screenshot 2024-10-11 at 12 22 59](https://github.com/user-attachments/assets/819d3ef5-2b09-4b43-8118-0870ee695bff) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- package.json | 2 +- yarn.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 57dc9224b765..fca1780d3300 100644 --- a/package.json +++ b/package.json @@ -314,7 +314,7 @@ "@metamask/eth-ledger-bridge-keyring": "^3.0.1", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^7.0.1", - "@metamask/eth-snap-keyring": "^4.3.6", + "@metamask/eth-snap-keyring": "^4.4.0", "@metamask/eth-token-tracker": "^8.0.0", "@metamask/eth-trezor-keyring": "^3.1.3", "@metamask/etherscan-link": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index a0f9df5f4706..3c34b05fee52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5382,22 +5382,22 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6": - version: 4.3.6 - resolution: "@metamask/eth-snap-keyring@npm:4.3.6" +"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6, @metamask/eth-snap-keyring@npm:^4.4.0": + version: 4.4.0 + resolution: "@metamask/eth-snap-keyring@npm:4.4.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^7.0.3" - "@metamask/snaps-controllers": "npm:^9.7.0" - "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "npm:^7.8.1" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@types/uuid": "npm:^9.0.8" uuid: "npm:^9.0.1" peerDependencies: "@metamask/keyring-api": ^8.1.3 - checksum: 10/378dce125ba9e38b9ba7d9b7124383b4fd8d2782207dc69e1ae9e262beb83f22044eae5200986d4c353de29e5283c289e56b3acb88c8971a63f9365bdde3d5b4 + checksum: 10/fd9926ba3706506bd9a16d1c2501e7c6cd7b7e3e7ea332bc7f28e0fca1f67f4514da51e6f9f4541a7354a2363d04c09c445f61b98fdc366432e1def9c2f27d07 languageName: node linkType: hard @@ -6282,7 +6282,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.11.1, @metamask/snaps-controllers@npm:^9.7.0": +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.11.1": version: 9.11.1 resolution: "@metamask/snaps-controllers@npm:9.11.1" dependencies: @@ -6379,7 +6379,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1": +"@metamask/snaps-utils@npm:^7.4.0": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" dependencies: @@ -6410,7 +6410,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1": +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1": version: 8.4.1 resolution: "@metamask/snaps-utils@npm:8.4.1" dependencies: @@ -26134,7 +26134,7 @@ __metadata: "@metamask/eth-ledger-bridge-keyring": "npm:^3.0.1" "@metamask/eth-query": "npm:^4.0.0" "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/eth-snap-keyring": "npm:^4.3.6" + "@metamask/eth-snap-keyring": "npm:^4.4.0" "@metamask/eth-token-tracker": "npm:^8.0.0" "@metamask/eth-trezor-keyring": "npm:^3.1.3" "@metamask/etherscan-link": "npm:^3.0.0" From f58d598d221c4ea75c15f649ca542ddf6f16d911 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:12:29 +0200 Subject: [PATCH 24/51] fix: flaky test `Vault Decryptor Page is able to decrypt the vault pasting the text in the vault-decryptor webapp` (#27921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The problem is that we are clicking an element when it's moving, making the click take no effect. In the onboarding steps, there is a carousel, where you can move from one page to the other. The issue is that the classical methods findElement or clickElement, can take place without having the correct page fully visible (while it's moving), making the click, take no effect. To avoid our clicks taking no effect, we need to add a new method to wait until an element is not moving. That's the condition we need before clicking that element. Note. adding this by default to the clickElement can be an overkill as the check will delay all instances of clickElement. Since this only happens in the onboarding, I decided to **not** add it by default in the clickEelemtn for this reason. Only in the onboarding flow, we need to make sure we wait for elements not moving. ![Screenshot from 2024-10-17 08-48-13](https://github.com/user-attachments/assets/3b4df3b3-8d3b-49b7-8de7-34082410cfdd) ![image](https://github.com/user-attachments/assets/dbd1d928-3c32-42c4-8e2a-006f1345a54e) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27921?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27922 ## **Manual testing steps** 1. Check ci run here https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/106117/workflows/e0bb3426-1c21-481b-8c0b-4a417f149172/jobs/3963202 ## **Screenshots/Recordings** https://github.com/user-attachments/assets/1d00a2b6-3b03-482f-a6c1-c14fb7e5c318 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/helpers.js | 23 +++++++++-- test/e2e/tests/onboarding/onboarding.spec.js | 41 +++++++++++-------- .../tests/privacy/basic-functionality.spec.js | 30 +++++++++++--- test/e2e/webdriver/driver.js | 40 ++++++++++++++++++ 4 files changed, 108 insertions(+), 26 deletions(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 564f99f2cde6..c857838f0810 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -535,7 +535,10 @@ const onboardingRevealAndConfirmSRP = async (driver) => { await driver.clickElement('[data-testid="confirm-recovery-phrase"]'); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Confirm', + }); }; /** @@ -566,7 +569,7 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); // opt-out from third party API on general section - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Manage default privacy settings', tag: 'button', }); @@ -575,7 +578,10 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { '[data-testid="basic-functionality-toggle"] .toggle-button', ); await driver.clickElement('[id="basic-configuration-checkbox"]'); - await driver.clickElement({ text: 'Turn off', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Turn off', + }); // opt-out from third party API on assets section await driver.clickElement('[data-testid="category-back-button"]'); @@ -588,10 +594,19 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { ).map((toggle) => toggle.click()), ); await driver.clickElement('[data-testid="category-back-button"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement('[data-testid="privacy-settings-back-button"]'); // complete onboarding - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Done', + }); await onboardingPinExtension(driver); }; diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 8d6b00de07ed..b5e273b7e978 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -328,28 +328,24 @@ describe('MetaMask onboarding @no-mmi', function () { tag: 'button', }); - await driver.clickElement({ text: 'Save', tag: 'button' }); - - await driver.delay(largeDelayMs); - await driver.waitForSelector('[data-testid="category-back-button"]'); - const generalBackButton = await driver.waitForSelector( - '[data-testid="category-back-button"]', - ); - await generalBackButton.click(); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Save', + }); - await driver.delay(largeDelayMs); + await driver.clickElement('[data-testid="category-back-button"]'); - await driver.waitForSelector( + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( '[data-testid="privacy-settings-back-button"]', ); - const defaultSettingsBackButton = await driver.findElement( + + await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await defaultSettingsBackButton.click(); - - await driver.delay(largeDelayMs); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -359,9 +355,14 @@ describe('MetaMask onboarding @no-mmi', function () { tag: 'button', }); - await driver.delay(largeDelayMs); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving({ + text: 'Done', + tag: 'button', + }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -412,6 +413,12 @@ describe('MetaMask onboarding @no-mmi', function () { await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); await driver.clickElement('[data-testid="category-back-button"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index b4fc0e138104..6ae14ca660be 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -81,15 +81,35 @@ describe('MetaMask onboarding @no-mmi', function () { '[data-testid="currency-rate-check-toggle"] .toggle-button', ); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.delay(regularDelayMs); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Done', tag: 'button' }); - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving({ + text: 'Done', + tag: 'button', + }); + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); await driver.clickElement('[data-testid="network-display"]'); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index b0648f122fb9..813d00d5e0e8 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -597,6 +597,46 @@ class Driver { } } + /** + * Checks if an element is moving by comparing its position at two different times. + * + * @param {string | object} rawLocator - Element locator. + * @returns {Promise} Promise that resolves to a boolean indicating if the element is moving. + */ + async isElementMoving(rawLocator) { + const element = await this.findElement(rawLocator); + const initialPosition = await element.getRect(); + + await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for a short period + + const newPosition = await element.getRect(); + + return ( + initialPosition.x !== newPosition.x || initialPosition.y !== newPosition.y + ); + } + + /** + * Waits until an element stops moving within a specified timeout period. + * + * @param {string | object} rawLocator - Element locator. + * @param {number} timeout - The maximum time to wait for the element to stop moving. + * @returns {Promise} Promise that resolves when the element stops moving. + * @throws {Error} Throws an error if the element does not stop moving within the timeout period. + */ + async waitForElementToStopMoving(rawLocator, timeout = 5000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + if (!(await this.isElementMoving(rawLocator))) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 500)); // Check every 500ms + } + + throw new Error('Element did not stop moving within the timeout period'); + } + /** @param {string} title - The title of the window or tab the screenshot is being taken in */ async takeScreenshot(title) { const filepathBase = `${artifactDir(title)}/test-screenshot`; From dc48117984e59f2bcf507dda48675bf9c4da9419 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 17 Oct 2024 13:57:29 +0100 Subject: [PATCH 25/51] feat: Add transaction flow and details sections (#27654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds two new sections for the wallet initiated ERC20 token transfer redesigned confirmation. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27654?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3220 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-10-07 at 11 04 58 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + .../legacy/watch-asset-confirmation.ts | 20 ++ .../redesign}/confirmation.ts | 4 +- ...proval-for-all-transaction-confirmation.ts | 6 +- .../redesign/token-transfer-confirmation.ts | 45 +++ .../redesign/transaction-confirmation.ts | 25 ++ .../pages/send/send-token-page.ts | 16 + test/e2e/page-objects/pages/test-dapp.ts | 15 +- .../pages/transaction-confirmation.ts | 5 - ...55-revoke-set-approval-for-all-redesign.ts | 2 +- ...1155-set-approval-for-all-redesign.spec.ts | 2 +- .../erc20-token-send-redesign.spec.ts | 115 +++++++ ...21-revoke-set-approval-for-all-redesign.ts | 2 +- ...c721-set-approval-for-all-redesign.spec.ts | 2 +- .../confirm/info/approve/approve.tsx | 8 +- .../base-transaction-info.tsx | 8 +- .../info/hooks/use-token-values.test.ts | 148 +++++---- .../confirm/info/hooks/use-token-values.ts | 87 +++-- .../set-approval-for-all-info.tsx | 8 +- .../advanced-details.test.tsx.snap | 23 +- .../advanced-details.test.tsx | 45 ++- .../advanced-details/advanced-details.tsx | 15 +- .../__snapshots__/send-heading.test.tsx.snap | 49 ++- .../send-heading/send-heading.stories.tsx | 4 +- .../info/shared/send-heading/send-heading.tsx | 17 +- .../token-details-section.test.tsx.snap | 118 +++++++ .../token-transfer.test.tsx.snap | 309 +++++++++++++++++- .../transaction-flow-section.test.tsx.snap | 53 +++ .../token-details-section.test.tsx | 26 ++ .../token-transfer/token-details-section.tsx | 76 +++++ .../token-transfer/token-transfer.stories.tsx | 18 +- .../token-transfer/token-transfer.test.tsx | 8 + .../info/token-transfer/token-transfer.tsx | 14 +- .../transaction-flow-section.test.tsx | 48 +++ .../transaction-flow-section.tsx | 61 ++++ .../alerts/useConfirmationOriginAlerts.ts | 2 +- 36 files changed, 1200 insertions(+), 207 deletions(-) create mode 100644 test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts rename test/e2e/page-objects/pages/{ => confirmations/redesign}/confirmation.ts (85%) rename test/e2e/page-objects/pages/{ => confirmations/redesign}/set-approval-for-all-transaction-confirmation.ts (89%) create mode 100644 test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts create mode 100644 test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts delete mode 100644 test/e2e/page-objects/pages/transaction-confirmation.ts create mode 100644 test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index bb10d6f579a0..6834ba7169c7 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6179,6 +6179,9 @@ "transactionFee": { "message": "Transaction fee" }, + "transactionFlowNetwork": { + "message": "Network" + }, "transactionHistoryBaseFee": { "message": "Base fee (GWEI)" }, diff --git a/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts b/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts new file mode 100644 index 000000000000..23f1b010de87 --- /dev/null +++ b/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts @@ -0,0 +1,20 @@ +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; + +class WatchAssetConfirmation { + private driver: Driver; + + private footerConfirmButton: RawLocator; + + constructor(driver: Driver) { + this.driver = driver; + + this.footerConfirmButton = '[data-testid="page-container-footer-next"]'; + } + + async clickFooterConfirmButton() { + await this.driver.clickElement(this.footerConfirmButton); + } +} + +export default WatchAssetConfirmation; diff --git a/test/e2e/page-objects/pages/confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts similarity index 85% rename from test/e2e/page-objects/pages/confirmation.ts rename to test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts index 3ec372cb3163..f8fc66c3fc65 100644 --- a/test/e2e/page-objects/pages/confirmation.ts +++ b/test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts @@ -1,5 +1,5 @@ -import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; class Confirmation { protected driver: Driver; diff --git a/test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts similarity index 89% rename from test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts rename to test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts index 5259e0a51dcd..a1aadeff3376 100644 --- a/test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts +++ b/test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts @@ -1,6 +1,6 @@ -import { tEn } from '../../../lib/i18n-helpers'; -import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { tEn } from '../../../../../lib/i18n-helpers'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; import TransactionConfirmation from './transaction-confirmation'; class SetApprovalForAllTransactionConfirmation extends TransactionConfirmation { diff --git a/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts new file mode 100644 index 000000000000..837c7aa24e21 --- /dev/null +++ b/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts @@ -0,0 +1,45 @@ +import { tEn } from '../../../../../lib/i18n-helpers'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; +import TransactionConfirmation from './transaction-confirmation'; + +class TokenTransferTransactionConfirmation extends TransactionConfirmation { + private networkParagraph: RawLocator; + + private interactingWithParagraph: RawLocator; + + private networkFeeParagraph: RawLocator; + + constructor(driver: Driver) { + super(driver); + + this.driver = driver; + + this.networkParagraph = { + css: 'p', + text: tEn('transactionFlowNetwork') as string, + }; + this.interactingWithParagraph = { + css: 'p', + text: tEn('interactingWith') as string, + }; + this.networkFeeParagraph = { + css: 'p', + text: tEn('networkFee') as string, + }; + } + + async check_networkParagraph() { + await this.driver.waitForSelector(this.networkParagraph); + } + + async check_interactingWithParagraph() { + await this.driver.waitForSelector(this.interactingWithParagraph); + } + + async check_networkFeeParagraph() { + await this.driver.waitForSelector(this.networkFeeParagraph); + } +} + +export default TokenTransferTransactionConfirmation; diff --git a/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts new file mode 100644 index 000000000000..661feef33197 --- /dev/null +++ b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts @@ -0,0 +1,25 @@ +import { tEn } from '../../../../../lib/i18n-helpers'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; +import Confirmation from './confirmation'; + +class TransactionConfirmation extends Confirmation { + private walletInitiatedHeadingTitle: RawLocator; + + constructor(driver: Driver) { + super(driver); + + this.driver = driver; + + this.walletInitiatedHeadingTitle = { + css: 'h3', + text: tEn('review') as string, + }; + } + + async check_walletInitiatedHeadingTitle() { + await this.driver.waitForSelector(this.walletInitiatedHeadingTitle); + } +} + +export default TransactionConfirmation; diff --git a/test/e2e/page-objects/pages/send/send-token-page.ts b/test/e2e/page-objects/pages/send/send-token-page.ts index 65727b106783..728afbfdd4df 100644 --- a/test/e2e/page-objects/pages/send/send-token-page.ts +++ b/test/e2e/page-objects/pages/send/send-token-page.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; import { Driver } from '../../../webdriver/driver'; +import { RawLocator } from '../../common'; class SendTokenPage { private driver: Driver; @@ -18,6 +19,10 @@ class SendTokenPage { private ensResolvedAddress: string; + private assetPickerButton: RawLocator; + + private tokenListButton: RawLocator; + constructor(driver: Driver) { this.driver = driver; this.inputAmount = '[data-testid="currency-input"]'; @@ -32,6 +37,8 @@ class SendTokenPage { text: 'Continue', tag: 'button', }; + this.assetPickerButton = '[data-testid="asset-picker-button"]'; + this.tokenListButton = '[data-testid="multichain-token-list-button"]'; } async check_pageIsLoaded(): Promise { @@ -125,6 +132,15 @@ class SendTokenPage { `ENS domain '${ensDomain}' resolved to address '${address}' and can be used as recipient on send token screen.`, ); } + + async click_assetPickerButton() { + await this.driver.clickElement(this.assetPickerButton); + } + + async click_secondTokenListButton() { + const elements = await this.driver.findElements(this.tokenListButton); + await elements[1].click(); + } } export default SendTokenPage; diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index ffb1f9033bdb..c0f71f1b3280 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -1,5 +1,6 @@ -import { Driver } from '../../webdriver/driver'; import { WINDOW_TITLES } from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import { RawLocator } from '../common'; const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; @@ -83,8 +84,16 @@ class TestDapp { private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult'; + private erc20WatchAssetButton: RawLocator; + constructor(driver: Driver) { this.driver = driver; + + this.erc721SetApprovalForAllButton = '#setApprovalForAllButton'; + this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; + this.erc721RevokeSetApprovalForAllButton = '#revokeButton'; + this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; + this.erc20WatchAssetButton = '#watchAssets'; } async check_pageIsLoaded(): Promise { @@ -143,6 +152,10 @@ class TestDapp { await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton); } + public async clickERC20WatchAssetButton() { + await this.driver.clickElement(this.erc20WatchAssetButton); + } + /** * Verify the failed personal sign signature. * diff --git a/test/e2e/page-objects/pages/transaction-confirmation.ts b/test/e2e/page-objects/pages/transaction-confirmation.ts deleted file mode 100644 index 7ae98d74d4c8..000000000000 --- a/test/e2e/page-objects/pages/transaction-confirmation.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Confirmation from './confirmation'; - -class TransactionConfirmation extends Confirmation {} - -export default TransactionConfirmation; diff --git a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts index 3e75adb34db8..1f9d05cd26a8 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts @@ -3,7 +3,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL } from '../../../constants'; import { unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts index 0e1134737c87..c9c9fdbd5eda 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts @@ -2,7 +2,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL, unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; diff --git a/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts new file mode 100644 index 000000000000..83892b1ca6e1 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { DAPP_URL } from '../../../constants'; +import { unlockWallet, WINDOW_TITLES } from '../../../helpers'; +import { Mockttp } from '../../../mock-e2e'; +import WatchAssetConfirmation from '../../../page-objects/pages/confirmations/legacy/watch-asset-confirmation'; +import TokenTransferTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/token-transfer-confirmation'; +import HomePage from '../../../page-objects/pages/homepage'; +import SendTokenPage from '../../../page-objects/pages/send/send-token-page'; +import TestDapp from '../../../page-objects/pages/test-dapp'; +import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from '../helpers'; +import { TestSuiteArguments } from './shared'; + +const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); + +describe('Confirmation Redesign ERC20 Token Send @no-mmi', function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createTransactionAndAssertDetails(driver, contractRegistry); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createTransactionAndAssertDetails(driver, contractRegistry); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); +}); + +async function mocks(server: Mockttp) { + return [await mockedSourcifyTokenSend(server)]; +} + +export async function mockedSourcifyTokenSend(mockServer: Mockttp) { + return await mockServer + .forGet('https://www.4byte.directory/api/v1/signatures/') + .withQuery({ hex_signature: '0xa9059cbb' }) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + count: 1, + next: null, + previous: null, + results: [ + { + bytes_signature: '©\u0005œ»', + created_at: '2016-07-09T03:58:28.234977Z', + hex_signature: '0xa9059cbb', + id: 145, + text_signature: 'transfer(address,uint256)', + }, + ], + }, + })); +} + +async function createTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.HST); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + + await testDapp.clickERC20WatchAssetButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + const watchAssetConfirmation = new WatchAssetConfirmation(driver); + await watchAssetConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + const homePage = new HomePage(driver); + await homePage.startSendFlow(); + + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.fillRecipient('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); + await sendToPage.fillAmount('1'); + + await sendToPage.click_assetPickerButton(); + await sendToPage.click_secondTokenListButton(); + await sendToPage.goToNextScreen(); + + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_walletInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_interactingWithParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts index 138695904e55..b0f1291a47d9 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts @@ -3,7 +3,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL } from '../../../constants'; import { unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts index 589670212be1..9e481ee9c75f 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts @@ -2,7 +2,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL, unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index eabf8639ccfb..fed03a75e17e 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -3,10 +3,8 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; import { useConfirmContext } from '../../../../context/confirm'; import { useAssetDetails } from '../../../../hooks/useAssetDetails'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; @@ -24,10 +22,6 @@ const ApproveInfo = () => { currentConfirmation: TransactionMeta; }; - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - const { isNFT } = useIsNFT(transactionMeta); const [isOpenEditSpendingCapModal, setIsOpenEditSpendingCapModal] = @@ -70,7 +64,7 @@ const ApproveInfo = () => { /> )} - {showAdvancedDetails && } + { const { currentConfirmation: transactionMeta } = useConfirmContext(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - if (!transactionMeta?.txParams) { return null; } @@ -33,7 +27,7 @@ const BaseTransactionInfo = () => { - {showAdvancedDetails && } + ); }; diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts index 7ac4aa5b5c92..1ed5e9c249ff 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts @@ -1,120 +1,126 @@ import { TransactionMeta } from '@metamask/transaction-controller'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; import mockState from '../../../../../../../test/data/mock-state.json'; -import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; -// import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; -import { Numeric } from '../../../../../../../shared/modules/Numeric'; +import { renderHookWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; -import { useTokenTracker } from '../../../../../../hooks/useTokenTracker'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; import { useTokenValues } from './use-token-values'; +import { useDecodedTransactionData } from './useDecodedTransactionData'; + +jest.mock('../../../../hooks/useAssetDetails', () => ({ + ...jest.requireActual('../../../../hooks/useAssetDetails'), + useAssetDetails: jest.fn(), +})); + +jest.mock('./useDecodedTransactionData', () => ({ + ...jest.requireActual('./useDecodedTransactionData'), + useDecodedTransactionData: jest.fn(), +})); jest.mock( '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate', () => jest.fn(), ); -jest.mock('../../../../../../hooks/useTokenTracker', () => ({ - ...jest.requireActual('../../../../../../hooks/useTokenTracker'), - useTokenTracker: jest.fn(), -})); - describe('useTokenValues', () => { + const useAssetDetailsMock = jest.mocked(useAssetDetails); + const useDecodedTransactionDataMock = jest.mocked(useDecodedTransactionData); const useTokenExchangeRateMock = jest.mocked(useTokenExchangeRate); - const useTokenTrackerMock = jest.mocked(useTokenTracker); - const TEST_SELECTED_TOKEN = { - address: 'address', - decimals: 18, - symbol: 'symbol', - iconUrl: 'iconUrl', - image: 'image', - }; - - it('returns native and fiat balances', async () => { - (useTokenTrackerMock as jest.Mock).mockResolvedValue({ - tokensWithBalances: [ - { - address: '0x076146c765189d51be3160a2140cf80bfc73ad68', - balance: '1000000000000000000', - decimals: 18, - }, - ], - }); - - (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( - new Numeric(1, 10), - ); - - const transactionMeta = genUnapprovedTokenTransferConfirmation( - {}, - ) as TransactionMeta; - - const { result, waitForNextUpdate } = renderHookWithProvider( - () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), - mockState, - ); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - fiatDisplayValue: '$1.00', - tokenBalance: '1', - }); + beforeEach(() => { + jest.resetAllMocks(); }); - it('returns undefined native and fiat balances if no token with balances is returned', async () => { - (useTokenTrackerMock as jest.Mock).mockResolvedValue({ - tokensWithBalances: [], - }); - + it('returns native and fiat balances', async () => { + (useAssetDetailsMock as jest.Mock).mockImplementation(() => ({ + decimals: '10', + })); + (useDecodedTransactionDataMock as jest.Mock).mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: 'transfer', + params: [ + { + type: 'address', + value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', + }, + { + type: 'uint256', + value: 70000000000, + }, + ], + }, + ], + source: 'FourByte', + }, + })); (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( - new Numeric(1, 10), + new Numeric(0.91, 10), ); const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, ) as TransactionMeta; - const { result, waitForNextUpdate } = renderHookWithProvider( - () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + const { result, waitForNextUpdate } = renderHookWithConfirmContextProvider( + () => useTokenValues(transactionMeta), mockState, ); await waitForNextUpdate(); expect(result.current).toEqual({ - fiatDisplayValue: undefined, - tokenBalance: undefined, + decodedTransferValue: 7, + fiatDisplayValue: '$6.37', + pending: false, }); }); it('returns undefined fiat balance if no token rate is returned', async () => { - (useTokenTrackerMock as jest.Mock).mockResolvedValue({ - tokensWithBalances: [ - { - address: '0x076146c765189d51be3160a2140cf80bfc73ad68', - balance: '1000000000000000000', - decimals: 18, - }, - ], - }); - + (useAssetDetailsMock as jest.Mock).mockImplementation(() => ({ + decimals: '10', + })); + (useDecodedTransactionDataMock as jest.Mock).mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: 'transfer', + params: [ + { + type: 'address', + value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', + }, + { + type: 'uint256', + value: 70000000000, + }, + ], + }, + ], + source: 'FourByte', + }, + })); (useTokenExchangeRateMock as jest.Mock).mockResolvedValue(null); const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, ) as TransactionMeta; - const { result, waitForNextUpdate } = renderHookWithProvider( - () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + const { result, waitForNextUpdate } = renderHookWithConfirmContextProvider( + () => useTokenValues(transactionMeta), mockState, ); await waitForNextUpdate(); expect(result.current).toEqual({ + decodedTransferValue: 7, fiatDisplayValue: null, - tokenBalance: '1', + pending: false, }); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts index 9515a45515bf..139a1e8116b9 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -1,38 +1,44 @@ import { TransactionMeta } from '@metamask/transaction-controller'; +import { isHexString } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; +import { isBoolean } from 'lodash'; import { useMemo, useState } from 'react'; -import { calcTokenAmount } from '../../../../../../../shared/lib/transactions-controller-utils'; -import { toChecksumHexAddress } from '../../../../../../../shared/modules/hexstring-utils'; import { Numeric } from '../../../../../../../shared/modules/Numeric'; import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; -import { useTokenTracker } from '../../../../../../hooks/useTokenTracker'; -import { SelectedToken } from '../shared/selected-token'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; +import { useDecodedTransactionData } from './useDecodedTransactionData'; -export const useTokenValues = ( - transactionMeta: TransactionMeta, - selectedToken: SelectedToken, -) => { - const [tokensWithBalances, setTokensWithBalances] = useState< - { balance: string; address: string; decimals: number; string: string }[] - >([]); +export const useTokenValues = (transactionMeta: TransactionMeta) => { + const { decimals } = useAssetDetails( + transactionMeta.txParams.to, + transactionMeta.txParams.from, + transactionMeta.txParams.data, + ); - const fetchTokenBalances = async () => { - const result: { - tokensWithBalances: { - balance: string; - address: string; - decimals: number; - string: string; - }[]; - } = await useTokenTracker({ - tokens: [selectedToken], - address: undefined, - }); + const decodedResponse = useDecodedTransactionData(); + const { value, pending } = decodedResponse; - setTokensWithBalances(result.tokensWithBalances); - }; + const decodedTransferValue = useMemo(() => { + if (!value || !decimals) { + return 0; + } - fetchTokenBalances(); + const paramIndex = value.data[0].params.findIndex( + (param) => + param.value !== undefined && + !isHexString(param.value) && + param.value.length === undefined && + !isBoolean(param.value), + ); + if (paramIndex === -1) { + return 0; + } + + return new BigNumber(value.data[0].params[paramIndex].value.toString()) + .dividedBy(new BigNumber(10).pow(Number(decimals))) + .toNumber(); + }, [value, decimals]); const [exchangeRate, setExchangeRate] = useState(); const fetchExchangeRate = async () => { @@ -40,38 +46,19 @@ export const useTokenValues = ( setExchangeRate(result); }; - fetchExchangeRate(); - const tokenBalance = useMemo(() => { - const tokenWithBalance = tokensWithBalances.find( - (token: { - balance: string; - address: string; - decimals: number; - string: string; - }) => - toChecksumHexAddress(token.address) === - toChecksumHexAddress(transactionMeta?.txParams?.to as string), - ); - - if (!tokenWithBalance) { - return undefined; - } - - return calcTokenAmount(tokenWithBalance.balance, tokenWithBalance.decimals); - }, [tokensWithBalances]); - const fiatValue = - exchangeRate && tokenBalance && exchangeRate.times(tokenBalance).toNumber(); - + exchangeRate && + decodedTransferValue && + exchangeRate.times(decodedTransferValue, 10).toNumber(); const fiatFormatter = useFiatFormatter(); - const fiatDisplayValue = fiatValue && fiatFormatter(fiatValue, { shorten: true }); return { + decodedTransferValue, fiatDisplayValue, - tokenBalance: tokenBalance && String(tokenBalance.toNumber()), + pending, }; }; diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx index 92df913783a1..6902a6da9b1f 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx @@ -1,8 +1,6 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import React from 'react'; -import { useSelector } from 'react-redux'; import { useConfirmContext } from '../../../../context/confirm'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { ApproveDetails } from '../approve/approve-details/approve-details'; import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; @@ -16,10 +14,6 @@ const SetApprovalForAllInfo = () => { const { currentConfirmation: transactionMeta } = useConfirmContext(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - const decodedResponse = useDecodedTransactionData(); const { value, pending } = decodedResponse; @@ -45,7 +39,7 @@ const SetApprovalForAllInfo = () => { )} - {showAdvancedDetails && } + ); }; diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap index f66db615defe..3a93bae1e26d 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap @@ -1,6 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` does not render component for advanced transaction details 1`] = ` +exports[` does not render component when the state property is false 1`] = `
    `; + +exports[` renders component when the prop override is passed 1`] = `
    does not render component for advanced transaction

    does not render component for advanced transaction
    `; -exports[` renders component for advanced transaction details 1`] = ` +exports[` renders component when the state property is true 1`] = `
    renders component for advanced transaction details

    renders component for advanced transaction details class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - 12 + undefined

    -
    diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx index b965e2015895..60512441e77d 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.test.tsx @@ -9,8 +9,18 @@ import { AdvancedDetails } from './advanced-details'; describe('', () => { const middleware = [thunk]; - it('does not render component for advanced transaction details', () => { - const state = mockState; + it('does not render component when the state property is false', () => { + const state = { + ...mockState, + metamask: { + ...mockState.metamask, + preferences: { + ...mockState.metamask.preferences, + showConfirmationAdvancedDetails: false, + }, + }, + }; + const mockStore = configureMockStore(middleware)(state); const { container } = renderWithConfirmContextProvider( , @@ -20,16 +30,18 @@ describe('', () => { expect(container).toMatchSnapshot(); }); - it('renders component for advanced transaction details', () => { + it('renders component when the state property is true', () => { const state = { ...mockState, metamask: { ...mockState.metamask, - useNonceField: true, - nextNonce: 1, - customNonceValue: '12', + preferences: { + ...mockState.metamask.preferences, + showConfirmationAdvancedDetails: true, + }, }, }; + const mockStore = configureMockStore(middleware)(state); const { container } = renderWithConfirmContextProvider( , @@ -38,4 +50,25 @@ describe('', () => { expect(container).toMatchSnapshot(); }); + + it('renders component when the prop override is passed', () => { + const state = { + ...mockState, + metamask: { + ...mockState.metamask, + preferences: { + ...mockState.metamask.preferences, + showConfirmationAdvancedDetails: false, + }, + }, + }; + + const mockStore = configureMockStore(middleware)(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx index 7e0cee721bb8..ebb0f69d75c1 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx @@ -16,6 +16,7 @@ import { showModal, updateCustomNonce, } from '../../../../../../../store/actions'; +import { selectConfirmationAdvancedDetailsOpen } from '../../../../../selectors/preferences'; import { TransactionData } from '../transaction-data/transaction-data'; const NonceDetails = () => { @@ -65,7 +66,19 @@ const NonceDetails = () => { ); }; -export const AdvancedDetails: React.FC = () => { +export const AdvancedDetails = ({ + overrideVisibility = false, +}: { + overrideVisibility?: boolean; +}) => { + const showAdvancedDetails = useSelector( + selectConfirmationAdvancedDetailsOpen, + ); + + if (!overrideVisibility && !showAdvancedDetails) { + return null; + } + return ( <> diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap index e4222b56cbc5..677a5a357155 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap @@ -3,18 +3,47 @@ exports[` renders component 1`] = `
    -
    - ? -
    -

    - Unknown -

    + + + + + + + +
    `; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx index f4bfb484c107..8944a84b770e 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx @@ -13,7 +13,9 @@ const Story = { component: SendHeading, decorators: [ (story: () => Meta) => ( - {story()} + + {story()} + ), ], }; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx index d571c61ee93e..2806c33936c0 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -22,6 +22,7 @@ import { MultichainState } from '../../../../../../../selectors/multichain'; import { useConfirmContext } from '../../../../../context/confirm'; import { useTokenImage } from '../../hooks/use-token-image'; import { useTokenValues } from '../../hooks/use-token-values'; +import { ConfirmLoader } from '../confirm-loader/confirm-loader'; const SendHeading = () => { const t = useI18nContext(); @@ -31,10 +32,8 @@ const SendHeading = () => { getWatchedToken(transactionMeta)(state), ); const { tokenImage } = useTokenImage(transactionMeta, selectedToken); - const { tokenBalance, fiatDisplayValue } = useTokenValues( - transactionMeta, - selectedToken, - ); + const { decodedTransferValue, fiatDisplayValue, pending } = + useTokenValues(transactionMeta); const TokenImage = ( { variant={TextVariant.headingLg} color={TextColor.inherit} marginTop={3} - >{`${tokenBalance || ''} ${selectedToken?.symbol || t('unknown')}`} + >{`${decodedTransferValue || ''} ${ + selectedToken?.symbol || t('unknown') + }`} {fiatDisplayValue && ( {fiatDisplayValue} @@ -67,13 +68,17 @@ const SendHeading = () => { ); + if (pending) { + return ; + } + return ( {TokenImage} {TokenValue} diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap new file mode 100644 index 000000000000..c545cea1f66d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenDetailsSection renders correctly 1`] = ` +
    +
    +
    +
    +
    +

    + Network +

    +
    +
    +
    +
    + G +
    +

    + Goerli +

    +
    +
    +
    +
    +
    +

    + Interacting with +

    +
    +
    +
    +
    + +

    + 0x07614...3ad68 +

    +
    +
    +
    +
    +
    +`; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap index 63b44d50173d..c9813ea1470e 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap @@ -3,18 +3,313 @@ exports[`TokenTransferInfo renders correctly 1`] = `
    + + + + + + + + + +
    +
    + + + + + + + + + +
    +
    - ? +
    +
    +

    + Network +

    +
    +
    +
    +
    + G +
    +

    + Goerli +

    +
    -

    - Unknown -

    +
    +
    +

    + Interacting with +

    +
    +
    +
    +
    + +

    + 0x07614...3ad68 +

    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Network fee +

    +
    +
    + +
    +
    +
    +
    +
    +

    + 0.0001 ETH +

    +

    + $0.04 +

    + +
    +
    +
    +
    +
    +

    + Speed +

    +
    +
    +
    +
    +

    + 🦊 Market +

    +

    + + ~ + 0 sec + +

    +
    +
    +
    `; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap new file mode 100644 index 000000000000..23cddb2b59b2 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +
    +
    +
    +
    +
    + +

    + 0x2e0D7...5d09B +

    +
    +
    + +
    +
    + +

    + 0x6B175...71d0F +

    +
    +
    +
    +
    +
    +`; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx new file mode 100644 index 000000000000..4188ea62bc84 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import { TokenDetailsSection } from './token-details-section'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +describe('TokenDetailsSection', () => { + it('renders correctly', () => { + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore([])(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx new file mode 100644 index 000000000000..48a5f2dad74c --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx @@ -0,0 +1,76 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../shared/constants/network'; +import { + ConfirmInfoRow, + ConfirmInfoRowAddress, +} from '../../../../../../components/app/confirm/info/row'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { + AvatarNetwork, + AvatarNetworkSize, + Box, + Text, +} from '../../../../../../components/component-library'; +import { + AlignItems, + BlockSize, + BorderColor, + Display, + FlexWrap, + TextColor, + TextVariant, +} from '../../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../../hooks/useI18nContext'; +import { getNetworkConfigurationsByChainId } from '../../../../../../selectors'; +import { useConfirmContext } from '../../../../context/confirm'; + +export const TokenDetailsSection = () => { + const t = useI18nContext(); + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const { chainId } = transactionMeta; + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const networkName = networkConfigurations[chainId].name; + + const networkRow = ( + + + + + {networkName} + + + + ); + + const tokenRow = ( + + + + ); + + return ( + + {networkRow} + {tokenRow} + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx index 384a8f161e9b..1cb5f3b40ab2 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx @@ -1,6 +1,13 @@ import React from 'react'; import { Provider } from 'react-redux'; import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { Box } from '../../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, +} from '../../../../../../helpers/constants/design-system'; import configureStore from '../../../../../../store/store'; import { ConfirmContextProvider } from '../../../../context/confirm'; import TokenTransferInfo from './token-transfer'; @@ -13,7 +20,16 @@ const Story = { decorators: [ (story: () => any) => ( - {story()} + + + {story()} + + ), ], diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx index 186505ee7740..01efc5db0005 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx @@ -13,6 +13,14 @@ jest.mock( }), ); +jest.mock('../../../../../../store/actions', () => ({ + ...jest.requireActual('../../../../../../store/actions'), + getGasFeeTimeEstimate: jest.fn().mockResolvedValue({ + lowerTimeBound: 0, + upperTimeBound: 60000, + }), +})); + describe('TokenTransferInfo', () => { it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx index 6fe5ecf166b2..9c0dfe81f536 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx @@ -1,8 +1,20 @@ import React from 'react'; +import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import SendHeading from '../shared/send-heading/send-heading'; +import { TokenDetailsSection } from './token-details-section'; +import { TransactionFlowSection } from './transaction-flow-section'; const TokenTransferInfo = () => { - return ; + return ( + <> + + + + + + + ); }; export default TokenTransferInfo; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx new file mode 100644 index 000000000000..c23d3645abd3 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx @@ -0,0 +1,48 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; +import { TransactionFlowSection } from './transaction-flow-section'; + +jest.mock('../hooks/useDecodedTransactionData', () => ({ + ...jest.requireActual('../hooks/useDecodedTransactionData'), + useDecodedTransactionData: jest.fn(), +})); + +describe('', () => { + const useDecodedTransactionDataMock = jest.fn().mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: TransactionType.tokenMethodTransfer, + params: [ + { + name: 'dst', + type: 'address', + value: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + }, + { name: 'wad', type: 'uint256', value: 0 }, + ], + }, + ], + source: 'Sourcify', + }, + })); + + (useDecodedTransactionData as jest.Mock).mockImplementation( + useDecodedTransactionDataMock, + ); + + it('renders correctly', () => { + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore([])(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx new file mode 100644 index 000000000000..de0e928c10f8 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx @@ -0,0 +1,61 @@ +import { NameType } from '@metamask/name-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import Name from '../../../../../../components/app/name'; +import { + Box, + Icon, + IconName, + IconSize, +} from '../../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + IconColor, + JustifyContent, +} from '../../../../../../helpers/constants/design-system'; +import { useConfirmContext } from '../../../../context/confirm'; +import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; + +export const TransactionFlowSection = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const { value, pending } = useDecodedTransactionData(); + + const recipientAddress = value?.data[0].params.find( + (param) => param.type === 'address', + )?.value; + + if (pending) { + return ; + } + + return ( + + + + + {recipientAddress && ( + + )} + + + ); +}; diff --git a/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts b/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts index f84ba991f071..2f2af8ddf0de 100644 --- a/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts @@ -20,7 +20,7 @@ const useConfirmationOriginAlerts = (): Alert[] => { : (currentConfirmation as TransactionMeta)?.origin; const originUndefinedOrValid = - origin === undefined || isValidASCIIURL(origin); + origin === undefined || origin === 'metamask' || isValidASCIIURL(origin); return useMemo((): Alert[] => { if (originUndefinedOrValid) { From 93fbaaf3604c1455339a1f59d794f905ff76d163 Mon Sep 17 00:00:00 2001 From: Derek Brans Date: Thu, 17 Oct 2024 09:04:31 -0400 Subject: [PATCH 26/51] feat(TXL-435): turn smart transactions on by default for new users (#27885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR: * enables smart transactions by default for new users * prevents the smart transaction opt-in modal from appearing To enable stx by default, we needed to replace the `getSmartTransactionsOptInStatus` selector with two distinct selectors: - `getSmartTransactionsOptInStatusForMetrics` - `getSmartTransactionsPreferenceEnabled` Previously, the `getSmartTransactionsOptInStatus` selector was doing double duty for the user's opt-in status and deciding whether the preference was enabled. Since the feature was disabled by default, and a user that has not opted-in or out of smart transactions has an opt-in status of null, this did not pose a problem. However, since we decided to enable smart transactions by default, we needed a separate selector for checking the preference for deciding to enable stx. ### Why not keep the `getSmartTransactionsOptInStatus` selector as-is and just add a new one? We considered keeping the `getSmartTransactionsOptInStatus` selector and adding a new one. However, by renaming it to `getSmartTransactionsOptInStatusForMetrics`, we avoid any confusion and make it clear that it is used for metrics collection and not for user preference handling. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27885?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TXL-435 ## **Manual testing steps** 1. add an account with funds on mainnet 2. opt-in modal does not display 3. transfer 0ETH to yourself uses smart transaction 4. preferences toggle starts enabled 5. set preferences toggle off 6. transfer 0ETH to yourself uses regular transaction ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 4 +- package.json | 1 + shared/modules/selectors/index.test.ts | 466 +++++++++++------- .../modules/selectors/smart-transactions.ts | 70 ++- ui/ducks/swaps/swaps.js | 19 +- .../confirm-transaction-base.component.js | 9 +- .../confirm-transaction-base.container.js | 7 +- ui/pages/home/home.container.js | 7 +- ui/pages/routes/routes.container.js | 2 - .../advanced-tab/advanced-tab.component.js | 10 +- .../advanced-tab.component.test.js | 6 +- .../advanced-tab/advanced-tab.container.js | 10 +- .../smart-transactions-opt-in-modal.test.tsx | 14 +- .../smart-transactions-opt-in-modal.tsx | 6 +- .../awaiting-signatures.js | 4 +- ui/pages/swaps/awaiting-swap/awaiting-swap.js | 4 +- ui/pages/swaps/index.js | 4 +- .../loading-swaps-quotes.js | 4 +- .../prepare-swap-page/prepare-swap-page.js | 8 +- .../swaps/prepare-swap-page/review-quote.js | 19 +- .../smart-transaction-status.js | 7 +- ui/store/actions.ts | 9 +- yarn.lock | 1 + 23 files changed, 426 insertions(+), 265 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 176c7aea10e5..142ae80d09c1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -221,9 +221,9 @@ import { getIsSmartTransaction, isHardwareWallet, getFeatureFlagsByChainId, - getSmartTransactionsOptInStatus, getCurrentChainSupportsSmartTransactions, getHardwareWalletType, + getSmartTransactionsPreferenceEnabled, } from '../../shared/modules/selectors'; import { createCaipStream } from '../../shared/modules/caip-stream'; import { BaseUrl } from '../../shared/constants/urls'; @@ -1904,7 +1904,7 @@ export default class MetamaskController extends EventEmitter { isResubmitEnabled: () => { const state = this._getMetaMaskState(); return !( - getSmartTransactionsOptInStatus(state) && + getSmartTransactionsPreferenceEnabled(state) && getCurrentChainSupportsSmartTransactions(state) ); }, diff --git a/package.json b/package.json index fca1780d3300..5709ea91a51c 100644 --- a/package.json +++ b/package.json @@ -460,6 +460,7 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", "@babel/register": "^7.22.15", + "@jest/globals": "^29.7.0", "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/lavadome-core": "0.0.10", "@lavamoat/lavapack": "^6.1.0", diff --git a/shared/modules/selectors/index.test.ts b/shared/modules/selectors/index.test.ts index f1dc4fee5ec2..9f0b1b201a5c 100644 --- a/shared/modules/selectors/index.test.ts +++ b/shared/modules/selectors/index.test.ts @@ -1,12 +1,16 @@ +// Mocha type definitions are conflicting with Jest +import { it as jestIt } from '@jest/globals'; + import { createSwapsMockStore } from '../../../test/jest'; import { CHAIN_IDS } from '../../constants/network'; import { mockNetworkState } from '../../../test/stub/networks'; import { - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, getCurrentChainSupportsSmartTransactions, getSmartTransactionsEnabled, getIsSmartTransaction, getIsSmartTransactionsOptInModalAvailable, + getSmartTransactionsPreferenceEnabled, } from '.'; describe('Selectors', () => { @@ -65,115 +69,190 @@ describe('Selectors', () => { }; }; - describe('getSmartTransactionsOptInStatus', () => { - it('should return the smart transactions opt-in status', () => { - const state = createMockState(); - const result = getSmartTransactionsOptInStatus(state); - expect(result).toBe(true); - }); - }); + describe('getSmartTransactionsOptInStatusForMetrics and getSmartTransactionsPreferenceEnabled', () => { + const createMockOptInStatusState = (status: boolean | null) => { + return { + metamask: { + preferences: { + smartTransactionsOptInStatus: status, + }, + }, + }; + }; + describe('getSmartTransactionsOptInStatusForMetrics', () => { + jestIt('should return the smart transactions opt-in status', () => { + const state = createMockState(); + const result = getSmartTransactionsOptInStatusForMetrics(state); + expect(result).toBe(true); + }); - describe('getCurrentChainSupportsSmartTransactions', () => { - it('should return true if the chain ID is allowed for smart transactions', () => { - const state = createMockState(); - const result = getCurrentChainSupportsSmartTransactions(state); - expect(result).toBe(true); + jestIt.each([ + { status: true, expected: true }, + { status: false, expected: false }, + { status: null, expected: null }, + ])( + 'should return $expected if the smart transactions opt-in status is $status', + ({ status, expected }) => { + const state = createMockOptInStatusState(status); + const result = getSmartTransactionsOptInStatusForMetrics(state); + expect(result).toBe(expected); + }, + ); }); - it('should return false if the chain ID is not allowed for smart transactions', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + describe('getSmartTransactionsPreferenceEnabled', () => { + jestIt( + 'should return the smart transactions preference enabled status', + () => { + const state = createMockState(); + const result = getSmartTransactionsPreferenceEnabled(state); + expect(result).toBe(true); }, - }; - const result = getCurrentChainSupportsSmartTransactions(newState); - expect(result).toBe(false); + ); + + jestIt.each([ + { status: true, expected: true }, + { status: false, expected: false }, + { status: null, expected: true }, + ])( + 'should return $expected if the smart transactions opt-in status is $status', + ({ status, expected }) => { + const state = createMockOptInStatusState(status); + const result = getSmartTransactionsPreferenceEnabled(state); + expect(result).toBe(expected); + }, + ); }); }); + describe('getCurrentChainSupportsSmartTransactions', () => { + jestIt( + 'should return true if the chain ID is allowed for smart transactions', + () => { + const state = createMockState(); + const result = getCurrentChainSupportsSmartTransactions(state); + expect(result).toBe(true); + }, + ); + + jestIt( + 'should return false if the chain ID is not allowed for smart transactions', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + }, + }; + const result = getCurrentChainSupportsSmartTransactions(newState); + expect(result).toBe(false); + }, + ); + }); + describe('getSmartTransactionsEnabled', () => { - it('returns true if feature flag is enabled, not a HW and is Ethereum network', () => { - const state = createSwapsMockStore(); - expect(getSmartTransactionsEnabled(state)).toBe(true); - }); + jestIt( + 'returns true if feature flag is enabled, not a HW and is Ethereum network', + () => { + const state = createSwapsMockStore(); + expect(getSmartTransactionsEnabled(state)).toBe(true); + }, + ); - it('returns false if feature flag is disabled, not a HW and is Ethereum network', () => { - const state = createSwapsMockStore(); - state.metamask.swapsState.swapsFeatureFlags.smartTransactions.extensionActive = - false; - expect(getSmartTransactionsEnabled(state)).toBe(false); - }); + jestIt( + 'returns false if feature flag is disabled, not a HW and is Ethereum network', + () => { + const state = createSwapsMockStore(); + state.metamask.swapsState.swapsFeatureFlags.smartTransactions.extensionActive = + false; + expect(getSmartTransactionsEnabled(state)).toBe(false); + }, + ); - it('returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network', () => { - const state = createSwapsMockStore(); - state.metamask.smartTransactionsState.liveness = false; - expect(getSmartTransactionsEnabled(state)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network', + () => { + const state = createSwapsMockStore(); + state.metamask.smartTransactionsState.liveness = false; + expect(getSmartTransactionsEnabled(state)).toBe(false); + }, + ); - it('returns true if feature flag is enabled, is a HW and is Ethereum network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - internalAccounts: { - ...state.metamask.internalAccounts, - selectedAccount: 'account2', - accounts: { - account2: { - metadata: { - keyring: { - type: 'Trezor Hardware', + jestIt( + 'returns true if feature flag is enabled, is a HW and is Ethereum network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + internalAccounts: { + ...state.metamask.internalAccounts, + selectedAccount: 'account2', + accounts: { + account2: { + metadata: { + keyring: { + type: 'Trezor Hardware', + }, }, }, }, }, }, - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(true); - }); + }; + expect(getSmartTransactionsEnabled(newState)).toBe(true); + }, + ); - it('returns false if feature flag is enabled, not a HW and is Polygon network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW and is Polygon network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + }, + }; + expect(getSmartTransactionsEnabled(newState)).toBe(false); + }, + ); - it('returns false if feature flag is enabled, not a HW and is BSC network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.BSC }), - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW and is BSC network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.BSC }), + }, + }; + expect(getSmartTransactionsEnabled(newState)).toBe(false); + }, + ); - it('returns false if feature flag is enabled, not a HW and is Linea network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }), - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW and is Linea network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }), + }, + }; + expect(getSmartTransactionsEnabled(newState)).toBe(false); + }, + ); - it('returns false if a snap account is used', () => { + jestIt('returns false if a snap account is used', () => { const state = createSwapsMockStore(); state.metamask.internalAccounts.selectedAccount = '36eb02e0-7925-47f0-859f-076608f09b69'; @@ -182,13 +261,16 @@ describe('Selectors', () => { }); describe('getIsSmartTransaction', () => { - it('should return true if smart transactions are opt-in and enabled', () => { - const state = createMockState(); - const result = getIsSmartTransaction(state); - expect(result).toBe(true); - }); + jestIt( + 'should return true if smart transactions are opt-in and enabled', + () => { + const state = createMockState(); + const result = getIsSmartTransaction(state); + expect(result).toBe(true); + }, + ); - it('should return false if smart transactions are not opt-in', () => { + jestIt('should return false if smart transactions are not opt-in', () => { const state = createMockState(); const newState = { ...state, @@ -204,7 +286,7 @@ describe('Selectors', () => { expect(result).toBe(false); }); - it('should return false if smart transactions are not enabled', () => { + jestIt('should return false if smart transactions are not enabled', () => { const state = createMockState(); const newState = { ...state, @@ -236,103 +318,121 @@ describe('Selectors', () => { }); describe('getIsSmartTransactionsOptInModalAvailable', () => { - it('returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, + jestIt( + 'returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true); + }, + ); - it('returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, + jestIt( + 'returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), }, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, + jestIt( + 'returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + ...mockNetworkState({ + chainId: CHAIN_IDS.MAINNET, + rpcUrl: 'https://mainnet.quiknode.pro/', + }), }, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - rpcUrl: 'https://mainnet.quiknode.pro/', - }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance', () => { - const state = createMockState(); - expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false); - }); + jestIt( + 'returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance', + () => { + const state = createMockState(); + expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - accounts: { - ...state.metamask.accounts, - '0x123': { - address: '0x123', - balance: '0x0', + jestIt( + 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + accounts: { + ...state.metamask.accounts, + '0x123': { + address: '0x123', + balance: '0x0', + }, }, }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - accounts: { - ...state.metamask.accounts, - '0x123': { - address: '0x123', - balance: '0x00', + jestIt( + 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + accounts: { + ...state.metamask.accounts, + '0x123': { + address: '0x123', + balance: '0x00', + }, }, }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); }); }); diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index 1c3147632381..a02fe63692b3 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -1,3 +1,4 @@ +import { createSelector } from 'reselect'; import { getAllowedSmartTransactionsChainIds, SKIP_STX_RPC_URL_CHECK_CHAIN_IDS, @@ -7,6 +8,7 @@ import { getCurrentNetwork, accountSupportsSmartTx, getSelectedAccount, + getPreferences, // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../ui/selectors/selectors'; // TODO: Migrate shared selectors to this file. @@ -56,11 +58,60 @@ type SmartTransactionsMetaMaskState = { }; }; -export const getSmartTransactionsOptInStatus = ( - state: SmartTransactionsMetaMaskState, -): boolean | null => { - return state.metamask.preferences?.smartTransactionsOptInStatus ?? null; -}; +/** + * Returns the user's explicit opt-in status for the smart transactions feature. + * This should only be used for reading the user's internal opt-in status, and + * not for determining if the smart transactions user preference is enabled. + * + * To determine if the smart transactions user preference is enabled, use + * getSmartTransactionsPreferenceEnabled instead. + * + * @param state - The state object. + * @returns true if the user has explicitly opted in, false if they have opted out, + * or null if they have not explicitly opted in or out. + */ +export const getSmartTransactionsOptInStatusInternal = createSelector( + getPreferences, + (preferences: { + smartTransactionsOptInStatus?: boolean | null; + }): boolean | null => { + return preferences?.smartTransactionsOptInStatus ?? null; + }, +); + +/** + * Returns the user's explicit opt-in status for the smart transactions feature. + * This should only be used for metrics collection, and not for determining if the + * smart transactions user preference is enabled. + * + * To determine if the smart transactions user preference is enabled, use + * getSmartTransactionsPreferenceEnabled instead. + * + * @param state - The state object. + * @returns true if the user has explicitly opted in, false if they have opted out, + * or null if they have not explicitly opted in or out. + */ +export const getSmartTransactionsOptInStatusForMetrics = createSelector( + getSmartTransactionsOptInStatusInternal, + (optInStatus: boolean | null): boolean | null => optInStatus, +); + +/** + * Returns the user's preference for the smart transactions feature. + * Defaults to `true` if the user has not set a preference. + * + * @param state + * @returns + */ +export const getSmartTransactionsPreferenceEnabled = createSelector( + getSmartTransactionsOptInStatusInternal, + (optInStatus: boolean | null): boolean => { + // In the absence of an explicit opt-in or opt-out, + // the Smart Transactions toggle is enabled. + const DEFAULT_SMART_TRANSACTIONS_ENABLED = true; + return optInStatus ?? DEFAULT_SMART_TRANSACTIONS_ENABLED; + }, +); export const getCurrentChainSupportsSmartTransactions = ( state: SmartTransactionsMetaMaskState, @@ -105,7 +156,7 @@ export const getIsSmartTransactionsOptInModalAvailable = ( return ( getCurrentChainSupportsSmartTransactions(state) && getIsAllowedRpcUrlForSmartTransactions(state) && - getSmartTransactionsOptInStatus(state) === null && + getSmartTransactionsOptInStatusInternal(state) === null && hasNonZeroBalance(state) ); }; @@ -132,7 +183,10 @@ export const getSmartTransactionsEnabled = ( export const getIsSmartTransaction = ( state: SmartTransactionsMetaMaskState, ): boolean => { - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); + const smartTransactionsPreferenceEnabled = + getSmartTransactionsPreferenceEnabled(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); - return Boolean(smartTransactionsOptInStatus && smartTransactionsEnabled); + return Boolean( + smartTransactionsPreferenceEnabled && smartTransactionsEnabled, + ); }; diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 91ed081eb719..8dd7336d7a62 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -69,8 +69,9 @@ import { getSelectedInternalAccount, } from '../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, + getSmartTransactionsPreferenceEnabled, } from '../../../shared/modules/selectors'; import { MetaMetricsEventCategory, @@ -746,7 +747,6 @@ export const fetchQuotesAndSetQuoteState = ( const hardwareWalletType = getHardwareWalletType(state); const networkAndAccountSupports1559 = checkNetworkAndAccountSupports1559(state); - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(state); @@ -764,7 +764,7 @@ export const fetchQuotesAndSetQuoteState = ( hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), anonymizedData: true, }, }); @@ -784,7 +784,8 @@ export const fetchQuotesAndSetQuoteState = ( balanceError, sourceDecimals: fromTokenDecimals, enableGasIncludedQuotes: - currentSmartTransactionsEnabled && smartTransactionsOptInStatus, + currentSmartTransactionsEnabled && + getSmartTransactionsPreferenceEnabled(state), }, { sourceTokenInfo, @@ -819,7 +820,7 @@ export const fetchQuotesAndSetQuoteState = ( hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), }, }); dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)); @@ -856,7 +857,7 @@ export const fetchQuotesAndSetQuoteState = ( hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), anonymizedData: true, }, }); @@ -910,7 +911,6 @@ export const signAndSendSwapsSmartTransaction = ({ usedQuote.destinationAmount, destinationTokenInfo.decimals || 18, ).toPrecision(8); - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(state); @@ -937,7 +937,7 @@ export const signAndSendSwapsSmartTransaction = ({ hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), gas_included: usedQuote.isGasIncludedTrade, ...additionalTrackingParams, }; @@ -1180,7 +1180,6 @@ export const signAndSendTransactions = ( numberOfDecimals: 6, }); - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(state); @@ -1212,7 +1211,7 @@ export const signAndSendTransactions = ( hardware_wallet_type: getHardwareWalletType(state), stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), ...additionalTrackingParams, }; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index b4d2d6a8def5..1ae7eaaeb33d 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -178,7 +178,7 @@ export default class ConfirmTransactionBase extends Component { isUserOpContractDeployError: PropTypes.bool, useMaxValue: PropTypes.bool, maxValue: PropTypes.string, - smartTransactionsOptInStatus: PropTypes.bool, + smartTransactionsPreferenceEnabled: PropTypes.bool, currentChainSupportsSmartTransactions: PropTypes.bool, selectedNetworkClientId: PropTypes.string, isSmartTransactionsEnabled: PropTypes.bool, @@ -1019,7 +1019,7 @@ export default class ConfirmTransactionBase extends Component { txData: { origin, chainId: txChainId } = {}, getNextNonce, tryReverseResolveAddress, - smartTransactionsOptInStatus, + smartTransactionsPreferenceEnabled, currentChainSupportsSmartTransactions, setSwapsFeatureFlags, fetchSmartTransactionsLiveness, @@ -1071,7 +1071,10 @@ export default class ConfirmTransactionBase extends Component { window.addEventListener('beforeunload', this._beforeUnloadForGasPolling); - if (smartTransactionsOptInStatus && currentChainSupportsSmartTransactions) { + if ( + smartTransactionsPreferenceEnabled && + currentChainSupportsSmartTransactions + ) { // TODO: Fetching swaps feature flags, which include feature flags for smart transactions, is only a short-term solution. // Long-term, we want to have a new proxy service specifically for feature flags. Promise.all([ diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index 5d92a1af9c56..e06090f48e75 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -59,10 +59,10 @@ import { } from '../../../selectors'; import { getCurrentChainSupportsSmartTransactions, - getSmartTransactionsOptInStatus, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) getSmartTransactionsEnabled, ///: END:ONLY_INCLUDE_IF + getSmartTransactionsPreferenceEnabled, } from '../../../../shared/modules/selectors'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { @@ -185,7 +185,8 @@ const mapStateToProps = (state, ownProps) => { data, } = (transaction && transaction.txParams) || txParams; const accounts = getMetaMaskAccounts(state); - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); + const smartTransactionsPreferenceEnabled = + getSmartTransactionsPreferenceEnabled(state); const currentChainSupportsSmartTransactions = getCurrentChainSupportsSmartTransactions(state); @@ -364,7 +365,7 @@ const mapStateToProps = (state, ownProps) => { isUserOpContractDeployError, useMaxValue, maxValue, - smartTransactionsOptInStatus, + smartTransactionsPreferenceEnabled, currentChainSupportsSmartTransactions, hasPriorityApprovalRequest, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 42bdfc685779..dfeb1a5e7cdb 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -51,7 +51,6 @@ import { getAccountType, ///: END:ONLY_INCLUDE_IF } from '../../selectors'; -import { getIsSmartTransactionsOptInModalAvailable } from '../../../shared/modules/selectors'; import { closeNotificationPopup, @@ -223,8 +222,10 @@ const mapStateToProps = (state) => { custodianDeepLink: getCustodianDeepLink(state), accountType: getAccountType(state), ///: END:ONLY_INCLUDE_IF - isSmartTransactionsOptInModalAvailable: - getIsSmartTransactionsOptInModalAvailable(state), + + // Set to false to prevent the opt-in modal from showing. + // TODO(dbrans): Remove opt-in modal once default opt-in is stable. + isSmartTransactionsOptInModalAvailable: false, showMultiRpcModal: state.metamask.preferences.showMultiRpcModal, }; }; diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index 419daf561778..2c26f0daa0b5 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -30,7 +30,6 @@ import { getNftDetectionEnablementToast, getCurrentNetwork, } from '../../selectors'; -import { getSmartTransactionsOptInStatus } from '../../../shared/modules/selectors'; import { lockMetamask, hideImportNftsModal, @@ -118,7 +117,6 @@ function mapStateToProps(state) { allAccountsOnNetworkAreEmpty: getAllAccountsOnNetworkAreEmpty(state), isTestNet: getIsTestnet(state), showExtensionInFullSizeView: getShowExtensionInFullSizeView(state), - smartTransactionsOptInStatus: getSmartTransactionsOptInStatus(state), currentChainId: getCurrentChainId(state), shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state), forgottenPassword: state.metamask.forgottenPassword, diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 5c7f4a659a6d..50aea4e0dc60 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -46,12 +46,12 @@ export default class AdvancedTab extends PureComponent { sendHexData: PropTypes.bool, showFiatInTestnets: PropTypes.bool, showTestNetworks: PropTypes.bool, - smartTransactionsOptInStatus: PropTypes.bool, + smartTransactionsEnabled: PropTypes.bool, autoLockTimeLimit: PropTypes.number, setAutoLockTimeLimit: PropTypes.func.isRequired, setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, setShowTestNetworks: PropTypes.func.isRequired, - setSmartTransactionsOptInStatus: PropTypes.func.isRequired, + setSmartTransactionsEnabled: PropTypes.func.isRequired, setDismissSeedBackUpReminder: PropTypes.func.isRequired, dismissSeedBackUpReminder: PropTypes.bool.isRequired, backupUserData: PropTypes.func.isRequired, @@ -199,7 +199,7 @@ export default class AdvancedTab extends PureComponent { renderToggleStxOptIn() { const { t } = this.context; - const { smartTransactionsOptInStatus, setSmartTransactionsOptInStatus } = + const { smartTransactionsEnabled, setSmartTransactionsEnabled } = this.props; const learMoreLink = ( @@ -237,10 +237,10 @@ export default class AdvancedTab extends PureComponent {
    { const newValue = !oldValue; - setSmartTransactionsOptInStatus(newValue); + setSmartTransactionsEnabled(newValue); }} offLabel={t('off')} onLabel={t('on')} diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js index 6be53fb4e21a..2c64b79e4f4d 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js @@ -9,7 +9,7 @@ import AdvancedTab from '.'; const mockSetAutoLockTimeLimit = jest.fn().mockReturnValue({ type: 'TYPE' }); const mockSetShowTestNetworks = jest.fn(); const mockSetShowFiatConversionOnTestnetsPreference = jest.fn(); -const mockSetStxOptIn = jest.fn(); +const mockSetStxPrefEnabled = jest.fn(); jest.mock('../../../store/actions.ts', () => { return { @@ -17,7 +17,7 @@ jest.mock('../../../store/actions.ts', () => { setShowTestNetworks: () => mockSetShowTestNetworks, setShowFiatConversionOnTestnetsPreference: () => mockSetShowFiatConversionOnTestnetsPreference, - setSmartTransactionsOptInStatus: () => mockSetStxOptIn, + setSmartTransactionsPreferenceEnabled: () => mockSetStxPrefEnabled, }; }); @@ -102,7 +102,7 @@ describe('AdvancedTab Component', () => { const { queryByTestId } = renderWithProvider(, mockStore); const toggleButton = queryByTestId('settings-page-stx-opt-in-toggle'); fireEvent.click(toggleButton); - expect(mockSetStxOptIn).toHaveBeenCalled(); + expect(mockSetStxPrefEnabled).toHaveBeenCalled(); }); }); }); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index 2a11b6751dae..f2ad894d1e8b 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -12,10 +12,11 @@ import { setShowExtensionInFullSizeView, setShowFiatConversionOnTestnetsPreference, setShowTestNetworks, - setSmartTransactionsOptInStatus, + setSmartTransactionsPreferenceEnabled, setUseNonceField, showModal, } from '../../../store/actions'; +import { getSmartTransactionsPreferenceEnabled } from '../../../../shared/modules/selectors'; import AdvancedTab from './advanced-tab.component'; export const mapStateToProps = (state) => { @@ -32,7 +33,6 @@ export const mapStateToProps = (state) => { showFiatInTestnets, showTestNetworks, showExtensionInFullSizeView, - smartTransactionsOptInStatus, autoLockTimeLimit = DEFAULT_AUTO_LOCK_TIME_LIMIT, } = getPreferences(state); @@ -42,7 +42,7 @@ export const mapStateToProps = (state) => { showFiatInTestnets, showTestNetworks, showExtensionInFullSizeView, - smartTransactionsOptInStatus, + smartTransactionsEnabled: getSmartTransactionsPreferenceEnabled(state), autoLockTimeLimit, useNonceField, dismissSeedBackUpReminder, @@ -67,8 +67,8 @@ export const mapDispatchToProps = (dispatch) => { setShowExtensionInFullSizeView: (value) => { return dispatch(setShowExtensionInFullSizeView(value)); }, - setSmartTransactionsOptInStatus: (value) => { - return dispatch(setSmartTransactionsOptInStatus(value)); + setSmartTransactionsEnabled: (value) => { + return dispatch(setSmartTransactionsPreferenceEnabled(value)); }, setAutoLockTimeLimit: (value) => { return dispatch(setAutoLockTimeLimit(value)); diff --git a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx index 15546c3aa09d..ab491ea05ea5 100644 --- a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx +++ b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx @@ -7,7 +7,7 @@ import { renderWithProvider, createSwapsMockStore, } from '../../../../test/jest'; -import { setSmartTransactionsOptInStatus } from '../../../store/actions'; +import { setSmartTransactionsPreferenceEnabled } from '../../../store/actions'; import SmartTransactionsOptInModal from './smart-transactions-opt-in-modal'; const middleware = [thunk]; @@ -35,8 +35,8 @@ describe('SmartTransactionsOptInModal', () => { }); it('calls setSmartTransactionsOptInStatus with false when the "No thanks" link is clicked', () => { - (setSmartTransactionsOptInStatus as jest.Mock).mockImplementationOnce(() => - jest.fn(), + (setSmartTransactionsPreferenceEnabled as jest.Mock).mockImplementationOnce( + () => jest.fn(), ); const store = configureMockStore(middleware)(createSwapsMockStore()); const { getByText } = renderWithProvider( @@ -48,12 +48,12 @@ describe('SmartTransactionsOptInModal', () => { ); const noThanksLink = getByText('No thanks'); fireEvent.click(noThanksLink); - expect(setSmartTransactionsOptInStatus).toHaveBeenCalledWith(false); + expect(setSmartTransactionsPreferenceEnabled).toHaveBeenCalledWith(false); }); it('calls setSmartTransactionsOptInStatus with true when the "Enable" button is clicked', () => { - (setSmartTransactionsOptInStatus as jest.Mock).mockImplementationOnce(() => - jest.fn(), + (setSmartTransactionsPreferenceEnabled as jest.Mock).mockImplementationOnce( + () => jest.fn(), ); const store = configureMockStore(middleware)(createSwapsMockStore()); const { getByText } = renderWithProvider( @@ -65,6 +65,6 @@ describe('SmartTransactionsOptInModal', () => { ); const enableButton = getByText('Enable'); fireEvent.click(enableButton); - expect(setSmartTransactionsOptInStatus).toHaveBeenCalledWith(true); + expect(setSmartTransactionsPreferenceEnabled).toHaveBeenCalledWith(true); }); }); diff --git a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx index 78055de42831..2269018e2239 100644 --- a/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx +++ b/ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx @@ -28,7 +28,7 @@ import { Icon, IconName, } from '../../../components/component-library'; -import { setSmartTransactionsOptInStatus } from '../../../store/actions'; +import { setSmartTransactionsPreferenceEnabled } from '../../../store/actions'; import { SMART_TRANSACTIONS_LEARN_MORE_URL } from '../../../../shared/constants/smartTransactions'; export type SmartTransactionsOptInModalProps = { @@ -166,12 +166,12 @@ export default function SmartTransactionsOptInModal({ const dispatch = useDispatch(); const handleEnableButtonClick = useCallback(() => { - dispatch(setSmartTransactionsOptInStatus(true)); + dispatch(setSmartTransactionsPreferenceEnabled(true)); }, [dispatch]); const handleNoThanksLinkClick = useCallback(() => { // Set the Smart Transactions opt-in status to false, so the opt-in modal is not shown again. - dispatch(setSmartTransactionsOptInStatus(false)); + dispatch(setSmartTransactionsPreferenceEnabled(false)); }, [dispatch]); useEffect(() => { diff --git a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js index a1b4179beefb..56db1578ce9a 100644 --- a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js +++ b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js @@ -15,8 +15,8 @@ import { getHardwareWalletType, } from '../../../selectors/selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { DEFAULT_ROUTE, @@ -47,7 +47,7 @@ export default function AwaitingSignatures() { const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index b5c2589fbd28..7c410ca03ce5 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -23,8 +23,8 @@ import { getFullTxData, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { @@ -120,7 +120,7 @@ export default function AwaitingSwap({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( diff --git a/ui/pages/swaps/index.js b/ui/pages/swaps/index.js index cf079acc9623..e16166297545 100644 --- a/ui/pages/swaps/index.js +++ b/ui/pages/swaps/index.js @@ -46,8 +46,8 @@ import { } from '../../ducks/swaps/swaps'; import { getCurrentNetworkTransactions } from '../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../shared/modules/selectors'; import { AWAITING_SIGNATURES_ROUTE, @@ -133,7 +133,7 @@ export default function Swap() { const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( diff --git a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js index e98d275f8aa8..ebbb6f652496 100644 --- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js +++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js @@ -16,8 +16,8 @@ import { getHardwareWalletType, } from '../../../selectors/selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { I18nContext } from '../../../contexts/i18n'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -51,7 +51,7 @@ export default function LoadingSwapsQuotes({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js index 72050df4aca9..8a701289bebd 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -69,8 +69,9 @@ import { getDataCollectionForMarketing, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsPreferenceEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { getValueFromWeiHex, @@ -212,14 +213,15 @@ export default function PrepareSwapPage({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); const isSmartTransaction = - currentSmartTransactionsEnabled && smartTransactionsOptInStatus; + useSelector(getSmartTransactionsPreferenceEnabled) && + currentSmartTransactionsEnabled; const currentCurrency = useSelector(getCurrentCurrency); const fetchingQuotes = useSelector(getFetchingQuotes); const loadingComplete = !fetchingQuotes && areQuotesPresent; diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 4c47437b1bd8..13d11a93cd1f 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -58,8 +58,9 @@ import { getUSDConversionRate, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, getSmartTransactionsEnabled, + getSmartTransactionsPreferenceEnabled, } from '../../../../shared/modules/selectors'; import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; import { @@ -240,7 +241,10 @@ export default function ReviewQuote({ setReceiveToAmount }) { const nativeCurrencySymbol = useSelector(getNativeCurrency); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, + ); + const smartTransactionsPreferenceEnabled = useSelector( + getSmartTransactionsPreferenceEnabled, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const swapsSTXLoading = useSelector(getSwapsSTXLoading); @@ -280,7 +284,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { const unsignedTransaction = usedQuote.trade; const { isGasIncludedTrade } = usedQuote; const isSmartTransaction = - currentSmartTransactionsEnabled && smartTransactionsOptInStatus; + useSelector(getSmartTransactionsPreferenceEnabled) && + currentSmartTransactionsEnabled; const [slippageErrorKey] = useState(() => { const slippage = Number(fetchParams?.slippage); @@ -377,7 +382,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { chainId, smartTransactionEstimatedGas: smartTransactionsEnabled && - smartTransactionsOptInStatus && + smartTransactionsPreferenceEnabled && smartTransactionFees?.tradeTxFees, nativeCurrencySymbol, multiLayerL1ApprovalFeeTotal, @@ -394,7 +399,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { smartTransactionFees?.tradeTxFees, nativeCurrencySymbol, smartTransactionsEnabled, - smartTransactionsOptInStatus, + smartTransactionsPreferenceEnabled, multiLayerL1ApprovalFeeTotal, ]); @@ -887,7 +892,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { (currentSmartTransactionsEnabled && (currentSmartTransactionsError || smartTransactionsError)) || (currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && + smartTransactionsPreferenceEnabled && !smartTransactionFees?.tradeTxFees), ); @@ -1136,7 +1141,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { initialAggId={usedQuote.aggregator} onQuoteDetailsIsOpened={trackQuoteDetailsOpened} hideEstimatedGasFee={ - smartTransactionsEnabled && smartTransactionsOptInStatus + smartTransactionsEnabled && smartTransactionsPreferenceEnabled } /> ) diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index d6e7f4d653cf..7b8d5910c218 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -20,8 +20,8 @@ import { getRpcPrefsForCurrentProvider, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; import { @@ -78,9 +78,6 @@ export default function SmartTransactionStatusPage() { getCurrentSmartTransactions, isEqual, ); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); const chainId = useSelector(getCurrentChainId); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); @@ -128,7 +125,7 @@ export default function SmartTransactionStatusPage() { hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: useSelector(getSmartTransactionsOptInStatusForMetrics), }; let destinationValue; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 43c7fb189822..3433a798a9d9 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -103,7 +103,7 @@ import { } from '../../shared/constants/metametrics'; import { parseSmartTransactionsError } from '../pages/swaps/swaps.util'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; -import { getSmartTransactionsOptInStatus } from '../../shared/modules/selectors'; +import { getSmartTransactionsOptInStatusInternal } from '../../shared/modules/selectors'; import { NOTIFICATIONS_EXPIRATION_DELAY } from '../helpers/constants/notifications'; import { fetchLocale, @@ -3090,13 +3090,12 @@ export function setTokenSortConfig(value: SortCriteria) { return setPreference('tokenSortConfig', value, false); } -export function setSmartTransactionsOptInStatus( +export function setSmartTransactionsPreferenceEnabled( value: boolean, ): ThunkAction { return async (dispatch, getState) => { - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus( - getState(), - ); + const smartTransactionsOptInStatus = + getSmartTransactionsOptInStatusInternal(getState()); trackMetaMetricsEvent({ category: MetaMetricsEventCategory.Settings, event: MetaMetricsEventName.SettingsUpdated, diff --git a/yarn.lock b/yarn.lock index 3c34b05fee52..19e1f3bf1a8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26086,6 +26086,7 @@ __metadata: "@ethersproject/providers": "npm:^5.7.2" "@ethersproject/wallet": "npm:^5.7.0" "@fortawesome/fontawesome-free": "npm:^5.13.0" + "@jest/globals": "npm:^29.7.0" "@keystonehq/bc-ur-registry-eth": "npm:^0.19.1" "@keystonehq/metamask-airgapped-keyring": "npm:^0.13.1" "@lavamoat/allow-scripts": "npm:^3.0.4" From bf475ee2cee9f63dee60492e39b39e64d2636fe1 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 17 Oct 2024 16:28:26 +0100 Subject: [PATCH 27/51] feat: convert AlertController to typescript (#27764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** As a prerequisite for migrating AlertController to BaseController v2, and to support the TypeScript migration effort, we want to convert AlertController to TypeScript. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27764?quickstart=1) ## **Related issues** Fixes: #25921 ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .eslintrc.js | 1 + .../controllers/alert-controller.test.ts | 258 ++++++++++++++++++ app/scripts/controllers/alert-controller.ts | 203 ++++++++++++++ app/scripts/controllers/alert.js | 136 --------- app/scripts/metamask-controller.js | 4 +- .../files-to-convert.json | 1 - 6 files changed, 464 insertions(+), 139 deletions(-) create mode 100644 app/scripts/controllers/alert-controller.test.ts create mode 100644 app/scripts/controllers/alert-controller.ts delete mode 100644 app/scripts/controllers/alert.js diff --git a/.eslintrc.js b/.eslintrc.js index 258556239ac3..97d52b6637cc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -309,6 +309,7 @@ module.exports = { '**/__snapshots__/*.snap', 'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/mmi-controller.test.ts', + 'app/scripts/controllers/alert-controller.test.ts', 'app/scripts/metamask-controller.actions.test.js', 'app/scripts/detect-multiple-instances.test.js', 'app/scripts/controllers/bridge.test.ts', diff --git a/app/scripts/controllers/alert-controller.test.ts b/app/scripts/controllers/alert-controller.test.ts new file mode 100644 index 000000000000..a8aee606e02d --- /dev/null +++ b/app/scripts/controllers/alert-controller.test.ts @@ -0,0 +1,258 @@ +/** + * @jest-environment node + */ +import { ControllerMessenger } from '@metamask/base-controller'; +import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; +import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; +import { EthAccountType } from '@metamask/keyring-api'; +import { + AlertControllerActions, + AlertControllerEvents, + AlertController, + AllowedActions, + AllowedEvents, + AlertControllerState, +} from './alert-controller'; + +const EMPTY_ACCOUNT = { + id: '', + address: '', + options: {}, + methods: [], + type: EthAccountType.Eoa, + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, + }, +}; +describe('AlertController', () => { + let controllerMessenger: ControllerMessenger< + AlertControllerActions | AllowedActions, + | AlertControllerEvents + | KeyringControllerStateChangeEvent + | SnapControllerStateChangeEvent + | AllowedEvents + >; + let alertController: AlertController; + + beforeEach(() => { + controllerMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + () => EMPTY_ACCOUNT, + ); + + const alertMessenger = controllerMessenger.getRestricted({ + name: 'AlertController', + allowedActions: [`AccountsController:getSelectedAccount`], + allowedEvents: [`AccountsController:selectedAccountChange`], + }); + + alertController = new AlertController({ + state: { + unconnectedAccountAlertShownOrigins: { + testUnconnectedOrigin: false, + }, + web3ShimUsageOrigins: { + testWeb3ShimUsageOrigin: 0, + }, + }, + controllerMessenger: alertMessenger, + }); + }); + + describe('default state', () => { + it('should be same as AlertControllerState initialized', () => { + expect(alertController.store.getState()).toStrictEqual({ + alertEnabledness: { + unconnectedAccount: true, + web3ShimUsage: true, + }, + unconnectedAccountAlertShownOrigins: { + testUnconnectedOrigin: false, + }, + web3ShimUsageOrigins: { + testWeb3ShimUsageOrigin: 0, + }, + }); + }); + }); + + describe('alertEnabledness', () => { + it('should default unconnectedAccount of alertEnabledness to true', () => { + expect( + alertController.store.getState().alertEnabledness.unconnectedAccount, + ).toStrictEqual(true); + }); + + it('should set unconnectedAccount of alertEnabledness to false', () => { + alertController.setAlertEnabledness('unconnectedAccount', false); + expect( + alertController.store.getState().alertEnabledness.unconnectedAccount, + ).toStrictEqual(false); + expect( + controllerMessenger.call('AlertController:getState').alertEnabledness + .unconnectedAccount, + ).toStrictEqual(false); + }); + }); + + describe('unconnectedAccountAlertShownOrigins', () => { + it('should default unconnectedAccountAlertShownOrigins', () => { + expect( + alertController.store.getState().unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: false, + }); + expect( + controllerMessenger.call('AlertController:getState') + .unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: false, + }); + }); + + it('should set unconnectedAccountAlertShownOrigins', () => { + alertController.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); + expect( + alertController.store.getState().unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: true, + }); + expect( + controllerMessenger.call('AlertController:getState') + .unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: true, + }); + }); + }); + + describe('web3ShimUsageOrigins', () => { + it('should default web3ShimUsageOrigins', () => { + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 0, + }); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 0, + }); + }); + + it('should set origin of web3ShimUsageOrigins to recorded', () => { + alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + }); + it('should set origin of web3ShimUsageOrigins to dismissed', () => { + alertController.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 2, + }); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 2, + }); + }); + }); + + describe('selectedAccount change', () => { + it('should set unconnectedAccountAlertShownOrigins to {}', () => { + controllerMessenger.publish('AccountsController:selectedAccountChange', { + id: '', + address: '0x1234567', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, + }, + }); + expect( + alertController.store.getState().unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); + expect( + controllerMessenger.call('AlertController:getState') + .unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); + }); + }); + + describe('AlertController:getState', () => { + it('should return the current state of the property', () => { + const defaultWeb3ShimUsageOrigins = { + testWeb3ShimUsageOrigin: 0, + }; + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual(defaultWeb3ShimUsageOrigins); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual(defaultWeb3ShimUsageOrigins); + }); + }); + + describe('AlertController:stateChange', () => { + it('state will be published when there is state change', () => { + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 0, + }); + + controllerMessenger.subscribe( + 'AlertController:stateChange', + (state: Partial) => { + expect(state.web3ShimUsageOrigins).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + }, + ); + + alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + expect( + alertController.getWeb3ShimUsageState('testWeb3ShimUsageOrigin'), + ).toStrictEqual(1); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + }); + }); +}); diff --git a/app/scripts/controllers/alert-controller.ts b/app/scripts/controllers/alert-controller.ts new file mode 100644 index 000000000000..9e1882035e02 --- /dev/null +++ b/app/scripts/controllers/alert-controller.ts @@ -0,0 +1,203 @@ +import { ObservableStore } from '@metamask/obs-store'; +import { + AccountsControllerGetSelectedAccountAction, + AccountsControllerSelectedAccountChangeEvent, +} from '@metamask/accounts-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + TOGGLEABLE_ALERT_TYPES, + Web3ShimUsageAlertStates, +} from '../../../shared/constants/alerts'; + +const controllerName = 'AlertController'; + +/** + * Returns the state of the {@link AlertController}. + */ +export type AlertControllerGetStateAction = { + type: 'AlertController:getState'; + handler: () => AlertControllerState; +}; + +/** + * Actions exposed by the {@link AlertController}. + */ +export type AlertControllerActions = AlertControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AlertController} changes. + */ +export type AlertControllerStateChangeEvent = { + type: 'AlertController:stateChange'; + payload: [AlertControllerState, []]; +}; + +/** + * Events emitted by {@link AlertController}. + */ +export type AlertControllerEvents = AlertControllerStateChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = AccountsControllerGetSelectedAccountAction; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; + +export type AlertControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AlertControllerActions | AllowedActions, + AlertControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +/** + * The alert controller state type + * + * @property alertEnabledness - A map of alerts IDs to booleans, where + * `true` indicates that the alert is enabled and shown, and `false` the opposite. + * @property unconnectedAccountAlertShownOrigins - A map of origin + * strings to booleans indicating whether the "switch to connected" alert has + * been shown (`true`) or otherwise (`false`). + */ +export type AlertControllerState = { + alertEnabledness: Record; + unconnectedAccountAlertShownOrigins: Record; + web3ShimUsageOrigins?: Record; +}; + +/** + * The alert controller options + * + * @property state - The initial controller state + * @property controllerMessenger - The controller messenger + */ +type AlertControllerOptions = { + state?: Partial; + controllerMessenger: AlertControllerMessenger; +}; + +const defaultState: AlertControllerState = { + alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( + (alertEnabledness: Record, alertType: string) => { + alertEnabledness[alertType] = true; + return alertEnabledness; + }, + {}, + ), + unconnectedAccountAlertShownOrigins: {}, + web3ShimUsageOrigins: {}, +}; + +/** + * Controller responsible for maintaining alert-related state. + */ +export class AlertController { + store: ObservableStore; + + readonly #controllerMessenger: AlertControllerMessenger; + + #selectedAddress: string; + + constructor(opts: AlertControllerOptions) { + const state: AlertControllerState = { + ...defaultState, + ...opts.state, + }; + + this.store = new ObservableStore(state); + this.#controllerMessenger = opts.controllerMessenger; + this.#controllerMessenger.registerActionHandler( + 'AlertController:getState', + () => this.store.getState(), + ); + this.store.subscribe((alertState: AlertControllerState) => { + this.#controllerMessenger.publish( + 'AlertController:stateChange', + alertState, + [], + ); + }); + + this.#selectedAddress = this.#controllerMessenger.call( + 'AccountsController:getSelectedAccount', + ).address; + + this.#controllerMessenger.subscribe( + 'AccountsController:selectedAccountChange', + (account: { address: string }) => { + const currentState = this.store.getState(); + if ( + currentState.unconnectedAccountAlertShownOrigins && + this.#selectedAddress !== account.address + ) { + this.#selectedAddress = account.address; + this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); + } + }, + ); + } + + setAlertEnabledness(alertId: string, enabledness: boolean): void { + const { alertEnabledness } = this.store.getState(); + alertEnabledness[alertId] = enabledness; + this.store.updateState({ alertEnabledness }); + } + + /** + * Sets the "switch to connected" alert as shown for the given origin + * + * @param origin - The origin the alert has been shown for + */ + setUnconnectedAccountAlertShown(origin: string): void { + const { unconnectedAccountAlertShownOrigins } = this.store.getState(); + unconnectedAccountAlertShownOrigins[origin] = true; + this.store.updateState({ unconnectedAccountAlertShownOrigins }); + } + + /** + * Gets the web3 shim usage state for the given origin. + * + * @param origin - The origin to get the web3 shim usage state for. + * @returns The web3 shim usage state for the given + * origin, or undefined. + */ + getWeb3ShimUsageState(origin: string): number | undefined { + return this.store.getState().web3ShimUsageOrigins?.[origin]; + } + + /** + * Sets the web3 shim usage state for the given origin to RECORDED. + * + * @param origin - The origin the that used the web3 shim. + */ + setWeb3ShimUsageRecorded(origin: string): void { + this.#setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.recorded); + } + + /** + * Sets the web3 shim usage state for the given origin to DISMISSED. + * + * @param origin - The origin that the web3 shim notification was + * dismissed for. + */ + setWeb3ShimUsageAlertDismissed(origin: string): void { + this.#setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.dismissed); + } + + /** + * @param origin - The origin to set the state for. + * @param value - The state value to set. + */ + #setWeb3ShimUsageState(origin: string, value: number): void { + const { web3ShimUsageOrigins } = this.store.getState(); + if (web3ShimUsageOrigins) { + web3ShimUsageOrigins[origin] = value; + this.store.updateState({ web3ShimUsageOrigins }); + } + } +} diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js deleted file mode 100644 index d13e4cb2fbbf..000000000000 --- a/app/scripts/controllers/alert.js +++ /dev/null @@ -1,136 +0,0 @@ -import { ObservableStore } from '@metamask/obs-store'; -import { - TOGGLEABLE_ALERT_TYPES, - Web3ShimUsageAlertStates, -} from '../../../shared/constants/alerts'; - -/** - * @typedef {object} AlertControllerInitState - * @property {object} alertEnabledness - A map of alerts IDs to booleans, where - * `true` indicates that the alert is enabled and shown, and `false` the opposite. - * @property {object} unconnectedAccountAlertShownOrigins - A map of origin - * strings to booleans indicating whether the "switch to connected" alert has - * been shown (`true`) or otherwise (`false`). - */ - -/** - * @typedef {object} AlertControllerOptions - * @property {AlertControllerInitState} initState - The initial controller state - */ - -const defaultState = { - alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( - (alertEnabledness, alertType) => { - alertEnabledness[alertType] = true; - return alertEnabledness; - }, - {}, - ), - unconnectedAccountAlertShownOrigins: {}, - web3ShimUsageOrigins: {}, -}; - -/** - * Controller responsible for maintaining alert-related state. - */ -export default class AlertController { - /** - * @param {AlertControllerOptions} [opts] - Controller configuration parameters - */ - constructor(opts = {}) { - const { initState = {}, controllerMessenger } = opts; - const state = { - ...defaultState, - alertEnabledness: { - ...defaultState.alertEnabledness, - ...initState.alertEnabledness, - }, - }; - - this.store = new ObservableStore(state); - this.controllerMessenger = controllerMessenger; - - this.selectedAddress = this.controllerMessenger.call( - 'AccountsController:getSelectedAccount', - ); - - this.controllerMessenger.subscribe( - 'AccountsController:selectedAccountChange', - (account) => { - const currentState = this.store.getState(); - if ( - currentState.unconnectedAccountAlertShownOrigins && - this.selectedAddress !== account.address - ) { - this.selectedAddress = account.address; - this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); - } - }, - ); - } - - setAlertEnabledness(alertId, enabledness) { - let { alertEnabledness } = this.store.getState(); - alertEnabledness = { ...alertEnabledness }; - alertEnabledness[alertId] = enabledness; - this.store.updateState({ alertEnabledness }); - } - - /** - * Sets the "switch to connected" alert as shown for the given origin - * - * @param {string} origin - The origin the alert has been shown for - */ - setUnconnectedAccountAlertShown(origin) { - let { unconnectedAccountAlertShownOrigins } = this.store.getState(); - unconnectedAccountAlertShownOrigins = { - ...unconnectedAccountAlertShownOrigins, - }; - unconnectedAccountAlertShownOrigins[origin] = true; - this.store.updateState({ unconnectedAccountAlertShownOrigins }); - } - - /** - * Gets the web3 shim usage state for the given origin. - * - * @param {string} origin - The origin to get the web3 shim usage state for. - * @returns {undefined | 1 | 2} The web3 shim usage state for the given - * origin, or undefined. - */ - getWeb3ShimUsageState(origin) { - return this.store.getState().web3ShimUsageOrigins[origin]; - } - - /** - * Sets the web3 shim usage state for the given origin to RECORDED. - * - * @param {string} origin - The origin the that used the web3 shim. - */ - setWeb3ShimUsageRecorded(origin) { - this._setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.recorded); - } - - /** - * Sets the web3 shim usage state for the given origin to DISMISSED. - * - * @param {string} origin - The origin that the web3 shim notification was - * dismissed for. - */ - setWeb3ShimUsageAlertDismissed(origin) { - this._setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.dismissed); - } - - /** - * @private - * @param {string} origin - The origin to set the state for. - * @param {number} value - The state value to set. - */ - _setWeb3ShimUsageState(origin, value) { - let { web3ShimUsageOrigins } = this.store.getState(); - web3ShimUsageOrigins = { - ...web3ShimUsageOrigins, - }; - web3ShimUsageOrigins[origin] = value; - this.store.updateState({ web3ShimUsageOrigins }); - } -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 142ae80d09c1..f7832f5893ec 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -298,7 +298,7 @@ import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { isStreamWritable, setupMultiplex } from './lib/stream-utils'; import { PreferencesController } from './controllers/preferences-controller'; import AppStateController from './controllers/app-state'; -import AlertController from './controllers/alert'; +import { AlertController } from './controllers/alert-controller'; import OnboardingController from './controllers/onboarding'; import Backup from './lib/backup'; import DecryptMessageController from './controllers/decrypt-message'; @@ -1789,7 +1789,7 @@ export default class MetamaskController extends EventEmitter { }); this.alertController = new AlertController({ - initState: initState.AlertController, + state: initState.AlertController, controllerMessenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 17d68bd0e500..e21cc6b03a0c 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -4,7 +4,6 @@ "app/scripts/constants/contracts.js", "app/scripts/constants/on-ramp.js", "app/scripts/contentscript.js", - "app/scripts/controllers/alert.js", "app/scripts/controllers/app-state.js", "app/scripts/controllers/cached-balances.js", "app/scripts/controllers/cached-balances.test.js", From 327a2601569e40dcad1a99e76b5313a640d1dfd8 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 17 Oct 2024 17:36:21 +0200 Subject: [PATCH 28/51] =?UTF-8?q?fix:=20fix=20currency=20display=20when=20?= =?UTF-8?q?tokenToFiatConversion=20rate=20is=20not=20avai=E2=80=A6=20(#278?= =?UTF-8?q?93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** When the fiat conversion for a token is not available; we should show the token balance. The default behavior is working as expected; but when a user switches to a token where fiat conversions are available and clicks on the swap icon then goes back to the token with no conversions it will show USD when it should default to token value. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27893?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27805 ## **Manual testing steps** 1. click on send 2. select account to send to 3. select token to send, preferably one with poor pricing data (ETH: 0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009) 4. confirm that balance is undefined 5. select a token that has pricing data 6. use arrows on the right of the amount field to swap USD amount to token amount 7. reselect token to send in prior step 8. confirm that token balance now shows. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/1b97544b-adcd-424b-9da4-e360b7b94968 ### **After** https://github.com/user-attachments/assets/09cc9428-450a-47a8-a111-414c5996149c ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../swappable-currency-input/swappable-currency-input.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx b/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx index ff587fe6c1c1..6d9820b67c8c 100644 --- a/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx +++ b/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.tsx @@ -19,6 +19,7 @@ import { import CurrencyInput from '../../../app/currency-input'; import { getIsFiatPrimary } from '../utils'; import { NFTInput } from '../nft-input/nft-input'; +import useTokenExchangeRate from '../../../app/currency-input/hooks/useTokenExchangeRate'; import SwapIcon from './swap-icon'; type BaseProps = { @@ -81,12 +82,17 @@ export function SwappableCurrencyInput({ const t = useI18nContext(); const isFiatPrimary = useSelector(getIsFiatPrimary); + const tokenToFiatConversionRate = useTokenExchangeRate( + asset?.details?.address, + ); const isSetToMax = useSelector(getSendMaxModeState); const TokenComponent = ( ( From fac442247f0180eb05973611e5d35db2fb1d3c21 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 17 Oct 2024 21:50:30 +0530 Subject: [PATCH 29/51] feat: NFT permit simulations (#27825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add simulation section to NFT permit ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27394 ## **Manual testing steps** 1. Submit NFT permit signature request 2. Check simulation section on the confirmation page ## **Screenshots/Recordings** Screenshot 2024-10-14 at 5 40 21 PM ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../permit-simulation.test.tsx.snap | 117 ++++++++++++++++++ .../permit-simulation.test.tsx | 21 +++- .../permit-simulation/permit-simulation.tsx | 11 +- .../__snapshots__/value-display.test.tsx.snap | 46 +++++++ .../value-display/value-display.test.tsx | 17 +++ .../value-display/value-display.tsx | 35 +++--- 6 files changed, 230 insertions(+), 17 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap index 7c5553495eb0..f35e2218cbfb 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/__snapshots__/permit-simulation.test.tsx.snap @@ -127,3 +127,120 @@ exports[`PermitSimulation renders component correctly 1`] = `
    `; + +exports[`PermitSimulation renders correctly for NFT permit 1`] = ` +
    +
    +
    +
    +
    +

    + Estimated changes +

    +
    +
    + +
    +
    +
    +
    +
    +

    + You're giving someone else permission to withdraw NFTs from your account. +

    +
    +
    +
    +
    +
    +

    + Withdraw +

    +
    +
    +
    +
    +
    +
    +
    +

    + #3606393 +

    +
    +
    +
    +
    + +

    + 0xC3644...1FE88 +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx index 1be34109a637..e89efb3c0dc1 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx @@ -4,7 +4,10 @@ import { act } from 'react-dom/test-utils'; import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; -import { permitSignatureMsg } from '../../../../../../../../test/data/confirmations/typed_sign'; +import { + permitNFTSignatureMsg, + permitSignatureMsg, +} from '../../../../../../../../test/data/confirmations/typed_sign'; import PermitSimulation from './permit-simulation'; jest.mock('../../../../../../../store/actions', () => { @@ -28,4 +31,20 @@ describe('PermitSimulation', () => { expect(container).toMatchSnapshot(); }); }); + + it('renders correctly for NFT permit', async () => { + const state = getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg); + const mockStore = configureMockStore([])(state); + + await act(async () => { + const { container, findByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(await findByText('Withdraw')).toBeInTheDocument(); + expect(await findByText('#3606393')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx index 231997d18547..44131ec18fbf 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx @@ -45,8 +45,10 @@ const PermitSimulation: React.FC = () => { const { domain: { verifyingContract }, message, + message: { tokenId }, primaryType, } = parseTypedDataMessage(msgData as string); + const isNFT = tokenId !== undefined; const tokenDetails = extractTokenDetailsByPrimaryType(message, primaryType); @@ -68,7 +70,9 @@ const PermitSimulation: React.FC = () => { ); const SpendingCapRow = ( - + {Array.isArray(tokenDetails) ? ( = () => { )} @@ -99,7 +104,9 @@ const PermitSimulation: React.FC = () => { ); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap index 26def806c6fa..9c4134aa1b2d 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap @@ -56,3 +56,49 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = ` `; + +exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = ` +
    +
    +
    +
    +
    +

    + #4321 +

    +
    +
    +
    +
    + +

    + 0xA0b86...6eB48 +

    +
    +
    +
    +
    +
    +
    +`; diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx index f6af7357502d..da86d497aac1 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx @@ -29,4 +29,21 @@ describe('PermitSimulationValueDisplay', () => { expect(container).toMatchSnapshot(); }); }); + + it('renders component correctly for NFT token', async () => { + const mockStore = configureMockStore([])(mockState); + + await act(async () => { + const { container, findByText } = renderWithProvider( + , + mockStore, + ); + + expect(await findByText('#4321')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx index 633191cd2638..360559493596 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx @@ -41,21 +41,26 @@ type PermitSimulationValueDisplayParams = { tokenContract: Hex | string; /** The token amount */ - value: number | string; + value?: number | string; + + /** The tokenId for NFT */ + tokenId?: string; }; const PermitSimulationValueDisplay: React.FC< PermitSimulationValueDisplayParams -> = ({ primaryType, tokenContract, value }) => { +> = ({ primaryType, tokenContract, value, tokenId }) => { const exchangeRate = useTokenExchangeRate(tokenContract); - const { value: tokenDecimals } = useAsyncResult( - async () => await fetchErc20Decimals(tokenContract), - [tokenContract], - ); + const { value: tokenDecimals } = useAsyncResult(async () => { + if (tokenId) { + return undefined; + } + return await fetchErc20Decimals(tokenContract); + }, [tokenContract]); const fiatValue = useMemo(() => { - if (exchangeRate && value) { + if (exchangeRate && value && !tokenId) { const tokenAmount = calcTokenAmount(value, tokenDecimals); return exchangeRate.times(tokenAmount).toNumber(); } @@ -63,7 +68,7 @@ const PermitSimulationValueDisplay: React.FC< }, [exchangeRate, tokenDecimals, value]); const { tokenValue, tokenValueMaxPrecision } = useMemo(() => { - if (!value) { + if (!value || tokenId) { return { tokenValue: null, tokenValueMaxPrecision: null }; } @@ -107,12 +112,14 @@ const PermitSimulationValueDisplay: React.FC< style={{ paddingTop: '1px', paddingBottom: '1px' }} textAlign={TextAlign.Center} > - {shortenString(tokenValue || '', { - truncatedCharLimit: 15, - truncatedStartChars: 15, - truncatedEndChars: 0, - skipCharacterInEnd: true, - })} + {tokenValue !== null && + shortenString(tokenValue || '', { + truncatedCharLimit: 15, + truncatedStartChars: 15, + truncatedEndChars: 0, + skipCharacterInEnd: true, + })} + {tokenId && `#${tokenId}`} From 654dff7f918bb32a5051443609b424b2559080b2 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 17 Oct 2024 19:43:39 +0200 Subject: [PATCH 30/51] fix: add APE network icon (#27841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds APE network icon [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27841?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/26874 ## **Manual testing steps** 1. Click on network picker and click on add custom network 2. ADD name "APE"; RPC: https://curtis.rpc.caldera.xyz/http and chainId 33111 3. Click on add network 4. You should see network icon ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/89abe1e2-49e4-46ba-ada5-953fa99aa9c0 ### **After** https://github.com/user-attachments/assets/d47f9b55-f5c1-4d07-a025-34c8008eef5f ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/images/ape.svg | 1658 +++++++++++++++++++++++++++++++++++ shared/constants/network.ts | 3 + 2 files changed, 1661 insertions(+) create mode 100644 app/images/ape.svg diff --git a/app/images/ape.svg b/app/images/ape.svg new file mode 100644 index 000000000000..495a73676a5e --- /dev/null +++ b/app/images/ape.svg @@ -0,0 +1,1658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 9ed2e26150a9..e911ce1aabf5 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -146,6 +146,7 @@ export const CHAIN_IDS = { CHZ: '0x15b38', NUMBERS: '0x290b', SEI: '0x531', + APE_TESTNET: '0x8157', BERACHAIN: '0x138d5', METACHAIN_ONE: '0x1b6e6', ARBITRUM_SEPOLIA: '0x66eee', @@ -448,6 +449,7 @@ export const NUMBERS_MAINNET_IMAGE_URL = './images/numbers-mainnet.svg'; export const NUMBERS_TOKEN_IMAGE_URL = './images/numbers-token.png'; export const SEI_IMAGE_URL = './images/sei.svg'; export const NEAR_IMAGE_URL = './images/near.svg'; +export const APE_TESTNET_IMAGE_URL = './images/ape.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -780,6 +782,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.ZKATANA]: ZKATANA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ZORA_MAINNET]: ZORA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.FILECOIN]: FILECOIN_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.APE_TESTNET]: APE_TESTNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.BASE]: BASE_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.NUMBERS]: NUMBERS_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.SEI]: SEI_IMAGE_URL, From 9716e949259d7c89cb99f688aef670b8d7af4e73 Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:54:23 +0200 Subject: [PATCH 31/51] fix: bump `@metamask/ppom-validator` from `0.34.0` to `0.35.1` (#27939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/ppom-validator ` from `0.34.0` to `0.35.1` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27939?quickstart=1) ## **Related issues** Fixes: #27909 ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- lavamoat/browserify/beta/policy.json | 67 +-------------------------- lavamoat/browserify/flask/policy.json | 67 +-------------------------- lavamoat/browserify/main/policy.json | 67 +-------------------------- lavamoat/browserify/mmi/policy.json | 67 +-------------------------- package.json | 2 +- yarn.lock | 35 ++++---------- 6 files changed, 19 insertions(+), 286 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 880b542673ea..27b06f2ba5b8 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2028,78 +2028,15 @@ "crypto": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, "@metamask/eth-query>json-rpc-random-id": true, - "@metamask/ppom-validator>@metamask/base-controller": true, - "@metamask/ppom-validator>@metamask/controller-utils": true, - "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, "await-semaphore": true, "browserify>buffer": true } }, - "@metamask/ppom-validator>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/ppom-validator>@metamask/controller-utils": { - "globals": { - "URL": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@spruceid/siwe-parser": true, - "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/ppom-validator>@metamask/utils": true, - "bn.js": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors": { - "packages": { - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/ppom-validator>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/ppom-validator>crypto-js": { "globals": { "crypto": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 880b542673ea..27b06f2ba5b8 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2028,78 +2028,15 @@ "crypto": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, "@metamask/eth-query>json-rpc-random-id": true, - "@metamask/ppom-validator>@metamask/base-controller": true, - "@metamask/ppom-validator>@metamask/controller-utils": true, - "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, "await-semaphore": true, "browserify>buffer": true } }, - "@metamask/ppom-validator>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/ppom-validator>@metamask/controller-utils": { - "globals": { - "URL": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@spruceid/siwe-parser": true, - "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/ppom-validator>@metamask/utils": true, - "bn.js": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors": { - "packages": { - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/ppom-validator>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/ppom-validator>crypto-js": { "globals": { "crypto": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 880b542673ea..27b06f2ba5b8 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2028,78 +2028,15 @@ "crypto": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, "@metamask/eth-query>json-rpc-random-id": true, - "@metamask/ppom-validator>@metamask/base-controller": true, - "@metamask/ppom-validator>@metamask/controller-utils": true, - "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, "await-semaphore": true, "browserify>buffer": true } }, - "@metamask/ppom-validator>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/ppom-validator>@metamask/controller-utils": { - "globals": { - "URL": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@spruceid/siwe-parser": true, - "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/ppom-validator>@metamask/utils": true, - "bn.js": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors": { - "packages": { - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/ppom-validator>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/ppom-validator>crypto-js": { "globals": { "crypto": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 25756f84ccc4..7c19b1b4c76e 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2120,78 +2120,15 @@ "crypto": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, "@metamask/eth-query>json-rpc-random-id": true, - "@metamask/ppom-validator>@metamask/base-controller": true, - "@metamask/ppom-validator>@metamask/controller-utils": true, - "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, "await-semaphore": true, "browserify>buffer": true } }, - "@metamask/ppom-validator>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/ppom-validator>@metamask/controller-utils": { - "globals": { - "URL": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@spruceid/siwe-parser": true, - "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/ppom-validator>@metamask/utils": true, - "bn.js": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors": { - "packages": { - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/ppom-validator>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/ppom-validator>crypto-js": { "globals": { "crypto": true, diff --git a/package.json b/package.json index 5709ea91a51c..3e51e31e1380 100644 --- a/package.json +++ b/package.json @@ -340,7 +340,7 @@ "@metamask/permission-log-controller": "^2.0.1", "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", - "@metamask/ppom-validator": "0.34.0", + "@metamask/ppom-validator": "0.35.1", "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", diff --git a/yarn.lock b/yarn.lock index 19e1f3bf1a8b..186f52706a3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5031,21 +5031,6 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^8.0.1": - version: 8.0.4 - resolution: "@metamask/controller-utils@npm:8.0.4" - dependencies: - "@ethereumjs/util": "npm:^8.1.0" - "@metamask/eth-query": "npm:^4.0.0" - "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/utils": "npm:^8.3.0" - "@spruceid/siwe-parser": "npm:1.1.3" - eth-ens-namehash: "npm:^2.0.8" - fast-deep-equal: "npm:^3.1.3" - checksum: 10/112a07614eec28cff270c99aa0695bec34cd29461d0c4cb83eb913a5bc37b3b72e4f33dad59a0ab23da5d1b091372ee5207657349bfdb814098c5a51d6570554 - languageName: node - linkType: hard - "@metamask/design-tokens@npm:^1.12.0": version: 1.13.0 resolution: "@metamask/design-tokens@npm:1.13.0" @@ -6039,21 +6024,21 @@ __metadata: languageName: node linkType: hard -"@metamask/ppom-validator@npm:0.34.0": - version: 0.34.0 - resolution: "@metamask/ppom-validator@npm:0.34.0" +"@metamask/ppom-validator@npm:0.35.1": + version: 0.35.1 + resolution: "@metamask/ppom-validator@npm:0.35.1" dependencies: - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/controller-utils": "npm:^8.0.1" - "@metamask/network-controller": "npm:^20.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^8.3.0" + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/utils": "npm:^9.2.1" await-semaphore: "npm:^0.1.3" crypto-js: "npm:^4.2.0" elliptic: "npm:^6.5.4" eslint-plugin-n: "npm:^16.6.2" json-rpc-random-id: "npm:^1.0.1" - checksum: 10/140b2070ddf4a9d7d13518ab1a10aa71961715434053096d0caa6f4ce104bbcaea5f5152edfa9b6c42f9bc929116afbb6a0542c1147e3101d04ef29bcf7a6c9f + peerDependencies: + "@metamask/network-controller": ^21.0.0 + checksum: 10/3dd37ced473a78e4b7847c61b6c6fb1e2ae4865dee67de9574462fd618dc5ea7be46874f12ff18383702c46c9c07c32dbac00be2e6ad26cb45a3dcc4ffa09ab7 languageName: node linkType: hard @@ -26163,7 +26148,7 @@ __metadata: "@metamask/phishing-controller": "npm:^12.0.1" "@metamask/phishing-warning": "npm:^4.0.0" "@metamask/post-message-stream": "npm:^8.0.0" - "@metamask/ppom-validator": "npm:0.34.0" + "@metamask/ppom-validator": "npm:0.35.1" "@metamask/preferences-controller": "npm:^13.0.2" "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" From 35b86fc0ad54ef09d08fe6d1ebe9448b28e9e417 Mon Sep 17 00:00:00 2001 From: David Drazic Date: Thu, 17 Oct 2024 23:04:01 +0200 Subject: [PATCH 32/51] fix: hide options menu that was being shown for preinstalled Snaps (#27937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR will hide Snaps `options menu` and `info icon` in Snaps header for **preinstalled Snaps**. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27937?quickstart=1) ## **Related issues** Fixes: n/a ## **Manual testing steps** 1. Go to Account watcher Snap and make sure that there is no button with three dots in Snaps header. 2. Go to ordinary Home Page Snap and make sure that three dots are available there. 3. Go to ordinary (non-preinstalled) custom UI Snap with confirmation popup of any type and make sure that there is Info icon in the right corner of the header. ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/user-attachments/assets/8cf66b7e-6719-44ea-a976-da7b0a94eb4e) ### **After** ![Screenshot 2024-10-17 at 17 44 34](https://github.com/user-attachments/assets/c4014f2d-90ac-4ca1-bd92-095c2a859084) ![Screenshot 2024-10-17 at 17 45 26](https://github.com/user-attachments/assets/08e4fc79-ae24-43bd-bdf1-5b6d7883a075) ![Screenshot 2024-10-17 at 17 46 03](https://github.com/user-attachments/assets/43c9ec79-cf0c-4e01-b0b6-c8a301af2c46) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../snap-authorship-header.js | 4 ++-- ui/pages/snaps/snap-view/snap-view.js | 14 ++++++++------ ui/selectors/selectors.js | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js index 0cb0a48dd2d6..ed835f6d241d 100644 --- a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js +++ b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js @@ -39,7 +39,7 @@ const SnapAuthorshipHeader = ({ const t = useI18nContext(); const [isModalOpen, setIsModalOpen] = useState(false); - const { name: snapName } = useSelector((state) => + const { name: snapName, hidden } = useSelector((state) => getSnapMetadata(state, snapId), ); @@ -105,7 +105,7 @@ const SnapAuthorshipHeader = ({ - {showInfo && ( + {showInfo && !hidden && ( + !snap.hidden && ( + + ) } /> )} diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 09c062012731..431d2c1b5d0a 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1612,6 +1612,7 @@ export const getSnapsMetadata = createDeepEqualSelector( snapsMetadata[snapId] = { name: manifest.proposedName, description: manifest.description, + hidden: snap.hidden, }; return snapsMetadata; }, {}); From 02f7ec4479ad06d1dc3618172fdc64d04f04ca85 Mon Sep 17 00:00:00 2001 From: Matteo Scurati Date: Fri, 18 Oct 2024 06:52:19 +0200 Subject: [PATCH 33/51] fix: bump message signing snap to support portfolio automatic connections (#27936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Bumps `@metamask/message-signing-snap` to `0.4.0` to allow portfolio to support automatic connections. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27306?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/NOTIFY-1136 ## **Manual testing steps** This does not effect anything within the wallet. ## **Screenshots/Recordings** ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/snaps/preinstalled-snaps.ts | 2 +- package.json | 4 +- yarn.lock | 61 +++++++++++++++---------- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts index f46681ddab57..c725a2cbd837 100644 --- a/app/scripts/snaps/preinstalled-snaps.ts +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -9,7 +9,7 @@ import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/pr // The casts here are less than ideal but we expect the SnapController to validate the inputs. const PREINSTALLED_SNAPS = Object.freeze([ - MessageSigningSnap as PreinstalledSnap, + MessageSigningSnap as unknown as PreinstalledSnap, EnsResolverSnap as PreinstalledSnap, ///: BEGIN:ONLY_INCLUDE_IF(build-flask) AccountWatcherSnap as PreinstalledSnap, diff --git a/package.json b/package.json index 3e51e31e1380..873e3ad6324c 100644 --- a/package.json +++ b/package.json @@ -328,7 +328,7 @@ "@metamask/logging-controller": "^6.0.0", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^10.1.0", - "@metamask/message-signing-snap": "^0.3.3", + "@metamask/message-signing-snap": "^0.4.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/name-controller": "^8.0.0", "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", @@ -349,7 +349,7 @@ "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", - "@metamask/selected-network-controller": "^18.0.1", + "@metamask/selected-network-controller": "^18.0.2", "@metamask/signature-controller": "^19.1.0", "@metamask/smart-transactions-controller": "^13.0.0", "@metamask/snaps-controllers": "^9.11.1", diff --git a/yarn.lock b/yarn.lock index 186f52706a3e..9f6ea0aa2fc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5581,6 +5581,17 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-engine@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/json-rpc-engine@npm:10.0.0" + dependencies: + "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^9.1.0" + checksum: 10/2c401a4a64392aeb11c4f7ca8d7b458ba1106cff1e0b3dba8b3e0cc90e82f8c55ac2dc9fdfcd914b289e3298fb726d637cf21382336dde2c207cf76129ce5eab + languageName: node + linkType: hard + "@metamask/json-rpc-engine@npm:^7.1.0, @metamask/json-rpc-engine@npm:^7.1.1, @metamask/json-rpc-engine@npm:^7.3.2": version: 7.3.3 resolution: "@metamask/json-rpc-engine@npm:7.3.3" @@ -5603,7 +5614,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2, @metamask/json-rpc-engine@npm:^9.0.3": +"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2": version: 9.0.3 resolution: "@metamask/json-rpc-engine@npm:9.0.3" dependencies: @@ -5712,18 +5723,18 @@ __metadata: languageName: node linkType: hard -"@metamask/message-signing-snap@npm:^0.3.3": - version: 0.3.3 - resolution: "@metamask/message-signing-snap@npm:0.3.3" +"@metamask/message-signing-snap@npm:^0.4.0": + version: 0.4.0 + resolution: "@metamask/message-signing-snap@npm:0.4.0" dependencies: - "@metamask/rpc-errors": "npm:^6.2.1" - "@metamask/snaps-sdk": "npm:^3.1.1" - "@metamask/utils": "npm:^8.3.0" - "@noble/ciphers": "npm:^0.5.1" - "@noble/curves": "npm:^1.4.0" + "@metamask/rpc-errors": "npm:^6.3.0" + "@metamask/snaps-sdk": "npm:^6.0.0" + "@metamask/utils": "npm:^9.0.0" + "@noble/ciphers": "npm:^0.5.3" + "@noble/curves": "npm:^1.4.2" "@noble/hashes": "npm:^1.4.0" - zod: "npm:^3.22.4" - checksum: 10/8290f9779e826965082ef1c18189e96502a51b9ed3ade486dab91a1bcf4af150ffb04207f620ba2b98b7b268efe107d4953ab64fed0932b66b87c72f98cc944e + zod: "npm:^3.23.8" + checksum: 10/fb61da8f2999305f99ad5a1d6be2def224c88c1059fcdc8e70d06641d695eef82d9b8463c6b57d797a519aa70dc741b7cb59596f503faf2eff68a1647248b4de languageName: node linkType: hard @@ -6154,7 +6165,7 @@ __metadata: languageName: node linkType: hard -"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1": +"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.0, @metamask/rpc-errors@npm:^6.3.1": version: 6.4.0 resolution: "@metamask/rpc-errors@npm:6.4.0" dependencies: @@ -6198,18 +6209,18 @@ __metadata: languageName: node linkType: hard -"@metamask/selected-network-controller@npm:^18.0.1": - version: 18.0.1 - resolution: "@metamask/selected-network-controller@npm:18.0.1" +"@metamask/selected-network-controller@npm:^18.0.2": + version: 18.0.2 + resolution: "@metamask/selected-network-controller@npm:18.0.2" dependencies: "@metamask/base-controller": "npm:^7.0.1" - "@metamask/json-rpc-engine": "npm:^9.0.3" + "@metamask/json-rpc-engine": "npm:^10.0.0" "@metamask/swappable-obj-proxy": "npm:^2.2.0" "@metamask/utils": "npm:^9.1.0" peerDependencies: "@metamask/network-controller": ^21.0.0 "@metamask/permission-controller": ^11.0.0 - checksum: 10/79a862f352a819185a7bcc87f380a03bcc929db125467fa7e2ec0fc06647899b611a8cafe6aac14f2a02622f704b77e29cc833ab465b8c233eeb0a37b9a1dffc + checksum: 10/cf46a1a7d4ca19d6327aeb5918b2e904933b3ae6959184a2d5773be294d1b0dbe4d16189c46bfcbd83f33d95fe0c6e5cb64e4745fa0c75243db4c8304ab6ec8e languageName: node linkType: hard @@ -6653,7 +6664,7 @@ __metadata: languageName: node linkType: hard -"@noble/ciphers@npm:^0.5.1, @noble/ciphers@npm:^0.5.2": +"@noble/ciphers@npm:^0.5.2, @noble/ciphers@npm:^0.5.3": version: 0.5.3 resolution: "@noble/ciphers@npm:0.5.3" checksum: 10/af0ad96b5807feace93e63549e05de6f5e305b36e2e95f02d90532893fbc3af3f19b9621b6de4caa98303659e5df2e7aa082064e5d4a82e6f38c728d48dfae5d @@ -6669,7 +6680,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.2.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": +"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.2.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": version: 1.4.2 resolution: "@noble/curves@npm:1.4.2" dependencies: @@ -26135,7 +26146,7 @@ __metadata: "@metamask/logging-controller": "npm:^6.0.0" "@metamask/logo": "npm:^3.1.2" "@metamask/message-manager": "npm:^10.1.0" - "@metamask/message-signing-snap": "npm:^0.3.3" + "@metamask/message-signing-snap": "npm:^0.4.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/name-controller": "npm:^8.0.0" "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch" @@ -26158,7 +26169,7 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" - "@metamask/selected-network-controller": "npm:^18.0.1" + "@metamask/selected-network-controller": "npm:^18.0.2" "@metamask/signature-controller": "npm:^19.1.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.11.1" @@ -37492,10 +37503,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4": - version: 3.22.4 - resolution: "zod@npm:3.22.4" - checksum: 10/73622ca36a916f785cf528fe612a884b3e0f183dbe6b33365a7d0fc92abdbedf7804c5e2bd8df0a278e1472106d46674281397a3dd800fa9031dc3429758c6ac +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 languageName: node linkType: hard From 1648b833971ae7c5679af0fde77603d54307fb89 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:30:22 +0200 Subject: [PATCH 34/51] test: [POM] Migrate contract interaction with snap account e2e tests to page object modal (#27924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the snap account contract interaction e2e tests to Page Object Model (POM) pattern, improving test stability and maintainability. Changes include: - Migrate test `snap-account-contract-interaction.spec.ts` to POM - Migrate test `snap-account-signatures-and-disconnects.spec.ts` to POM - Created related functions in TestDapp class - Avoid several delays in the original function implementation - Reduced flakiness [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: #27933 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chloe Gao --- test/e2e/accounts/common.ts | 400 ------------------ .../snap-account-contract-interaction.spec.ts | 90 ---- ...account-signatures-and-disconnects.spec.ts | 53 --- test/e2e/flask/btc/common-btc.ts | 23 +- test/e2e/flask/btc/create-btc-account.spec.ts | 3 +- test/e2e/page-objects/pages/test-dapp.ts | 109 ++++- .../snap-account-contract-interaction.spec.ts | 83 ++++ ...account-signatures-and-disconnects.spec.ts | 66 +++ .../account/snap-account-signatures.spec.ts | 1 + 9 files changed, 274 insertions(+), 554 deletions(-) delete mode 100644 test/e2e/accounts/common.ts delete mode 100644 test/e2e/accounts/snap-account-contract-interaction.spec.ts delete mode 100644 test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts create mode 100644 test/e2e/tests/account/snap-account-contract-interaction.spec.ts create mode 100644 test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts deleted file mode 100644 index 62f3fc082b53..000000000000 --- a/test/e2e/accounts/common.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { privateToAddress } from 'ethereumjs-util'; -import messages from '../../../app/_locales/en/messages.json'; -import FixtureBuilder from '../fixture-builder'; -import { - PRIVATE_KEY, - PRIVATE_KEY_TWO, - WINDOW_TITLES, - clickSignOnSignatureConfirmation, - switchToOrOpenDapp, - unlockWallet, - validateContractDetails, - multipleGanacheOptions, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; -import { retry } from '../../../development/lib/retry'; - -/** - * These are fixtures specific to Account Snap E2E tests: - * -- connected to Test Dapp - * -- two private keys with 25 ETH each - * - * @param title - */ -export const accountSnapFixtures = (title: string | undefined) => { - return { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp({ - restrictReturnedAccounts: false, - }) - .build(), - ganacheOptions: multipleGanacheOptions, - title, - }; -}; - -// convert PRIVATE_KEY to public key -export const PUBLIC_KEY = privateToAddress( - Buffer.from(PRIVATE_KEY.slice(2), 'hex'), -).toString('hex'); - -export async function installSnapSimpleKeyring( - driver: Driver, - isAsyncFlow: boolean, -) { - await unlockWallet(driver); - - // navigate to test Snaps page and connect - await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); - - await driver.clickElement('#connectButton'); - - await driver.delay(500); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.delay(500); - - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - - await driver.findElement({ text: 'Add to MetaMask', tag: 'h3' }); - - await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - - await driver.clickElementAndWaitForWindowToClose({ - text: 'OK', - tag: 'button', - }); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await driver.waitForSelector({ - text: 'Connected', - tag: 'span', - }); - - if (isAsyncFlow) { - await toggleAsyncFlow(driver); - } -} - -async function toggleAsyncFlow(driver: Driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await driver.clickElement('[data-testid="use-sync-flow-toggle"]'); -} - -export async function importKeyAndSwitch(driver: Driver) { - await driver.clickElement({ - text: 'Import account', - tag: 'div', - }); - - await driver.fill('#import-account-private-key', PRIVATE_KEY_TWO); - - await driver.clickElement({ - text: 'Import Account', - tag: 'button', - }); - - // Click "Create" on the Snap's confirmation popup - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Create', - }); - // Click the add account button on the naming modal - await driver.clickElement({ - css: '[data-testid="submit-add-account-with-name"]', - text: 'Add account', - }); - // Click the ok button on the success modal - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Ok', - }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await switchToAccount2(driver); -} - -export async function makeNewAccountAndSwitch(driver: Driver) { - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - // Click "Create" on the Snap's confirmation popup - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Create', - }); - // Click the add account button on the naming modal - await driver.clickElement({ - css: '[data-testid="submit-add-account-with-name"]', - text: 'Add account', - }); - // Click the ok button on the success modal - await driver.clickElementAndWaitForWindowToClose({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Ok', - }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - const newPublicKey = await ( - await driver.findElement({ - text: '0x', - tag: 'p', - }) - ).getText(); - - await switchToAccount2(driver); - - return newPublicKey; -} - -async function switchToAccount2(driver: Driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); - - // click on Accounts - await driver.clickElement('[data-testid="account-menu-icon"]'); - - await driver.clickElement({ - tag: 'Button', - text: 'SSK Account', - }); - - await driver.assertElementNotPresent({ - tag: 'header', - text: 'Select an account', - }); -} - -export async function connectAccountToTestDapp(driver: Driver) { - await switchToOrOpenDapp(driver); - await driver.clickElement('#connectButton'); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - // Extra steps needed to preserve the current network. - // Those can be removed once the issue is fixed (#27891) - const edit = await driver.findClickableElements({ - text: 'Edit', - tag: 'button', - }); - await edit[1].click(); - - await driver.clickElement({ - tag: 'p', - text: 'Localhost 8545', - }); - - await driver.clickElement({ - text: 'Update', - tag: 'button', - }); - - // Connect to the test dapp - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - - await driver.switchToWindowWithUrl(DAPP_URL); - // Ensure network is preserved after connecting - await driver.waitForSelector({ - css: '[id="chainId"]', - text: '0x539', - }); -} - -export async function disconnectFromTestDapp(driver: Driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); - await driver.clickElement('[data-testid="account-options-menu-button"]'); - await driver.clickElement({ text: 'All Permissions', tag: 'div' }); - await driver.clickElementAndWaitToDisappear({ - text: 'Got it', - tag: 'button', - }); - await driver.clickElement({ - text: '127.0.0.1:8080', - tag: 'p', - }); - await driver.clickElement({ text: 'Disconnect', tag: 'button' }); - await driver.clickElement('[data-testid ="disconnect-all"]'); -} - -export async function approveOrRejectRequest(driver: Driver, flowType: string) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await driver.clickElementUsingMouseMove({ - text: 'List requests', - tag: 'div', - }); - - await driver.clickElement({ - text: 'List Requests', - tag: 'button', - }); - - // get the JSON from the screen - const requestJSON = await ( - await driver.findElement({ - text: '"scope":', - tag: 'div', - }) - ).getText(); - - const requestID = JSON.parse(requestJSON)[0].id; - - if (flowType === 'approve') { - await driver.clickElementUsingMouseMove({ - text: 'Approve request', - tag: 'div', - }); - - await driver.fill('#approve-request-request-id', requestID); - - await driver.clickElement({ - text: 'Approve Request', - tag: 'button', - }); - } else if (flowType === 'reject') { - await driver.clickElementUsingMouseMove({ - text: 'Reject request', - tag: 'div', - }); - - await driver.fill('#reject-request-request-id', requestID); - - await driver.clickElement({ - text: 'Reject Request', - tag: 'button', - }); - } - - // Close the SnapSimpleKeyringDapp, so that 6 of the same tab doesn't pile up - await driver.closeWindow(); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); -} - -export async function signData( - driver: Driver, - locatorID: string, - newPublicKey: string, - flowType: string, -) { - const isAsyncFlow = flowType !== 'sync'; - - // This step can frequently fail, so retry it - await retry( - { - retries: 3, - delay: 2000, - }, - async () => { - await switchToOrOpenDapp(driver); - await driver.clickElement(locatorID); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - }, - ); - - // these three don't have a contract details page - if (!['#ethSign', '#personalSign', '#signTypedData'].includes(locatorID)) { - await validateContractDetails(driver); - } - - await clickSignOnSignatureConfirmation({ driver }); - - if (isAsyncFlow) { - await driver.delay(2000); - - // This step can frequently fail, so retry it - await retry( - { - retries: 3, - delay: 1000, - }, - async () => { - // Navigate to the Notification window and click 'Go to site' button - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Go to site', - tag: 'button', - }); - }, - ); - - await driver.delay(1000); - await approveOrRejectRequest(driver, flowType); - } - - await driver.delay(500); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - if (flowType === 'sync' || flowType === 'approve') { - if (locatorID === '#ethSign') { - // there is no Verify button for #ethSign - await driver.findElement({ - css: '#ethSignResult', - text: '0x', // we are just making sure that it contains ANY hex value - }); - } else { - await driver.clickElement(`${locatorID}Verify`); - - const resultLocator = - locatorID === '#personalSign' - ? '#personalSignVerifyECRecoverResult' // the verify span IDs are different with Personal Sign - : `${locatorID}VerifyResult`; - - await driver.findElement({ - css: resultLocator, - text: newPublicKey.toLowerCase(), - }); - } - } else if (flowType === 'reject') { - // ensure the transaction was rejected by the Snap - await driver.findElement({ - text: 'Error: Request rejected by user or snap.', - }); - } -} - -export async function createBtcAccount(driver: Driver) { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement({ - text: messages.addNewBitcoinAccount.message, - tag: 'button', - }); - await driver.clickElementAndWaitToDisappear( - { - text: 'Add account', - tag: 'button', - }, - // Longer timeout than usual, this reduces the flakiness - // around Bitcoin account creation (mainly required for - // Firefox) - 5000, - ); -} diff --git a/test/e2e/accounts/snap-account-contract-interaction.spec.ts b/test/e2e/accounts/snap-account-contract-interaction.spec.ts deleted file mode 100644 index 885e048272d8..000000000000 --- a/test/e2e/accounts/snap-account-contract-interaction.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import GanacheContractAddressRegistry from '../seeder/ganache-contract-address-registry'; -import { scrollAndConfirmAndAssertConfirm } from '../tests/confirmations/helpers'; -import { - createDepositTransaction, - TestSuiteArguments, -} from '../tests/confirmations/transactions/shared'; -import { - multipleGanacheOptionsForType2Transactions, - withFixtures, - openDapp, - WINDOW_TITLES, - locateAccountBalanceDOM, - clickNestedButton, - ACCOUNT_2, -} from '../helpers'; -import FixtureBuilder from '../fixture-builder'; -import { installSnapSimpleKeyring } from '../page-objects/flows/snap-simple-keyring.flow'; -import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; -import { SMART_CONTRACTS } from '../seeder/smart-contracts'; -import { importKeyAndSwitch } from './common'; - -describe('Snap Account Contract interaction', function () { - const smartContract = SMART_CONTRACTS.PIGGYBANK; - - it('deposits to piggybank contract', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerSnapAccountConnectedToTestDapp() - .withPreferencesController({ - preferences: { - redesignedConfirmationsEnabled: true, - isRedesignedConfirmationsDeveloperEnabled: true, - }, - }) - .build(), - ganacheOptions: multipleGanacheOptionsForType2Transactions, - smartContract, - title: this.test?.fullTitle(), - }, - async ({ - driver, - contractRegistry, - ganacheServer, - }: TestSuiteArguments) => { - // Install Snap Simple Keyring and import key - await loginWithBalanceValidation(driver, ganacheServer); - await installSnapSimpleKeyring(driver); - await importKeyAndSwitch(driver); - - // Open DApp with contract - const contractAddress = await ( - contractRegistry as GanacheContractAddressRegistry - ).getContractAddress(smartContract); - await openDapp(driver, contractAddress); - - // Create and confirm deposit transaction - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await createDepositTransaction(driver); - await driver.waitUntilXWindowHandles(4); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.waitForSelector({ - css: 'h2', - text: 'Transaction request', - }); - await scrollAndConfirmAndAssertConfirm(driver); - - // Confirm the transaction activity - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await clickNestedButton(driver, 'Activity'); - await driver.waitForSelector( - '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', - ); - await driver.waitForSelector({ - css: '[data-testid="transaction-list-item-primary-currency"]', - text: '-4 ETH', - }); - - // renders the correct ETH balance - await locateAccountBalanceDOM(driver, ganacheServer, ACCOUNT_2); - }, - ); - }); -}); diff --git a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts deleted file mode 100644 index 24e996671da9..000000000000 --- a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Suite } from 'mocha'; -import FixtureBuilder from '../fixture-builder'; -import { - withFixtures, - multipleGanacheOptions, - tempToggleSettingRedesignedConfirmations, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - installSnapSimpleKeyring, - makeNewAccountAndSwitch, - connectAccountToTestDapp, - disconnectFromTestDapp, - signData, -} from './common'; - -describe('Snap Account Signatures and Disconnects', function (this: Suite) { - it('can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - ganacheOptions: multipleGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - const flowType = 'approve'; - const isAsyncFlow = true; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - - const newPublicKey = await makeNewAccountAndSwitch(driver); - - await tempToggleSettingRedesignedConfirmations(driver); - - // open the Test Dapp and connect Account 2 to it - await connectAccountToTestDapp(driver); - - // do #signTypedDataV3 - await signData(driver, '#signTypedDataV3', newPublicKey, flowType); - - // disconnect from the Test Dapp - await disconnectFromTestDapp(driver); - - // reconnect Account 2 to the Test Dapp - await connectAccountToTestDapp(driver); - - // do #signTypedDataV4 - await signData(driver, '#signTypedDataV4', newPublicKey, flowType); - }, - ); - }); -}); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index 15bf7d49eb0b..6891b3bfd60e 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -4,7 +4,28 @@ import { withFixtures, unlockWallet } from '../../helpers'; import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { Driver } from '../../webdriver/driver'; -import { createBtcAccount } from '../../accounts/common'; +import messages from '../../../../app/_locales/en/messages.json'; + +export async function createBtcAccount(driver: Driver) { + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement({ + text: messages.addNewBitcoinAccount.message, + tag: 'button', + }); + await driver.clickElementAndWaitToDisappear( + { + text: 'Add account', + tag: 'button', + }, + // Longer timeout than usual, this reduces the flakiness + // around Bitcoin account creation (mainly required for + // Firefox) + 5000, + ); +} export async function mockBtcBalanceQuote( mockServer: Mockttp, diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index a4ac650f8f78..1b10599bf5ca 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -10,8 +10,7 @@ import { removeSelectedAccount, tapAndHoldToRevealSRP, } from '../../helpers'; -import { createBtcAccount } from '../../accounts/common'; -import { withBtcAccountSnap } from './common-btc'; +import { createBtcAccount, withBtcAccountSnap } from './common-btc'; describe('Create BTC Account', function (this: Suite) { it('create BTC account from the menu', async function () { diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index c0f71f1b3280..0940225b5e3b 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -1,6 +1,5 @@ import { WINDOW_TITLES } from '../../helpers'; import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; @@ -8,22 +7,55 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; class TestDapp { private driver: Driver; + private readonly confirmDepositButton = + '[data-testid="confirm-footer-button"]'; + + private readonly confirmDialogButton = '[data-testid="confirm-btn"]'; + private readonly confirmDialogScrollButton = '[data-testid="signature-request-scroll-button"]'; private readonly confirmSignatureButton = '[data-testid="page-container-footer-next"]'; + private readonly connectAccountButton = '#connectButton'; + + private readonly connectMetaMaskMessage = { + text: 'Connect with MetaMask', + tag: 'h2', + }; + + private readonly connectedAccount = '#accounts'; + + private readonly depositPiggyBankContractButton = '#depositButton'; + + private readonly editConnectButton = { + text: 'Edit', + tag: 'button', + }; + private readonly erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; private readonly erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; + private readonly erc20WatchAssetButton = '#watchAssets'; + private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton'; private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton'; + private readonly localhostCheckbox = { + text: 'Localhost 8545', + tag: 'p', + }; + + private readonly localhostNetworkMessage = { + css: '#chainId', + text: '0x539', + }; + private readonly mmlogo = '#mm-logo'; private readonly personalSignButton = '#personalSign'; @@ -37,6 +69,8 @@ class TestDapp { private readonly personalSignVerifyButton = '#personalSignVerify'; + private readonly revokePermissionButton = '#revokeAccountsPermission'; + private readonly signPermitButton = '#signPermit'; private readonly signPermitResult = '#signPermitResult'; @@ -84,16 +118,18 @@ class TestDapp { private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult'; - private erc20WatchAssetButton: RawLocator; + private readonly transactionRequestMessage = { + text: 'Transaction request', + tag: 'h2', + }; + + private readonly updateNetworkButton = { + text: 'Update', + tag: 'button', + }; constructor(driver: Driver) { this.driver = driver; - - this.erc721SetApprovalForAllButton = '#setApprovalForAllButton'; - this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; - this.erc721RevokeSetApprovalForAllButton = '#revokeButton'; - this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; - this.erc20WatchAssetButton = '#watchAssets'; } async check_pageIsLoaded(): Promise { @@ -156,6 +192,63 @@ class TestDapp { await this.driver.clickElement(this.erc20WatchAssetButton); } + /** + * Connect account to test dapp. + * + * @param publicAddress - The public address to connect to test dapp. + */ + async connectAccount(publicAddress: string) { + console.log('Connect account to test dapp'); + await this.driver.clickElement(this.connectAccountButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.connectMetaMaskMessage); + + // TODO: Extra steps needed to preserve the current network. + // Following steps can be removed once the issue is fixed (#27891) + const editNetworkButton = await this.driver.findClickableElements( + this.editConnectButton, + ); + await editNetworkButton[1].click(); + await this.driver.clickElement(this.localhostCheckbox); + await this.driver.clickElement(this.updateNetworkButton); + + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmDialogButton, + ); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.connectedAccount, + text: publicAddress.toLowerCase(), + }); + await this.driver.waitForSelector(this.localhostNetworkMessage); + } + + async createDepositTransaction() { + console.log('Create a deposit transaction on test dapp page'); + await this.driver.clickElement(this.depositPiggyBankContractButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.transactionRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmDepositButton, + ); + } + + /** + * Disconnect current connected account from test dapp. + * + * @param publicAddress - The public address of the account to disconnect from test dapp. + */ + async disconnectAccount(publicAddress: string) { + console.log('Disconnect account from test dapp'); + await this.driver.clickElement(this.revokePermissionButton); + await this.driver.refresh(); + await this.check_pageIsLoaded(); + await this.driver.assertElementNotPresent({ + css: this.connectedAccount, + text: publicAddress.toLowerCase(), + }); + } + /** * Verify the failed personal sign signature. * diff --git a/test/e2e/tests/account/snap-account-contract-interaction.spec.ts b/test/e2e/tests/account/snap-account-contract-interaction.spec.ts new file mode 100644 index 000000000000..e4753f5ff05b --- /dev/null +++ b/test/e2e/tests/account/snap-account-contract-interaction.spec.ts @@ -0,0 +1,83 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { Ganache } from '../../seeder/ganache'; +import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry'; +import { + multipleGanacheOptionsForType2Transactions, + PRIVATE_KEY_TWO, + withFixtures, + WINDOW_TITLES, +} from '../../helpers'; +import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Snap Account Contract interaction @no-mmi', function (this: Suite) { + const smartContract = SMART_CONTRACTS.PIGGYBANK; + it('deposits to piggybank contract', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerSnapAccountConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: multipleGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ + driver, + contractRegistry, + ganacheServer, + }: { + driver: Driver; + contractRegistry: GanacheContractAddressRegistry; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // Import snap account with private key on snap simple keyring page. + await snapSimpleKeyringPage.importAccountWithPrivateKey( + PRIVATE_KEY_TWO, + ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Open Dapp with contract + const testDapp = new TestDapp(driver); + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + await testDapp.openTestDappPage({ contractAddress }); + await testDapp.check_pageIsLoaded(); + await testDapp.createDepositTransaction(); + + // Confirm the transaction in activity list on MetaMask + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.goToActivityList(); + await homePage.check_confirmedTxNumberDisplayedInActivity(); + await homePage.check_txAmountInActivity('-4 ETH'); + }, + ); + }); +}); diff --git a/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts new file mode 100644 index 000000000000..7398747671c7 --- /dev/null +++ b/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts @@ -0,0 +1,66 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES, withFixtures } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import ExperimentalSettings from '../../page-objects/pages/experimental-settings'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + signTypedDataV3WithSnapAccount, + signTypedDataV4WithSnapAccount, +} from '../../page-objects/flows/sign.flow'; + +describe('Snap Account Signatures and Disconnects @no-mmi', function (this: Suite) { + it('can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp({ + restrictReturnedAccounts: false, + }) + .build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver, false); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const newPublicKey = await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to experimental settings and disable redesigned signature. + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleRedesignedSignature(); + + // Open the Test Dapp and signTypedDataV3 + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage(); + await signTypedDataV3WithSnapAccount(driver, newPublicKey, false, true); + + // Disconnect from Test Dapp and reconnect to Test Dapp + await testDapp.disconnectAccount(newPublicKey); + await testDapp.connectAccount(newPublicKey); + + // SignTypedDataV4 with Test Dapp + await signTypedDataV4WithSnapAccount(driver, newPublicKey, false, true); + }, + ); + }); +}); diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts index f5010fb61269..fd2fe013c3c1 100644 --- a/test/e2e/tests/account/snap-account-signatures.spec.ts +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -18,6 +18,7 @@ import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring- import TestDapp from '../../page-objects/pages/test-dapp'; describe('Snap Account Signatures @no-mmi', function (this: Suite) { + this.timeout(120000); // This test is very long, so we need an unusually high timeout // Run sync, async approve, and async reject flows // (in Jest we could do this with test.each, but that does not exist here) From ce8eeb1818acaffd4425ce2ddd7c93fbaf07e007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Tavares?= Date: Fri, 18 Oct 2024 11:25:06 +0100 Subject: [PATCH 35/51] chore: add testing-library/dom dependency (#27493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We are adding [testing-library/dom](https://www.npmjs.com/package/@testing-library/dom) as direct dependency in order to start leveraging the testing library [testing playground](https://testing-playground.com/). (As a note, latest versions of `testing-library/react` have `testing-library/dom` as a peer dep instead of a dependency. Though we are not updating `testing-library/react` to the latest version as it requires React 18 and we are still in React 16). Using the `screen` from the `dom` library we can now use `screen.logTestingPlaygroundURL()` (with or without a specific section passed into it) and when running the test it will log an url in the console for that specific component in the testing playground. We can use that to validate what is getting rendered and what is the suggested query to infer and write expectations on certain elements/text/values. Example of a logged url: https://testing-playground.com/#markup=DwEwlgbgBAxgNgQwM5ILwCIC2mC0AjAewA9YCA7AMzACdcxKCdqCB3KbfYnHTBagc3o4ALgQAOOAEztchIt14ChhYaNzSOc7mIQhwZfkzD8AFsKkzO8nDr31DcAKYVzG2VxzgkYxAE8cFE4kmh6BjvLg1I4wwmDkTKyWWgFBOCzUCBLpmUkeAFYArkixFP4w5MKOZObeCDCO+I7CLI5VudblcATUIuHmIM4IBXDC7dzMBWQDIDhw-OhQxb5OGAQQjtSBrGkZYgBcUAhkviwmG44A3OxCZ8ZmB5IALGJEV2IESGCx5AdRiLHrC7oAB8oEgsEQKAwIWC7msXh8CH8YVhVm4KM8NGi3zICTYMO4hWKYFKOHK1Sq5nqFOoYxwCDgxlxX0cmCQKXCOGKfGEILB0HgyDQWDhdIRfg5qOSDKZOBZbLJlI2fLEEKF0NFHEqRFGWr63EIIH8mBmmEc4AKmDpnW6crIZ2oXxBAFFibxKiBYCYjvxHEhgAB6MSg8AQEPgwVQ9ALYQIPD0AZEDAABgWSxW6HFSIOKKBUBACFjIgIBBGYDEYnNGAWfDACE8fpgjrw5rwvgwsQr-lEpc7OAAjKn84X691jPQGSIvhnXbF3eavT6-YcolBToXrqZRt6K20SVBfAQClB+AQoMITBNTGuviZzyYwEhzxkyEg6jiAHRQAAqD6fj6gIlRgQKAxCicAYjiMgABooDIAhgJPAo+COSpHA-PlajINUoxhSwwHJfDyW4T4AC8GiQK0CR4PhBFxJwXAHMVH0Rfx6EZMgGjwLoYAAa2tUtbQI+JMAKD002EZZHGhZBeLlXhfQOApqDgAAKAByD8AzABS-W08kkADABHAo-U-JAIH4dSAEogVBAMsPs0MnMgFyw0DZz+Rw4U8OorNkSCOl+EyCxqIxbIsl2OkZX4ZlKgValKlpajMCEFgwBAC8cFTUFVUjHzNVwbVdSK-VOCNHhPWom0enoB0nUWSSM1OFkuR0eoDjAhoIrsgBNI91NXQQIHsRYCDNchHCgRw4CQKbK1oR9PnIc8z28KpPQvACEEwI9qigChmCtQ9lMOGBykmYQP0DYMPNcu73NDbyNSsUhKBoOgGDxOlFDo4sJDcNEaKUXEVDUUK4W0XR9EMR0twhoHbBh2ZnFcZjvAlFE6QxSJsSg76wtSCKdhyaigJJMoKkpNq6i4poWjaarBJ6YqGwoIYRjpCYpnNWZ5kaqTVnWTYuhYEn9kOY5TnOK5UtxW4tweZ5XlAj4vig34ZsLSBLj5J78peuR0dYyVsdSXHIPiZh8UhnBydJclKmqRUaWixlYrleL2QxblqF5cMBUhArXr8ljMcC6iYri1l2US5VcuekUQ9KnVLFZ8rjVNc0wEtASulq+0NidYEAGVKymUaYEyG63NrryDaTuQJMFrBaKEBjhAOIZRFlhB5HSzKTAOQdkwAUjs+ug8N4hjfD8I3dleVY6Vag9YjKfG5nlK2+ZMgOIaDamNDjGkTtfefrSjKspy4A8o33yU5K3odQNAgKpNNOytZqOXaSukkfsGfegDRAbSndtHBKK8862lZgMdmwwn7JDwHUXi-BuYzBqvgFBaC9ozAZElMg2t1hc1wbzIgcAFgFiLJUYkIAMCfFEv8fGoheJVBwBABkpkQT9hrg9AOAdE6ELNHBHajgAD6YjUooHsJhHQ2EG54SEeIsRwlsIcFUUReIXIwDkUqrPU+7FgH4B4vxJm+c7T1V5ALDMvAkByR0ggRSUBlJqU0tpXShlVGGRMmZKCH4LJWVsnyByciE4KMKs-UYSiJEcLgKZT+L8M56LMTA-UcCOb+2TEQZMAB2AAbP2R4H5ikAGZdB5IABy8IDJ5Gp906nuQabXIAA [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27493?quickstart=1) ## **Related issues** Fixes: None ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .depcheckrc.yml | 1 + package.json | 1 + .../__snapshots__/nft-details.test.js.snap | 180 ----- .../__snapshots__/nft-full-image.test.js.snap | 2 - .../connect-accounts-modal.test.tsx.snap | 8 - .../confirm-legacy-gas-display.test.js.snap | 2 - .../base-transaction-info.test.tsx.snap | 747 ------------------ .../create-named-snap-account.test.js.snap | 100 --- .../switch-ethereum-chain.test.js.snap | 2 - ...ctive-replacement-token-page.test.tsx.snap | 54 -- .../confirm-recovery-phrase.test.js | 14 +- yarn.lock | 76 +- 12 files changed, 50 insertions(+), 1137 deletions(-) diff --git a/.depcheckrc.yml b/.depcheckrc.yml index bafacc56c918..d0d6eac5b5bc 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -80,6 +80,7 @@ ignores: - '@babel/plugin-transform-logical-assignment-operators' # trezor - 'ts-mixer' + - '@testing-library/dom' # files depcheck should not parse ignorePatterns: diff --git a/package.json b/package.json index 873e3ad6324c..3c1ea2bfbbba 100644 --- a/package.json +++ b/package.json @@ -506,6 +506,7 @@ "@storybook/test-runner": "^0.14.1", "@storybook/theming": "^7.6.20", "@swc/helpers": "^0.5.7", + "@testing-library/dom": "^7.31.2", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^10.4.8", "@testing-library/react-hooks": "^8.0.1", diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap index 22c5342d2026..d6b0de0c043e 100644 --- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap +++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap @@ -182,183 +182,3 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = `
    `; - -exports[`NFT Details should match minimal props and state snapshot 2`] = ` -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    -
    -
    -

    - MUNK #1 -

    -
    -
    -

    -

    -
    -
    -

    - Contract address -

    -
    - - -
    -
    -
    -

    - Token ID -

    -

    - 1 -

    -
    -
    -

    - Token standard -

    -

    - ERC721 -

    -
    -
    - -
    -
    -
    - Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted. -
    -
    -
    -
    -
    -
    -
    -`; - -exports[`NFT Details should match minimal props and state snapshot 3`] = `
    `; diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap index 086754f9b489..7b4d6b11abc6 100644 --- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap +++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap @@ -80,5 +80,3 @@ exports[`NFT full image should match snapshot 1`] = `
    `; - -exports[`NFT full image should match snapshot 2`] = `
    `; diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap index caf13117e1db..d53c8e7d8d8a 100644 --- a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap +++ b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap @@ -442,11 +442,3 @@ exports[`Connect More Accounts Modal should render correctly 1`] = `
    `; - -exports[`Connect More Accounts Modal should render correctly 2`] = ` - -
    - -`; diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap index b586f2d5cd95..f6e40da8118c 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap @@ -115,5 +115,3 @@ exports[`ConfirmLegacyGasDisplay should match snapshot 1`] = `
    `; - -exports[`ConfirmLegacyGasDisplay should match snapshot 2`] = `
    `; diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap index 5f0370343f00..f88485e985b3 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap @@ -338,750 +338,3 @@ exports[` renders component for contract interaction requ
    `; - -exports[` renders component for contract interaction request 2`] = ` -
    -
    -
    -
    -
    -

    - Estimated changes -

    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -

    - You send -

    -
    -
    -
    -
    -
    -

    - - 4 -

    -
    -
    -
    -
    -
    -
    - ETH logo -
    -

    - ETH -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Request from -

    -
    -
    - -
    -
    -
    -
    -
    -

    - metamask.github.io -

    -
    -
    -
    -
    -
    -

    - Interacting with -

    -
    -
    - -
    -
    -
    -
    -
    -
    - -

    - 0x88AA6...A5125 -

    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Network fee -

    -
    -
    - -
    -
    -
    -
    -
    -

    - 0.0001 ETH -

    -

    - $0.04 -

    - -
    -
    -
    -
    -
    -

    - Speed -

    -
    -
    -
    -
    -

    - 🦊 Market -

    -

    - - ~ - 0 sec - -

    -
    -
    -
    -
    -
    -`; - -exports[` renders component for contract interaction request 3`] = ` -
    -
    -
    -
    -
    -

    - Estimated changes -

    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -

    - You send -

    -
    -
    -
    -
    -
    -

    - - 4 -

    -
    -
    -
    -
    -
    -
    - E -
    -

    - ETH -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Request from -

    -
    -
    - -
    -
    -
    -
    -
    -

    - metamask.github.io -

    -
    -
    -
    -
    -
    -

    - Interacting with -

    -
    -
    - -
    -
    -
    -
    -
    -
    - -

    - 0x88AA6...A5125 -

    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Network fee -

    -
    -
    - -
    -
    -
    -
    -
    -

    - 0.0001 ETH -

    -

    - $0.04 -

    - -
    -
    -
    -
    -
    -

    - Speed -

    -
    -
    -
    -
    -

    - 🦊 Market -

    -

    - - ~ - 0 sec - -

    -
    -
    -
    -
    -
    -`; diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap index 28306720e577..0bd7028048ef 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap @@ -99,103 +99,3 @@ exports[`create-named-snap-account confirmation matches snapshot 1`] = ` `; - -exports[`create-named-snap-account confirmation matches snapshot 2`] = ` -
    -
    -
    - -
    - -
    -`; diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/switch-ethereum-chain.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/switch-ethereum-chain.test.js.snap index a292ffac273c..5982b3325d3b 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/switch-ethereum-chain.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/switch-ethereum-chain.test.js.snap @@ -115,8 +115,6 @@ exports[`switch-ethereum-chain confirmation should match snapshot 1`] = `
    `; -exports[`switch-ethereum-chain confirmation should match snapshot 2`] = `
    `; - exports[`switch-ethereum-chain confirmation should show alert if there are pending txs 1`] = `
    `; - -exports[`Interactive Replacement Token Page should reject if there are errors 2`] = ` -
    -
    -
    -
    - Replace custodian token - - failed -
    -
    -
    -
    -

    - Please go to displayName and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again. -

    -
    -
    -
    -
    - - -
    -
    -
    -
    -`; diff --git a/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js index 18208977fb90..8c37094dcbe4 100644 --- a/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js +++ b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js @@ -1,4 +1,4 @@ -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, act } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -141,14 +141,16 @@ describe('Confirm Recovery Phrase Component', () => { 'recovery-phrase-confirm', ); - await waitFor(() => { + expect(confirmRecoveryPhraseButton).toBeDisabled(); + + act(() => { clock.advanceTimersByTime(500); // Wait for debounce + }); - expect(confirmRecoveryPhraseButton).not.toBeDisabled(); + expect(confirmRecoveryPhraseButton).not.toBeDisabled(); - fireEvent.click(confirmRecoveryPhraseButton); + fireEvent.click(confirmRecoveryPhraseButton); - expect(setSeedPhraseBackedUp).toHaveBeenCalledWith(true); - }); + expect(setSeedPhraseBackedUp).toHaveBeenCalledWith(true); }); }); diff --git a/yarn.lock b/yarn.lock index 9f6ea0aa2fc2..f6df3487c4ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,7 +88,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.7": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.7": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" dependencies: @@ -3997,15 +3997,16 @@ __metadata: languageName: node linkType: hard -"@jest/types@npm:^25.5.0": - version: 25.5.0 - resolution: "@jest/types@npm:25.5.0" +"@jest/types@npm:^26.6.2": + version: 26.6.2 + resolution: "@jest/types@npm:26.6.2" dependencies: "@types/istanbul-lib-coverage": "npm:^2.0.0" - "@types/istanbul-reports": "npm:^1.1.1" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" "@types/yargs": "npm:^15.0.0" - chalk: "npm:^3.0.0" - checksum: 10/49cb06ab867bb4085de86b1c86cd76983aa97179b5de65a1de6ee2f345563fc19543c1b7470d5b626f08190da4e3c2e66b6fd2091a3c4f7bc10be3a000db7f0f + chalk: "npm:^4.0.0" + checksum: 10/02d42749c8c6dc7e3184d0ff0293dd91c97233c2e6dc3708d61ef33d3162d4f07ad38d2d8a39abd94cf2fced69b92a87565c7099137c4529809242ca327254af languageName: node linkType: hard @@ -9514,16 +9515,19 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^7.17.1": - version: 7.22.2 - resolution: "@testing-library/dom@npm:7.22.2" +"@testing-library/dom@npm:^7.17.1, @testing-library/dom@npm:^7.31.2": + version: 7.31.2 + resolution: "@testing-library/dom@npm:7.31.2" dependencies: - "@babel/runtime": "npm:^7.10.3" + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" "@types/aria-query": "npm:^4.2.0" aria-query: "npm:^4.2.2" - dom-accessibility-api: "npm:^0.5.0" - pretty-format: "npm:^25.5.0" - checksum: 10/2da0d8d577be7d5cfb6cf2b712e4ca65671e090190eb3ffdebd336c5ef2158dac4dee12709c6e06a38810291c7f407701187e7eec86f0b5ad2ff76487d28382d + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.6" + lz-string: "npm:^1.4.4" + pretty-format: "npm:^26.6.2" + checksum: 10/5082aaf14c80df529738d4ee3e85170371236162ce908430516ab6c9c581ea31e9ac9b87fdc9a8d298f98956c683b2068b029fcfdb5785ab7247348a6eab3854 languageName: node linkType: hard @@ -10493,16 +10497,6 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-reports@npm:^1.1.1": - version: 1.1.2 - resolution: "@types/istanbul-reports@npm:1.1.2" - dependencies: - "@types/istanbul-lib-coverage": "npm:*" - "@types/istanbul-lib-report": "npm:*" - checksum: 10/00866e815d1e68d0a590d691506937b79d8d65ad8eab5ed34dbfee66136c7c0f4ea65327d32046d5fe469f22abea2b294987591dc66365ebc3991f7e413b2d78 - languageName: node - linkType: hard - "@types/istanbul-reports@npm:^3.0.0": version: 3.0.0 resolution: "@types/istanbul-reports@npm:3.0.0" @@ -16873,10 +16867,10 @@ __metadata: languageName: node linkType: hard -"dom-accessibility-api@npm:^0.5.0": - version: 0.5.0 - resolution: "dom-accessibility-api@npm:0.5.0" - checksum: 10/2448657f072b4664f69616788da03f2f76ed5a47e21b8d36e872240eb9a3ca638c2f09fb9a31d9055ded4b50b0ef3013831dca47db62b9f809cb67ec9050bcd1 +"dom-accessibility-api@npm:^0.5.6": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16" + checksum: 10/377b4a7f9eae0a5d72e1068c369c99e0e4ca17fdfd5219f3abd32a73a590749a267475a59d7b03a891f9b673c27429133a818c44b2e47e32fec024b34274e2ca languageName: node linkType: hard @@ -25465,6 +25459,15 @@ __metadata: languageName: node linkType: hard +"lz-string@npm:^1.4.4": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 10/e86f0280e99a8d8cd4eef24d8601ddae15ce54e43ac9990dfcb79e1e081c255ad24424a30d78d2ad8e51a8ce82a66a930047fed4b4aa38c6f0b392ff9300edfc + languageName: node + linkType: hard + "magic-string@npm:^0.25.7": version: 0.25.7 resolution: "magic-string@npm:0.25.7" @@ -26217,6 +26220,7 @@ __metadata: "@storybook/theming": "npm:^7.6.20" "@swc/core": "npm:1.4.11" "@swc/helpers": "npm:^0.5.7" + "@testing-library/dom": "npm:^7.31.2" "@testing-library/jest-dom": "npm:^5.11.10" "@testing-library/react": "npm:^10.4.8" "@testing-library/react-hooks": "npm:^8.0.1" @@ -29699,15 +29703,15 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^25.5.0": - version: 25.5.0 - resolution: "pretty-format@npm:25.5.0" +"pretty-format@npm:^26.6.2": + version: 26.6.2 + resolution: "pretty-format@npm:26.6.2" dependencies: - "@jest/types": "npm:^25.5.0" + "@jest/types": "npm:^26.6.2" ansi-regex: "npm:^5.0.0" ansi-styles: "npm:^4.0.0" - react-is: "npm:^16.12.0" - checksum: 10/da9e79b2b98e48cabdb0d5b090993a5677969565be898c06ffe38ec792bf1f0c0fcf5f752552eb039b03e7cad2203347208a9b0b132e4a401e6eac655d061b31 + react-is: "npm:^17.0.1" + checksum: 10/94a4c661bf77ed7c448d064c5af35796acbd972a33cff8a38030547ac396087bcd47f2f6e530824486cf4c8e9d9342cc8dd55fd068f135b19325b51e0cd06f87 languageName: node linkType: hard @@ -30513,14 +30517,14 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.0": +"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10/5aa564a1cde7d391ac980bedee21202fc90bdea3b399952117f54fb71a932af1e5902020144fb354b4690b2414a0c7aafe798eb617b76a3d441d956db7726fdf languageName: node linkType: hard -"react-is@npm:^17.0.0, react-is@npm:^17.0.2": +"react-is@npm:^17.0.0, react-is@npm:^17.0.1, react-is@npm:^17.0.2": version: 17.0.2 resolution: "react-is@npm:17.0.2" checksum: 10/73b36281e58eeb27c9cc6031301b6ae19ecdc9f18ae2d518bdb39b0ac564e65c5779405d623f1df9abf378a13858b79442480244bd579968afc1faf9a2ce5e05 From 079da08131b6516d2b053f717a55aa8ec9324879 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 18 Oct 2024 11:27:18 +0100 Subject: [PATCH 36/51] chore: bump signature controller to remove message managers (#27787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Update the `SignatureController` to `20.0.0` which no longer uses the `@metamask/message-manager` package. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27787?quickstart=1) ## **Related issues** ## **Manual testing steps** Full regression of all signature functionality. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ...ture-controller-npm-6.1.2-f60d8a4960.patch | 23 ------------------- app/scripts/controllers/mmi-controller.ts | 13 ++++++----- app/scripts/lib/ppom/ppom-util.test.ts | 8 ++++--- lavamoat/browserify/beta/policy.json | 11 +++++---- lavamoat/browserify/flask/policy.json | 11 +++++---- lavamoat/browserify/main/policy.json | 11 +++++---- lavamoat/browserify/mmi/policy.json | 11 +++++---- package.json | 2 +- ...rs-after-init-opt-in-background-state.json | 1 + .../errors-after-init-opt-in-ui-state.json | 2 +- yarn.lock | 14 ++++++----- 11 files changed, 47 insertions(+), 60 deletions(-) delete mode 100644 .yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch diff --git a/.yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch b/.yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch deleted file mode 100644 index 692db45490f5..000000000000 --- a/.yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/dist/SignatureController.js b/dist/SignatureController.js -index 8ac1b2158ff4564fe2f942ca955bd337d78a94ef..c6552d874d830e610fcff791eb0f87f51fae1770 100644 ---- a/dist/SignatureController.js -+++ b/dist/SignatureController.js -@@ -278,6 +278,9 @@ _SignatureController_isEthSignEnabled = new WeakMap(), _SignatureController_getA - const messageParamsWithId = Object.assign(Object.assign(Object.assign({}, messageParams), { metamaskId: messageId }), (version && { version })); - const signaturePromise = messageManager.waitForFinishStatus(messageParamsWithId, messageName); - try { -+ signaturePromise.catch(() => { -+ // Expecting reject error but throwing manually rather than waiting -+ }); - // Signature request is proposed to the user - __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_addLog).call(this, signTypeForLogger, logging_controller_1.SigningStage.Proposed, messageParamsWithId); - const acceptResult = yield __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_requestApproval).call(this, messageParamsWithId, approvalType); -@@ -287,7 +290,7 @@ _SignatureController_isEthSignEnabled = new WeakMap(), _SignatureController_getA - // User rejected the signature request - __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_addLog).call(this, signTypeForLogger, logging_controller_1.SigningStage.Rejected, messageParamsWithId); - __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_cancelAbstractMessage).call(this, messageManager, messageId); -- throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the request.'); -+ throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest(`MetaMask ${messageName} Signature: User denied message signature.`); - } - yield signMessage(messageParamsWithId, signingOpts); - const signatureResult = yield signaturePromise; diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 8c5f1ee4b49b..571c000106b1 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -21,11 +21,12 @@ import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { TransactionMeta } from '@metamask/transaction-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; -import { SignatureController } from '@metamask/signature-controller'; import { - OriginalRequest, - PersonalMessageParams, -} from '@metamask/message-manager'; + MessageParamsPersonal, + MessageParamsTyped, + SignatureController, +} from '@metamask/signature-controller'; +import { OriginalRequest } from '@metamask/message-manager'; import { NetworkController } from '@metamask/network-controller'; import { InternalAccount } from '@metamask/keyring-api'; import { toHex } from '@metamask/controller-utils'; @@ -805,14 +806,14 @@ export default class MMIController extends EventEmitter { req.method === 'eth_signTypedData_v4' ) { return await this.signatureController.newUnsignedTypedMessage( - updatedMsgParams as PersonalMessageParams, + updatedMsgParams as MessageParamsTyped, req as OriginalRequest, version, { parseJsonData: false }, ); } else if (req.method === 'personal_sign') { return await this.signatureController.newUnsignedPersonalMessage( - updatedMsgParams as PersonalMessageParams, + updatedMsgParams as MessageParamsPersonal, req as OriginalRequest, ); } diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index 2fd1932a649d..f6a0d3a1213c 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -6,8 +6,10 @@ import { TransactionParams, normalizeTransactionParams, } from '@metamask/transaction-controller'; -import { SignatureController } from '@metamask/signature-controller'; -import type { PersonalMessage } from '@metamask/message-manager'; +import { + SignatureController, + SignatureRequest, +} from '@metamask/signature-controller'; import { BlockaidReason, BlockaidResultType, @@ -246,7 +248,7 @@ describe('PPOM Utils', () => { ...SECURITY_ALERT_RESPONSE_MOCK, securityAlertId: SECURITY_ALERT_ID_MOCK, }, - } as unknown as PersonalMessage, + } as unknown as SignatureRequest, }); await updateSecurityAlertResponse({ diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 27b06f2ba5b8..61c36624b27d 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2297,15 +2297,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 27b06f2ba5b8..61c36624b27d 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2297,15 +2297,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 27b06f2ba5b8..61c36624b27d 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2297,15 +2297,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7c19b1b4c76e..abcdb192390d 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2389,15 +2389,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, diff --git a/package.json b/package.json index 3c1ea2bfbbba..252e355b9fa6 100644 --- a/package.json +++ b/package.json @@ -350,7 +350,7 @@ "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^18.0.2", - "@metamask/signature-controller": "^19.1.0", + "@metamask/signature-controller": "^20.0.0", "@metamask/smart-transactions-controller": "^13.0.0", "@metamask/snaps-controllers": "^9.11.1", "@metamask/snaps-execution-environments": "^6.9.1", 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 78988fc87cc8..f96d03d96da0 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 @@ -235,6 +235,7 @@ "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SignatureController": { + "signatureRequests": "object", "unapprovedPersonalMsgs": "object", "unapprovedTypedMessages": "object", "unapprovedPersonalMsgCount": 0, 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 f40d36f85aad..d7c2caead3a5 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 @@ -203,7 +203,6 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", - "submitHistory": "object", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", @@ -219,6 +218,7 @@ "accounts": "object", "accountsByChainId": "object", "marketData": "object", + "signatureRequests": "object", "unapprovedDecryptMsgs": "object", "unapprovedDecryptMsgCount": 0, "unapprovedEncryptionPublicKeyMsgs": "object", diff --git a/yarn.lock b/yarn.lock index f6df3487c4ef..2a52221beb3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6225,20 +6225,22 @@ __metadata: languageName: node linkType: hard -"@metamask/signature-controller@npm:^19.1.0": - version: 19.1.0 - resolution: "@metamask/signature-controller@npm:19.1.0" +"@metamask/signature-controller@npm:^20.0.0": + version: 20.0.0 + resolution: "@metamask/signature-controller@npm:20.0.0" dependencies: "@metamask/base-controller": "npm:^7.0.1" "@metamask/controller-utils": "npm:^11.3.0" - "@metamask/message-manager": "npm:^10.1.1" + "@metamask/eth-sig-util": "npm:^7.0.1" "@metamask/utils": "npm:^9.1.0" + jsonschema: "npm:^1.2.4" lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" peerDependencies: "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 "@metamask/logging-controller": ^6.0.0 - checksum: 10/ac01b4ba6708e2e74b92ef1c5d4fb9aeff06ae2bd3b445fe8a10bc8e84641ad3bed6fb245f0303ef9d13b7458d022ef07d5ce211a05b14e1ad5ce44ad49cd4ec + checksum: 10/5647e362b4478d9cdb9f04027d7bad950efbe310496fc0347a92649a084bb92fc92a7fc5f911f8835e0d6b4e7ed6cf572594a79a57a31240948b87dd2267cdf8 languageName: node linkType: hard @@ -26173,7 +26175,7 @@ __metadata: "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^18.0.2" - "@metamask/signature-controller": "npm:^19.1.0" + "@metamask/signature-controller": "npm:^20.0.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.11.1" "@metamask/snaps-execution-environments": "npm:^6.9.1" From 776bc1e1465d32351c91972a951d869bd767c1ee Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 18 Oct 2024 11:33:17 +0100 Subject: [PATCH 37/51] feat: dapp initiated token transfer (#27875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Implements new header for dapp initiated token transfer confirmations - Enables simulations component conditionally - Includes e2e tests for said confirmation [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27875?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3017 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-10-15 at 16 20 37 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + .../redesign/transaction-confirmation.ts | 10 + test/e2e/page-objects/pages/test-dapp.ts | 6 + .../erc20-token-send-redesign.spec.ts | 124 ++++++++-- .../dapp-initiated-header.test.tsx.snap | 35 +++ .../header/__snapshots__/header.test.tsx.snap | 127 ++-------- .../header/dapp-initiated-header.test.tsx | 21 ++ .../confirm/header/dapp-initiated-header.tsx | 39 +++ .../components/confirm/header/header.tsx | 2 + .../token-transfer.test.tsx.snap | 225 ++++++++++++------ .../token-transfer/token-transfer.test.tsx | 9 +- .../info/token-transfer/token-transfer.tsx | 18 ++ ui/pages/confirmations/utils/confirm.ts | 2 +- 13 files changed, 412 insertions(+), 209 deletions(-) create mode 100644 ui/pages/confirmations/components/confirm/header/__snapshots__/dapp-initiated-header.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/header/dapp-initiated-header.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/header/dapp-initiated-header.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6834ba7169c7..70cb4da8cfb6 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6230,6 +6230,9 @@ "transferFrom": { "message": "Transfer from" }, + "transferRequest": { + "message": "Transfer request" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" diff --git a/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts index 661feef33197..c7f618d3fc61 100644 --- a/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts +++ b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts @@ -6,6 +6,8 @@ import Confirmation from './confirmation'; class TransactionConfirmation extends Confirmation { private walletInitiatedHeadingTitle: RawLocator; + private dappInitiatedHeadingTitle: RawLocator; + constructor(driver: Driver) { super(driver); @@ -15,11 +17,19 @@ class TransactionConfirmation extends Confirmation { css: 'h3', text: tEn('review') as string, }; + this.dappInitiatedHeadingTitle = { + css: 'h3', + text: tEn('transferRequest') as string, + }; } async check_walletInitiatedHeadingTitle() { await this.driver.waitForSelector(this.walletInitiatedHeadingTitle); } + + async check_dappInitiatedHeadingTitle() { + await this.driver.waitForSelector(this.dappInitiatedHeadingTitle); + } } export default TransactionConfirmation; diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 0940225b5e3b..4a02d80459e0 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -128,6 +128,8 @@ class TestDapp { tag: 'button', }; + private erc20TokenTransferButton = '#transferTokens'; + constructor(driver: Driver) { this.driver = driver; } @@ -192,6 +194,10 @@ class TestDapp { await this.driver.clickElement(this.erc20WatchAssetButton); } + public async clickERC20TokenTransferButton() { + await this.driver.clickElement(this.erc20TokenTransferButton); + } + /** * Connect account to test dapp. * diff --git a/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts index 83892b1ca6e1..7bacf156b71a 100644 --- a/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts @@ -1,7 +1,11 @@ /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL } from '../../../constants'; -import { unlockWallet, WINDOW_TITLES } from '../../../helpers'; +import { + unlockWallet, + veryLargeDelayMs, + WINDOW_TITLES, +} from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; import WatchAssetConfirmation from '../../../page-objects/pages/confirmations/legacy/watch-asset-confirmation'; import TokenTransferTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/token-transfer-confirmation'; @@ -16,28 +20,68 @@ import { TestSuiteArguments } from './shared'; const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); describe('Confirmation Redesign ERC20 Token Send @no-mmi', function () { - it('Sends a type 0 transaction (Legacy)', async function () { - await withRedesignConfirmationFixtures( - this.test?.fullTitle(), - TransactionEnvelopeType.legacy, - async ({ driver, contractRegistry }: TestSuiteArguments) => { - await createTransactionAndAssertDetails(driver, contractRegistry); - }, - mocks, - SMART_CONTRACTS.HST, - ); + describe('Wallet initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); }); - it('Sends a type 2 transaction (EIP1559)', async function () { - await withRedesignConfirmationFixtures( - this.test?.fullTitle(), - TransactionEnvelopeType.feeMarket, - async ({ driver, contractRegistry }: TestSuiteArguments) => { - await createTransactionAndAssertDetails(driver, contractRegistry); - }, - mocks, - SMART_CONTRACTS.HST, - ); + describe('dApp initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); }); }); @@ -69,7 +113,7 @@ export async function mockedSourcifyTokenSend(mockServer: Mockttp) { })); } -async function createTransactionAndAssertDetails( +async function createWalletInitiatedTransactionAndAssertDetails( driver: Driver, contractRegistry?: GanacheContractAddressRegistry, ) { @@ -113,3 +157,39 @@ async function createTransactionAndAssertDetails( await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); } + +async function createDAppInitiatedTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.HST); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + + await testDapp.clickERC20WatchAssetButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const watchAssetConfirmation = new WatchAssetConfirmation(driver); + await watchAssetConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await testDapp.clickERC20TokenTransferButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_dappInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_interactingWithParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} diff --git a/ui/pages/confirmations/components/confirm/header/__snapshots__/dapp-initiated-header.test.tsx.snap b/ui/pages/confirmations/components/confirm/header/__snapshots__/dapp-initiated-header.test.tsx.snap new file mode 100644 index 000000000000..26b0fed0b969 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/__snapshots__/dapp-initiated-header.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should match snapshot 1`] = ` +
    +
    +

    + Transfer request +

    +
    +
    + +
    +
    +
    +
    +`; diff --git a/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap b/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap index 4346963ead15..621318c6ebe2 100644 --- a/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap @@ -116,122 +116,31 @@ exports[`Header should match snapshot with signature confirmation 1`] = ` exports[`Header should match snapshot with token transfer confirmation initiated in a dApp 1`] = `
    -
    -
    -
    -
    -
    - - - - - -
    -
    -
    -
    - G -
    -
    -
    -

    -

    - Goerli -

    -
    -
    + Transfer request +
    -
    -
    - -
    -
    -
    - -
    + +
    diff --git a/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.test.tsx b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.test.tsx new file mode 100644 index 000000000000..4ae30d90936c --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { DefaultRootState } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; +import configureStore from '../../../../../store/store'; +import { DAppInitiatedHeader } from './dapp-initiated-header'; + +const render = ( + state: DefaultRootState = getMockTokenTransferConfirmState({}), +) => { + const store = configureStore(state); + return renderWithConfirmContextProvider(, store); +}; + +describe('', () => { + it('should match snapshot', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.tsx new file mode 100644 index 000000000000..3d4734659117 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Box, Text } from '../../../../../components/component-library'; +import { + AlignItems, + BackgroundColor, + Display, + FlexDirection, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { AdvancedDetailsButton } from './advanced-details-button'; + +export const DAppInitiatedHeader = () => { + const t = useI18nContext(); + + return ( + + + {t('transferRequest')} + + + + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/header/header.tsx b/ui/pages/confirmations/components/confirm/header/header.tsx index 9c113effe6a5..dacc432612b8 100644 --- a/ui/pages/confirmations/components/confirm/header/header.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.tsx @@ -22,6 +22,7 @@ import { useConfirmContext } from '../../../context/confirm'; import useConfirmationNetworkInfo from '../../../hooks/useConfirmationNetworkInfo'; import useConfirmationRecipientInfo from '../../../hooks/useConfirmationRecipientInfo'; import { Confirmation } from '../../../types/confirm'; +import { DAppInitiatedHeader } from './dapp-initiated-header'; import HeaderInfo from './header-info'; import { WalletInitiatedHeader } from './wallet-initiated-header'; @@ -39,6 +40,7 @@ const Header = () => { if (isWalletInitiated) { return ; } + return ; } return ( diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap index c9813ea1470e..e1d19241252b 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap @@ -3,90 +3,163 @@ exports[`TokenTransferInfo renders correctly 1`] = `
    - - - - - - - - - + ? +
    +

    + Unknown +

    - - +
    + +

    + 0x2e0D7...5d09B +

    +
    +
    + - +
    +
    +
    +
    - - - +

    + Estimated changes +

    +
    +
    + +
    +
    +
    +
    +
    - - - +
    +
    +
    +

    + You send +

    +
    +
    +
    +
    +
    +

    + - 4 +

    +
    +
    +
    +
    +
    +
    + E +
    +

    + ETH +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    ({ })); describe('TokenTransferInfo', () => { - it('renders correctly', () => { + it('renders correctly', async () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( , mockStore, ); + + await waitFor(() => { + expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx index 9c0dfe81f536..b89e87350a36 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx @@ -1,4 +1,8 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { useConfirmContext } from '../../../../context/confirm'; +import { SimulationDetails } from '../../../simulation-details'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import SendHeading from '../shared/send-heading/send-heading'; @@ -6,10 +10,24 @@ import { TokenDetailsSection } from './token-details-section'; import { TransactionFlowSection } from './transaction-flow-section'; const TokenTransferInfo = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const isWalletInitiated = transactionMeta.origin === 'metamask'; + return ( <> + {!isWalletInitiated && ( + + + + )} diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 41ffd2832169..a33c1dae735c 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -22,11 +22,11 @@ export const REDESIGN_USER_TRANSACTION_TYPES = [ TransactionType.tokenMethodApprove, TransactionType.tokenMethodIncreaseAllowance, TransactionType.tokenMethodSetApprovalForAll, + TransactionType.tokenMethodTransfer, ]; export const REDESIGN_DEV_TRANSACTION_TYPES = [ ...REDESIGN_USER_TRANSACTION_TYPES, - TransactionType.tokenMethodTransfer, ]; const SIGNATURE_APPROVAL_TYPES = [ From eedeb240778bea0970462f9ca25e24f86fe70497 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:56:16 +0000 Subject: [PATCH 38/51] chore(deps): upgrade from json-rpc-engine to @metamask/json-rpc-engine (#22875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ## **Related issues** - https://github.com/MetaMask/metamask-extension/issues/27784 - https://github.com/MetaMask/eth-json-rpc-middleware/issues/335 - #27917 - https://github.com/MetaMask/metamask-extension/issues/18510 - https://github.com/MetaMask/metamask-extension/issues/15250 - https://github.com/MetaMask/metamask-improvement-proposals/pull/36 ### Blocked by - [x] #24496 ### Follow-up to - https://github.com/MetaMask/metamask-extension/pull/24496 ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've clearly explained what problem this PR is solving and how it is solved. - [x] I've linked related issues - [x] I've included manual testing steps - [x] I've included screenshots/recordings if applicable - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [x] I’ve properly set the pull request status: - [x] In case it's not yet "ready for review", I've set it to "draft". - [x] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../controllers/preferences-controller.ts | 3 +- ...ToNonEvmAccountReqFilterMiddleware.test.ts | 46 +++++- ...thodsToNonEvmAccountReqFilterMiddleware.ts | 13 +- app/scripts/lib/createMetamaskMiddleware.js | 5 +- .../lib/createRPCMethodTrackingMiddleware.js | 10 +- app/scripts/lib/middleware/pending.js | 2 +- .../createMethodMiddleware.js | 2 +- .../createMethodMiddleware.test.js | 4 +- .../createUnsupportedMethodMiddleware.ts | 7 +- .../handlers/eth-accounts.ts | 2 +- .../handlers/get-provider-state.test.ts | 2 +- .../handlers/get-provider-state.ts | 2 +- .../institutional/mmi-authenticate.js | 4 +- .../mmi-check-if-token-is-present.js | 4 +- .../mmi-open-add-hardware-wallet.js | 4 +- .../handlers/institutional/mmi-portfolio.js | 4 +- .../mmi-set-account-and-network.js | 4 +- .../handlers/institutional/mmi-supported.js | 4 +- .../handlers/log-web3-shim-usage.test.ts | 2 +- .../handlers/log-web3-shim-usage.ts | 2 +- .../handlers/request-accounts.js | 4 +- .../handlers/send-metadata.js | 4 +- .../handlers/watch-asset.js | 4 +- .../tx-verification-middleware.ts | 11 +- app/scripts/metamask-controller.js | 8 +- lavamoat/browserify/beta/policy.json | 131 +++++++++++++----- lavamoat/browserify/flask/policy.json | 131 +++++++++++++----- lavamoat/browserify/main/policy.json | 131 +++++++++++++----- lavamoat/browserify/mmi/policy.json | 131 +++++++++++++----- package.json | 2 +- test/stub/provider.js | 5 +- ui/store/actions.ts | 68 +++++---- yarn.lock | 21 +-- 33 files changed, 535 insertions(+), 242 deletions(-) diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index a7ede69bb26c..536ec33b34eb 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -6,14 +6,13 @@ import { AccountsControllerSetSelectedAccountAction, AccountsControllerState, } from '@metamask/accounts-controller'; -import { Hex } from '@metamask/utils'; +import { Hex, Json } from '@metamask/utils'; import { BaseController, ControllerGetStateAction, ControllerStateChangeEvent, RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { Json } from 'json-rpc-engine'; import { NetworkControllerGetStateAction } from '@metamask/network-controller'; import { ETHERSCAN_SUPPORTED_CHAIN_IDS, diff --git a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts index a5e04f6b7834..063271a9984a 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts @@ -1,12 +1,12 @@ -import { jsonrpc2 } from '@metamask/utils'; +import { jsonrpc2, Json } from '@metamask/utils'; import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; -import { Json } from 'json-rpc-engine'; +import type { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware, { EvmMethodsToNonEvmAccountFilterMessenger, } from './createEvmMethodsToNonEvmAccountReqFilterMiddleware'; describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { - const getMockRequest = (method: string, params?: Json) => ({ + const getMockRequest = (method: string, params: Json) => ({ jsonrpc: jsonrpc2, id: 1, method, @@ -20,71 +20,85 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { { accountType: BtcAccountType.P2wpkh, method: 'eth_accounts', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_sendRawTransaction', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_sendTransaction', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData_v1', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData_v3', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData_v4', + params: undefined, calledNext: false, }, { accountType: EthAccountType.Eoa, method: 'eth_accounts', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_sendRawTransaction', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_sendTransaction', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData_v1', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData_v3', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData_v4', + params: undefined, calledNext: true, }, @@ -92,21 +106,25 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { { accountType: BtcAccountType.P2wpkh, method: 'eth_blockNumber', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_chainId', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_blockNumber', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_chainId', + params: undefined, calledNext: true, }, @@ -114,91 +132,109 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { { accountType: BtcAccountType.P2wpkh, method: 'wallet_getSnaps', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_invokeSnap', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_requestSnaps', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'snap_getClientStatus', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_addEthereumChain', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_getPermissions', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_requestPermissions', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_revokePermissions', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_switchEthereumChain', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_getSnaps', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_invokeSnap', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_requestSnaps', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'snap_getClientStatus', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_addEthereumChain', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_getPermissions', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_requestPermissions', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_revokePermissions', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_switchEthereumChain', + params: undefined, calledNext: true, }, @@ -250,7 +286,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { }: { accountType: EthAccountType | BtcAccountType; method: string; - params?: Json; + params: Json; calledNext: number; }) => { const filterFn = createEvmMethodsToNonEvmAccountReqFilterMiddleware({ @@ -262,7 +298,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { const mockEnd = jest.fn(); filterFn( - getMockRequest(method, params), + getMockRequest(method, params) as JsonRpcRequest, getMockResponse(), mockNext, mockEnd, diff --git a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts index 3e1eca86997e..cc912b5113a7 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts @@ -1,7 +1,8 @@ import { isEvmAccountType } from '@metamask/keyring-api'; import { RestrictedControllerMessenger } from '@metamask/base-controller'; import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; -import { JsonRpcMiddleware } from 'json-rpc-engine'; +import { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { Json, JsonRpcParams } from '@metamask/utils'; import { RestrictedEthMethods } from '../../../shared/constants/permissions'; import { unrestrictedEthSigningMethods } from '../controllers/permissions'; @@ -32,7 +33,7 @@ export default function createEvmMethodsToNonEvmAccountReqFilterMiddleware({ messenger, }: { messenger: EvmMethodsToNonEvmAccountFilterMessenger; -}): JsonRpcMiddleware { +}): JsonRpcMiddleware { return function filterEvmRequestToNonEvmAccountsMiddleware( req, _res, @@ -74,7 +75,13 @@ export default function createEvmMethodsToNonEvmAccountReqFilterMiddleware({ // TODO: Convert this to superstruct schema const isWalletRequestPermission = req.method === 'wallet_requestPermissions'; - if (isWalletRequestPermission && req?.params && Array.isArray(req.params)) { + if ( + isWalletRequestPermission && + req?.params && + Array.isArray(req.params) && + req.params.length > 0 && + req.params[0] + ) { const permissionsMethodRequest = Object.keys(req.params[0]); const isEvmPermissionRequest = METHODS_TO_CHECK.some((method) => diff --git a/app/scripts/lib/createMetamaskMiddleware.js b/app/scripts/lib/createMetamaskMiddleware.js index d48ae32dc4a3..9ea07b0d28e5 100644 --- a/app/scripts/lib/createMetamaskMiddleware.js +++ b/app/scripts/lib/createMetamaskMiddleware.js @@ -1,4 +1,7 @@ -import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'; +import { + createScaffoldMiddleware, + mergeMiddleware, +} from '@metamask/json-rpc-engine'; import { createWalletMiddleware } from '@metamask/eth-json-rpc-middleware'; import { createPendingNonceMiddleware, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index a5f12687f89e..a1c5a036f13f 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -18,6 +18,7 @@ import { PRIMARY_TYPES_PERMIT, } from '../../../shared/constants/signatures'; import { SIGNING_METHODS } from '../../../shared/constants/transaction'; +import { getErrorMessage } from '../../../shared/modules/error'; import { generateSignatureUniqueId, getBlockaidMetricsProps, @@ -419,15 +420,20 @@ export default function createRPCMethodTrackingMiddleware({ const location = res.error?.data?.location; let event; + + const errorMessage = getErrorMessage(res.error); + if (res.error?.code === errorCodes.provider.userRejectedRequest) { event = eventType.REJECTED; } else if ( res.error?.code === errorCodes.rpc.internal && - res.error?.message === 'Request rejected by user or snap.' + [errorMessage, res.error.message].includes( + 'Request rejected by user or snap.', + ) ) { // The signature was approved in MetaMask but rejected in the snap event = eventType.REJECTED; - eventProperties.status = res.error.message; + eventProperties.status = errorMessage; } else { event = eventType.APPROVED; } diff --git a/app/scripts/lib/middleware/pending.js b/app/scripts/lib/middleware/pending.js index 9e01d11ffcb2..0c9d3445a01e 100644 --- a/app/scripts/lib/middleware/pending.js +++ b/app/scripts/lib/middleware/pending.js @@ -1,4 +1,4 @@ -import { createAsyncMiddleware } from 'json-rpc-engine'; +import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; import { formatTxMetaForRpcResult } from '../util'; export function createPendingNonceMiddleware({ getPendingNonce }) { diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index cee4e7763255..bbc06e7033f5 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -42,7 +42,7 @@ function makeMethodMiddlewareMaker(handlers) { * * @param {Record unknown | Promise>} hooks - Required "hooks" into our * controllers. - * @returns {import('json-rpc-engine').JsonRpcMiddleware} The method middleware function. + * @returns {import('@metamask/json-rpc-engine').JsonRpcMiddleware} The method middleware function. */ const makeMethodMiddleware = (hooks) => { assertExpectedHook(hooks, expectedHookNames); diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js index 46aba9abe746..48ea5ae90d58 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js @@ -1,4 +1,4 @@ -import { JsonRpcEngine } from 'json-rpc-engine'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { assertIsJsonRpcFailure, assertIsJsonRpcSuccess, @@ -140,6 +140,7 @@ describe.each([ assertIsJsonRpcFailure(response); expect(response.error.message).toBe('test error'); + expect(response.error.data.cause.message).toBe('test error'); }); it('should handle errors thrown by the implementation', async () => { @@ -156,6 +157,7 @@ describe.each([ assertIsJsonRpcFailure(response); expect(response.error.message).toBe('test error'); + expect(response.error.data.cause.message).toBe('test error'); }); it('should handle non-errors thrown by the implementation', async () => { diff --git a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts index 12abc82d4b21..c96201041d36 100644 --- a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts +++ b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts @@ -1,5 +1,6 @@ +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcParams } from '@metamask/utils'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { JsonRpcMiddleware } from 'json-rpc-engine'; import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; /** @@ -7,8 +8,8 @@ import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; * appropriate error. */ export function createUnsupportedMethodMiddleware(): JsonRpcMiddleware< - unknown, - void + JsonRpcParams, + null > { return async function unsupportedMethodMiddleware(req, _res, next, end) { if ((UNSUPPORTED_RPC_METHODS as Set).has(req.method)) { diff --git a/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts b/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts index 003cbd88281b..47c2f0c2e318 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts @@ -1,7 +1,7 @@ import type { JsonRpcEngineEndCallback, JsonRpcEngineNextCallback, -} from 'json-rpc-engine'; +} from '@metamask/json-rpc-engine'; import type { JsonRpcRequest, JsonRpcParams, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts index f3d76a09f5fc..078bd7866a31 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts @@ -1,5 +1,5 @@ import { PendingJsonRpcResponse } from '@metamask/utils'; -import { JsonRpcEngineEndCallback } from 'json-rpc-engine'; +import { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import getProviderState, { GetProviderState, ProviderStateHandlerResult, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts index c95b66e1a20d..514f8af6dfa7 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts @@ -1,7 +1,7 @@ import type { JsonRpcEngineNextCallback, JsonRpcEngineEndCallback, -} from 'json-rpc-engine'; +} from '@metamask/json-rpc-engine'; import type { PendingJsonRpcResponse, JsonRpcParams, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js index 48014a6d66fa..e30332651c8c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js @@ -21,8 +21,8 @@ export default mmiAuthenticate; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js index 1e05251a25c4..428997bff70c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js @@ -23,8 +23,8 @@ export default mmiAuthenticate; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param options0 diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js index 61af6eb43fe8..7e685e1754a1 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js @@ -16,8 +16,8 @@ export default mmiOpenAddHardwareWallet; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js index cbe96127682f..7f76d3239afb 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js @@ -22,8 +22,8 @@ export default mmiPortfolio; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js index 6c3dc41da9d2..46754e144fab 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js @@ -23,8 +23,8 @@ export default mmiSetAccountAndNetwork; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js index 5aa987ed880f..e72a1aebf830 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js @@ -19,8 +19,8 @@ export default mmiSupported; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} _req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} _req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. */ diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts index d81427af8c26..1b48b75b5e4d 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts @@ -1,4 +1,4 @@ -import type { JsonRpcEngineEndCallback } from 'json-rpc-engine'; +import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import { PendingJsonRpcResponse } from '@metamask/utils'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { HandlerRequestType as LogWeb3ShimUsageHandlerRequest } from './types'; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts index bff4215ea5aa..c91bd4fa4650 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts @@ -1,7 +1,7 @@ import type { JsonRpcEngineNextCallback, JsonRpcEngineEndCallback, -} from 'json-rpc-engine'; +} from '@metamask/json-rpc-engine'; import type { JsonRpcParams, PendingJsonRpcResponse } from '@metamask/utils'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { diff --git a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js index 04977fe465d9..68b52ea75549 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js @@ -48,8 +48,8 @@ const locks = new Set(); /** * - * @param {import('json-rpc-engine').JsonRpcRequest} _req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} _req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {RequestEthereumAccountsOptions} options - The RPC method hooks. diff --git a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js index 35ec117a1f63..5dcfdf274fb6 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js @@ -25,8 +25,8 @@ export default sendMetadata; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {SendMetadataOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index fdfacb373c77..2f3475f7df71 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -23,8 +23,8 @@ export default watchAsset; */ /** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse} res - The JSON-RPC response object. * @param {Function} _next - The json-rpc-engine 'next' callback. * @param {Function} end - The json-rpc-engine 'end' callback. * @param {WatchAssetOptions} options diff --git a/app/scripts/lib/tx-verification/tx-verification-middleware.ts b/app/scripts/lib/tx-verification/tx-verification-middleware.ts index 5349feaf1cf9..7abdf73e3637 100644 --- a/app/scripts/lib/tx-verification/tx-verification-middleware.ts +++ b/app/scripts/lib/tx-verification/tx-verification-middleware.ts @@ -2,14 +2,17 @@ import { hashMessage } from '@ethersproject/hash'; import { verifyMessage } from '@ethersproject/wallet'; import type { NetworkController } from '@metamask/network-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { Json, JsonRpcParams, Hex } from '@metamask/utils'; -import { hasProperty, isObject } from '@metamask/utils'; import type { + Json, + JsonRpcParams, JsonRpcResponse, + Hex, +} from '@metamask/utils'; +import { hasProperty, isObject, JsonRpcRequest } from '@metamask/utils'; +import type { JsonRpcEngineEndCallback, JsonRpcEngineNextCallback, -} from 'json-rpc-engine'; -import { JsonRpcRequest } from 'json-rpc-engine'; +} from '@metamask/json-rpc-engine'; import { EXPERIENCES_TO_VERIFY, getExperience, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f7832f5893ec..75a0da28157e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -13,9 +13,9 @@ import { RatesController, fetchMultiExchangeRate, } from '@metamask/assets-controllers'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; -import { JsonRpcEngine } from 'json-rpc-engine'; import { createEngineStream } from 'json-rpc-middleware-stream'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; import { debounce, throttle, memoize, wrap } from 'lodash'; @@ -5492,11 +5492,7 @@ export default class MetamaskController extends EventEmitter { outStream, (err) => { // handle any middleware cleanup - engine._middleware.forEach((mid) => { - if (mid.destroy && typeof mid.destroy === 'function') { - mid.destroy(); - } - }); + engine.destroy(); connectionId && this.removeConnection(origin, connectionId); // For context and todos related to the error message match, see https://github.com/MetaMask/metamask-extension/issues/26337 if (err && !err.message?.match('Premature close')) { diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 61c36624b27d..9cbdda6ac03e 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -919,11 +919,18 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, "@metamask/utils": true } }, @@ -1345,6 +1352,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1544,10 +1558,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1655,8 +1669,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true } @@ -1678,18 +1692,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -1904,10 +1932,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1920,6 +1948,28 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/rpc-errors": { "packages": { "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2147,10 +2197,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2161,6 +2211,28 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/rpc-errors": { "packages": { "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2587,13 +2659,7 @@ "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, - "@metamask/utils": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2702,8 +2768,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2719,6 +2785,13 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -2804,8 +2877,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2813,6 +2886,13 @@ "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -4675,25 +4755,6 @@ "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, - "json-rpc-engine>eth-rpc-errors": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "json-rpc-engine>eth-rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 61c36624b27d..9cbdda6ac03e 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -919,11 +919,18 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, "@metamask/utils": true } }, @@ -1345,6 +1352,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1544,10 +1558,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1655,8 +1669,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true } @@ -1678,18 +1692,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -1904,10 +1932,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1920,6 +1948,28 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/rpc-errors": { "packages": { "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2147,10 +2197,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2161,6 +2211,28 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/rpc-errors": { "packages": { "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2587,13 +2659,7 @@ "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, - "@metamask/utils": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2702,8 +2768,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2719,6 +2785,13 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -2804,8 +2877,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2813,6 +2886,13 @@ "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -4675,25 +4755,6 @@ "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, - "json-rpc-engine>eth-rpc-errors": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "json-rpc-engine>eth-rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 61c36624b27d..9cbdda6ac03e 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -919,11 +919,18 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, "@metamask/utils": true } }, @@ -1345,6 +1352,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1544,10 +1558,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1655,8 +1669,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true } @@ -1678,18 +1692,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -1904,10 +1932,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1920,6 +1948,28 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/rpc-errors": { "packages": { "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2147,10 +2197,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2161,6 +2211,28 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/rpc-errors": { "packages": { "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2587,13 +2659,7 @@ "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, - "@metamask/utils": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2702,8 +2768,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2719,6 +2785,13 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -2804,8 +2877,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2813,6 +2886,13 @@ "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -4675,25 +4755,6 @@ "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, - "json-rpc-engine>eth-rpc-errors": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "json-rpc-engine>eth-rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index abcdb192390d..fae253f8b9d5 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1011,11 +1011,18 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, "@metamask/utils": true } }, @@ -1437,6 +1444,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1636,10 +1650,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1747,8 +1761,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true } @@ -1770,18 +1784,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -1996,10 +2024,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -2012,6 +2040,28 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/rpc-errors": { "packages": { "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2239,10 +2289,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2253,6 +2303,28 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/rpc-errors": { "packages": { "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, @@ -2679,13 +2751,7 @@ "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, - "@metamask/utils": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2794,8 +2860,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2811,6 +2877,13 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -2896,8 +2969,8 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, @@ -2905,6 +2978,13 @@ "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -4767,25 +4847,6 @@ "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, - "json-rpc-engine>eth-rpc-errors": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "json-rpc-engine>eth-rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/package.json b/package.json index 252e355b9fa6..6c52604d6b84 100644 --- a/package.json +++ b/package.json @@ -323,6 +323,7 @@ "@metamask/ethjs-query": "^0.7.1", "@metamask/gas-fee-controller": "^18.0.0", "@metamask/jazzicon": "^2.0.0", + "@metamask/json-rpc-engine": "^10.0.0", "@metamask/keyring-api": "^8.1.3", "@metamask/keyring-controller": "^17.2.2", "@metamask/logging-controller": "^6.0.0", @@ -401,7 +402,6 @@ "immer": "^9.0.6", "is-retry-allowed": "^2.2.0", "jest-junit": "^14.0.1", - "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^5.0.1", "labeled-stream-splicer": "^2.0.2", "localforage": "^1.9.0", diff --git a/test/stub/provider.js b/test/stub/provider.js index e070d55fa6b0..f86762218adf 100644 --- a/test/stub/provider.js +++ b/test/stub/provider.js @@ -1,4 +1,7 @@ -import { JsonRpcEngine, createScaffoldMiddleware } from 'json-rpc-engine'; +import { + JsonRpcEngine, + createScaffoldMiddleware, +} from '@metamask/json-rpc-engine'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; import Ganache from 'ganache'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 3433a798a9d9..a81dabb5e5c6 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -182,7 +182,7 @@ export function tryUnlockMetamask( dispatch(hideLoadingIndication()); }) .catch((err) => { - dispatch(unlockFailed(err.message)); + dispatch(unlockFailed(getErrorMessage(err))); dispatch(hideLoadingIndication()); return Promise.reject(err); }); @@ -4213,7 +4213,7 @@ export function setConnectedStatusPopoverHasBeenShown(): ThunkAction< return () => { callBackgroundMethod('setConnectedStatusPopoverHasBeenShown', [], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; @@ -4223,7 +4223,7 @@ export function setRecoveryPhraseReminderHasBeenShown() { return () => { callBackgroundMethod('setRecoveryPhraseReminderHasBeenShown', [], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; @@ -4238,7 +4238,7 @@ export function setRecoveryPhraseReminderLastShown( [lastShown], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }, ); @@ -4723,12 +4723,15 @@ export function fetchSmartTransactionFees( return smartTransactionFees; } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -4800,12 +4803,15 @@ export function signAndSendSmartTransaction({ return response.uuid; } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -4826,12 +4832,15 @@ export function updateSmartTransaction( ]); } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -4860,12 +4869,15 @@ export function cancelSmartTransaction( await submitRequestToBackground('cancelSmartTransaction', [uuid]); } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } diff --git a/yarn.lock b/yarn.lock index 2a52221beb3c..52894233bfab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18486,15 +18486,6 @@ __metadata: languageName: node linkType: hard -"eth-rpc-errors@npm:^4.0.2": - version: 4.0.3 - resolution: "eth-rpc-errors@npm:4.0.3" - dependencies: - fast-safe-stringify: "npm:^2.0.6" - checksum: 10/47ce14170eabaee51ab1cc7e643bb3ef96ee6b15c6404806aedcd51750e00ae0b1a12c37785b180679b8d452b6dd44a0240bb018d01fa73efc85fcfa808b35a7 - languageName: node - linkType: hard - "ethereum-cryptography@npm:^0.1.3": version: 0.1.3 resolution: "ethereum-cryptography@npm:0.1.3" @@ -24186,16 +24177,6 @@ __metadata: languageName: node linkType: hard -"json-rpc-engine@npm:^6.1.0": - version: 6.1.0 - resolution: "json-rpc-engine@npm:6.1.0" - dependencies: - "@metamask/safe-event-emitter": "npm:^2.0.0" - eth-rpc-errors: "npm:^4.0.2" - checksum: 10/00d5b5228e90f126dd52176598db6e5611d295d3a3f7be21254c30c1b6555811260ef2ec2df035cd8e583e4b12096259da721e29f4ea2affb615f7dfc960a6a6 - languageName: node - linkType: hard - "json-rpc-middleware-stream@npm:^5.0.1": version: 5.0.1 resolution: "json-rpc-middleware-stream@npm:5.0.1" @@ -26146,6 +26127,7 @@ __metadata: "@metamask/forwarder": "npm:^1.1.0" "@metamask/gas-fee-controller": "npm:^18.0.0" "@metamask/jazzicon": "npm:^2.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.0" "@metamask/keyring-api": "npm:^8.1.3" "@metamask/keyring-controller": "npm:^17.2.2" "@metamask/logging-controller": "npm:^6.0.0" @@ -26364,7 +26346,6 @@ __metadata: jest-environment-jsdom: "patch:jest-environment-jsdom@npm%3A29.7.0#~/.yarn/patches/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b.patch" jest-junit: "npm:^14.0.1" jsdom: "npm:^16.7.0" - json-rpc-engine: "npm:^6.1.0" json-rpc-middleware-stream: "npm:^5.0.1" json-schema-to-ts: "npm:^3.0.1" koa: "npm:^2.7.0" From 36bde61d3e8697b889c7cf4dbede6e2240f5880b Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Fri, 18 Oct 2024 14:27:22 +0100 Subject: [PATCH 39/51] feat: Convert AppStateController to typescript (#27572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** As a prerequisite for migrating AppStateController to BaseController v2, and to support the TypeScript migration effort for the extension, we want to convert AppStateController to TypeScript along with its tests. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27572?quickstart=1) ## **Related issues** Fixes: #25922 ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .eslintrc.js | 2 +- .../controllers/app-state-controller.test.ts | 730 +++++++++++++++ .../controllers/app-state-controller.ts | 874 ++++++++++++++++++ app/scripts/controllers/app-state.d.ts | 24 - app/scripts/controllers/app-state.js | 651 ------------- app/scripts/controllers/app-state.test.js | 396 -------- .../controllers/mmi-controller.test.ts | 12 +- app/scripts/controllers/mmi-controller.ts | 4 +- app/scripts/lib/ppom/ppom-middleware.ts | 2 +- app/scripts/lib/ppom/ppom-util.test.ts | 2 +- app/scripts/lib/ppom/ppom-util.ts | 2 +- app/scripts/metamask-controller.js | 4 +- .../files-to-convert.json | 1 - shared/constants/mmi-controller.ts | 2 +- 14 files changed, 1619 insertions(+), 1087 deletions(-) create mode 100644 app/scripts/controllers/app-state-controller.test.ts create mode 100644 app/scripts/controllers/app-state-controller.ts delete mode 100644 app/scripts/controllers/app-state.d.ts delete mode 100644 app/scripts/controllers/app-state.js delete mode 100644 app/scripts/controllers/app-state.test.js diff --git a/.eslintrc.js b/.eslintrc.js index 97d52b6637cc..846158a741ef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -307,7 +307,7 @@ module.exports = { { files: [ '**/__snapshots__/*.snap', - 'app/scripts/controllers/app-state.test.js', + 'app/scripts/controllers/app-state-controller.test.ts', 'app/scripts/controllers/mmi-controller.test.ts', 'app/scripts/controllers/alert-controller.test.ts', 'app/scripts/metamask-controller.actions.test.js', diff --git a/app/scripts/controllers/app-state-controller.test.ts b/app/scripts/controllers/app-state-controller.test.ts new file mode 100644 index 000000000000..740c4a7d33f8 --- /dev/null +++ b/app/scripts/controllers/app-state-controller.test.ts @@ -0,0 +1,730 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import { Browser } from 'webextension-polyfill'; +import { + ENVIRONMENT_TYPE_POPUP, + ORIGIN_METAMASK, + POLLING_TOKEN_ENVIRONMENT_TYPES, +} from '../../../shared/constants/app'; +import { AppStateController } from './app-state-controller'; +import type { + AllowedActions, + AllowedEvents, + AppStateControllerActions, + AppStateControllerEvents, + AppStateControllerState, +} from './app-state-controller'; +import { PreferencesControllerState } from './preferences-controller'; + +jest.mock('webextension-polyfill'); + +const mockIsManifestV3 = jest.fn().mockReturnValue(false); +jest.mock('../../../shared/modules/mv3.utils', () => ({ + get isManifestV3() { + return mockIsManifestV3(); + }, +})); + +let appStateController: AppStateController; +let controllerMessenger: ControllerMessenger< + AppStateControllerActions | AllowedActions, + AppStateControllerEvents | AllowedEvents +>; + +const extensionMock = { + alarms: { + getAll: jest.fn(() => Promise.resolve([])), + create: jest.fn(), + clear: jest.fn(), + onAlarm: { + addListener: jest.fn(), + }, + }, +} as unknown as jest.Mocked; + +describe('AppStateController', () => { + const createAppStateController = ( + initState: Partial = {}, + ): { + appStateController: AppStateController; + controllerMessenger: typeof controllerMessenger; + } => { + controllerMessenger = new ControllerMessenger(); + jest.spyOn(ControllerMessenger.prototype, 'call'); + const appStateMessenger = controllerMessenger.getRestricted({ + name: 'AppStateController', + allowedActions: [ + `ApprovalController:addRequest`, + `ApprovalController:acceptRequest`, + `PreferencesController:getState`, + ], + allowedEvents: [ + `PreferencesController:stateChange`, + `KeyringController:qrKeyringStateChange`, + ], + }); + controllerMessenger.registerActionHandler( + 'PreferencesController:getState', + jest.fn().mockReturnValue({ + preferences: { + autoLockTimeLimit: 0, + }, + }), + ); + controllerMessenger.registerActionHandler( + 'ApprovalController:addRequest', + jest.fn().mockReturnValue({ + catch: jest.fn(), + }), + ); + appStateController = new AppStateController({ + addUnlockListener: jest.fn(), + isUnlocked: jest.fn(() => true), + initState, + onInactiveTimeout: jest.fn(), + messenger: appStateMessenger, + extension: extensionMock, + }); + + return { appStateController, controllerMessenger }; + }; + + const createIsUnlockedMock = (isUnlocked: boolean) => { + return jest + .spyOn( + appStateController as unknown as { isUnlocked: () => boolean }, + 'isUnlocked', + ) + .mockReturnValue(isUnlocked); + }; + + beforeEach(() => { + ({ appStateController } = createAppStateController()); + }); + + describe('setOutdatedBrowserWarningLastShown', () => { + it('sets the last shown time', () => { + ({ appStateController } = createAppStateController()); + const timestamp: number = Date.now(); + + appStateController.setOutdatedBrowserWarningLastShown(timestamp); + + expect( + appStateController.store.getState().outdatedBrowserWarningLastShown, + ).toStrictEqual(timestamp); + }); + + it('sets outdated browser warning last shown timestamp', () => { + const lastShownTimestamp: number = Date.now(); + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + outdatedBrowserWarningLastShown: lastShownTimestamp, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('getUnlockPromise', () => { + it('waits for unlock if the extension is locked', async () => { + ({ appStateController } = createAppStateController()); + const isUnlockedMock = createIsUnlockedMock(false); + const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock'); + + appStateController.getUnlockPromise(true); + expect(isUnlockedMock).toHaveBeenCalled(); + expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true); + }); + + it('resolves immediately if the extension is already unlocked', async () => { + ({ appStateController } = createAppStateController()); + const isUnlockedMock = createIsUnlockedMock(true); + + await expect( + appStateController.getUnlockPromise(false), + ).resolves.toBeUndefined(); + + expect(isUnlockedMock).toHaveBeenCalled(); + }); + }); + + describe('waitForUnlock', () => { + it('resolves immediately if already unlocked', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn: () => void = jest.fn(); + appStateController.waitForUnlock(resolveFn, false); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(controllerMessenger.call).toHaveBeenCalledTimes(1); + }); + + it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => { + createIsUnlockedMock(false); + + const resolveFn: () => void = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + expect(controllerMessenger.call).toHaveBeenCalledTimes(2); + expect(controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + expect.objectContaining({ + id: expect.any(String), + origin: ORIGIN_METAMASK, + type: 'unlock', + }), + true, + ); + }); + }); + + describe('handleUnlock', () => { + beforeEach(() => { + createIsUnlockedMock(false); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('accepts approval request revolving all the related promises', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn: () => void = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + appStateController.handleUnlock(); + + expect(emitSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(controllerMessenger.call).toHaveBeenCalled(); + expect(controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + expect.any(String), + ); + }); + }); + + describe('setDefaultHomeActiveTabName', () => { + it('sets the default home tab name', () => { + appStateController.setDefaultHomeActiveTabName('testTabName'); + expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( + 'testTabName', + ); + }); + }); + + describe('setConnectedStatusPopoverHasBeenShown', () => { + it('sets connected status popover as shown', () => { + appStateController.setConnectedStatusPopoverHasBeenShown(); + expect( + appStateController.store.getState().connectedStatusPopoverHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderHasBeenShown', () => { + it('sets recovery phrase reminder as shown', () => { + appStateController.setRecoveryPhraseReminderHasBeenShown(); + expect( + appStateController.store.getState().recoveryPhraseReminderHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderLastShown', () => { + it('sets the last shown time of recovery phrase reminder', () => { + const timestamp: number = Date.now(); + appStateController.setRecoveryPhraseReminderLastShown(timestamp); + + expect( + appStateController.store.getState().recoveryPhraseReminderLastShown, + ).toBe(timestamp); + }); + }); + + describe('setLastActiveTime', () => { + it('sets the last active time to the current time', () => { + const spy = jest.spyOn( + appStateController as unknown as { _resetTimer: () => void }, + '_resetTimer', + ); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + }); + + it('sets the timer if timeoutMinutes is set', () => { + const timeout = Date.now(); + controllerMessenger.publish( + 'PreferencesController:stateChange', + { + preferences: { autoLockTimeLimit: timeout }, + } as unknown as PreferencesControllerState, + [], + ); + const spy = jest.spyOn( + appStateController as unknown as { _resetTimer: () => void }, + '_resetTimer', + ); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('setBrowserEnvironment', () => { + it('sets the current browser and OS environment', () => { + appStateController.setBrowserEnvironment('Windows', 'Chrome'); + expect( + appStateController.store.getState().browserEnvironment, + ).toStrictEqual({ + os: 'Windows', + browser: 'Chrome', + }); + }); + }); + + describe('addPollingToken', () => { + it('adds a pollingToken for a given environmentType', () => { + const pollingTokenType = + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_POPUP]; + appStateController.addPollingToken('token1', pollingTokenType); + expect(appStateController.store.getState()[pollingTokenType]).toContain( + 'token1', + ); + }); + }); + + describe('removePollingToken', () => { + it('removes a pollingToken for a given environmentType', () => { + const pollingTokenType = + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_POPUP]; + appStateController.addPollingToken('token1', pollingTokenType); + appStateController.removePollingToken('token1', pollingTokenType); + expect( + appStateController.store.getState()[pollingTokenType], + ).not.toContain('token1'); + }); + }); + + describe('clearPollingTokens', () => { + it('clears all pollingTokens', () => { + appStateController.addPollingToken('token1', 'popupGasPollTokens'); + appStateController.addPollingToken('token2', 'notificationGasPollTokens'); + appStateController.addPollingToken('token3', 'fullScreenGasPollTokens'); + appStateController.clearPollingTokens(); + + expect( + appStateController.store.getState().popupGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().notificationGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().fullScreenGasPollTokens, + ).toStrictEqual([]); + }); + }); + + describe('setShowTestnetMessageInDropdown', () => { + it('sets whether the testnet dismissal link should be shown in the network dropdown', () => { + appStateController.setShowTestnetMessageInDropdown(true); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(true); + + appStateController.setShowTestnetMessageInDropdown(false); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(false); + }); + }); + + describe('setShowBetaHeader', () => { + it('sets whether the beta notification heading on the home page', () => { + appStateController.setShowBetaHeader(true); + expect(appStateController.store.getState().showBetaHeader).toBe(true); + + appStateController.setShowBetaHeader(false); + expect(appStateController.store.getState().showBetaHeader).toBe(false); + }); + }); + + describe('setCurrentPopupId', () => { + it('sets the currentPopupId in the appState', () => { + const popupId = 12345; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.store.getState().currentPopupId).toBe(popupId); + }); + }); + + describe('getCurrentPopupId', () => { + it('retrieves the currentPopupId saved in the appState', () => { + const popupId = 54321; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.getCurrentPopupId()).toBe(popupId); + }); + }); + + describe('setFirstTimeUsedNetwork', () => { + it('updates the array of the first time used networks', () => { + const chainId = '0x1'; + + appStateController.setFirstTimeUsedNetwork(chainId); + expect(appStateController.store.getState().usedNetworks[chainId]).toBe( + true, + ); + }); + }); + + describe('setLastInteractedConfirmationInfo', () => { + it('sets information about last confirmation user has interacted with', () => { + const lastInteractedConfirmationInfo = { + id: '123', + chainId: '0x1', + timestamp: new Date().getTime(), + }; + appStateController.setLastInteractedConfirmationInfo( + lastInteractedConfirmationInfo, + ); + expect(appStateController.getLastInteractedConfirmationInfo()).toBe( + lastInteractedConfirmationInfo, + ); + + appStateController.setLastInteractedConfirmationInfo(undefined); + expect(appStateController.getLastInteractedConfirmationInfo()).toBe( + undefined, + ); + }); + }); + + describe('setSnapsInstallPrivacyWarningShownStatus', () => { + it('updates the status of snaps install privacy warning', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setSnapsInstallPrivacyWarningShownStatus(true); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + snapsInstallPrivacyWarningShown: true, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('institutional', () => { + it('set the interactive replacement token with a url and the old refresh token', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = { + url: 'https://example.com', + oldRefreshToken: 'old', + }; + + appStateController.showInteractiveReplacementTokenBanner(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + interactiveReplacementToken: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + + it('set the setCustodianDeepLink with the fromAddress and custodyId', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = { + fromAddress: '0x', + custodyId: 'custodyId', + }; + + appStateController.setCustodianDeepLink(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + custodianDeepLink: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + + it('set the setNoteToTraderMessage with a message', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = 'some message'; + + appStateController.setNoteToTraderMessage(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + noteToTraderMessage: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setSurveyLinkLastClickedOrClosed', () => { + it('set the surveyLinkLastClickedOrClosed time', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = Date.now(); + + appStateController.setSurveyLinkLastClickedOrClosed(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + surveyLinkLastClickedOrClosed: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setOnboardingDate', () => { + it('set the onboardingDate', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setOnboardingDate(); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setLastViewedUserSurvey', () => { + it('set the lastViewedUserSurvey with id 1', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = 1; + + appStateController.setLastViewedUserSurvey(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + lastViewedUserSurvey: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setNewPrivacyPolicyToastClickedOrClosed', () => { + it('set the newPrivacyPolicyToastClickedOrClosed to true', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setNewPrivacyPolicyToastClickedOrClosed(); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect( + appStateController.store.getState() + .newPrivacyPolicyToastClickedOrClosed, + ).toStrictEqual(true); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setNewPrivacyPolicyToastShownDate', () => { + it('set the newPrivacyPolicyToastShownDate', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = Date.now(); + + appStateController.setNewPrivacyPolicyToastShownDate(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + newPrivacyPolicyToastShownDate: mockParams, + }); + expect( + appStateController.store.getState().newPrivacyPolicyToastShownDate, + ).toStrictEqual(mockParams); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setTermsOfUseLastAgreed', () => { + it('set the termsOfUseLastAgreed timestamp', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = Date.now(); + + appStateController.setTermsOfUseLastAgreed(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + termsOfUseLastAgreed: mockParams, + }); + expect( + appStateController.store.getState().termsOfUseLastAgreed, + ).toStrictEqual(mockParams); + + updateStateSpy.mockRestore(); + }); + }); + + describe('onPreferencesStateChange', () => { + it('should update the timeoutMinutes with the autoLockTimeLimit', () => { + ({ appStateController, controllerMessenger } = + createAppStateController()); + const timeout = Date.now(); + + controllerMessenger.publish( + 'PreferencesController:stateChange', + { + preferences: { autoLockTimeLimit: timeout }, + } as unknown as PreferencesControllerState, + [], + ); + + expect(appStateController.store.getState().timeoutMinutes).toStrictEqual( + timeout, + ); + }); + }); + + describe('isManifestV3', () => { + it('creates alarm when isManifestV3 is true', () => { + mockIsManifestV3.mockReturnValue(true); + ({ appStateController } = createAppStateController()); + + const timeout = Date.now(); + controllerMessenger.publish( + 'PreferencesController:stateChange', + { + preferences: { autoLockTimeLimit: timeout }, + } as unknown as PreferencesControllerState, + [], + ); + const spy = jest.spyOn( + appStateController as unknown as { _resetTimer: () => void }, + '_resetTimer', + ); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + expect(extensionMock.alarms.clear).toHaveBeenCalled(); + expect(extensionMock.alarms.onAlarm.addListener).toHaveBeenCalled(); + }); + }); + + describe('AppStateController:getState', () => { + it('should return the current state of the property', () => { + expect( + appStateController.store.getState().recoveryPhraseReminderHasBeenShown, + ).toStrictEqual(false); + expect( + controllerMessenger.call('AppStateController:getState') + .recoveryPhraseReminderHasBeenShown, + ).toStrictEqual(false); + }); + }); + + describe('AppStateController:stateChange', () => { + it('subscribers will recieve the state when published', () => { + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(null); + const timeNow = Date.now(); + controllerMessenger.subscribe( + 'AppStateController:stateChange', + (state: Partial) => { + if (typeof state.surveyLinkLastClickedOrClosed === 'number') { + appStateController.setSurveyLinkLastClickedOrClosed( + state.surveyLinkLastClickedOrClosed, + ); + } + }, + ); + + controllerMessenger.publish( + 'AppStateController:stateChange', + { + surveyLinkLastClickedOrClosed: timeNow, + } as unknown as AppStateControllerState, + [], + ); + + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + expect( + controllerMessenger.call('AppStateController:getState') + .surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + }); + + it('state will be published when there is state change', () => { + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(null); + const timeNow = Date.now(); + controllerMessenger.subscribe( + 'AppStateController:stateChange', + (state: Partial) => { + expect(state.surveyLinkLastClickedOrClosed).toStrictEqual(timeNow); + }, + ); + + appStateController.setSurveyLinkLastClickedOrClosed(timeNow); + + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + expect( + controllerMessenger.call('AppStateController:getState') + .surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + }); + }); +}); diff --git a/app/scripts/controllers/app-state-controller.ts b/app/scripts/controllers/app-state-controller.ts new file mode 100644 index 000000000000..e76b8fe3888e --- /dev/null +++ b/app/scripts/controllers/app-state-controller.ts @@ -0,0 +1,874 @@ +import EventEmitter from 'events'; +import { ObservableStore } from '@metamask/obs-store'; +import { v4 as uuid } from 'uuid'; +import log from 'loglevel'; +import { ApprovalType } from '@metamask/controller-utils'; +import { KeyringControllerQRKeyringStateChangeEvent } from '@metamask/keyring-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + AcceptRequest, + AddApprovalRequest, +} from '@metamask/approval-controller'; +import { Json } from '@metamask/utils'; +import { Browser } from 'webextension-polyfill'; +import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; +import { MINUTE } from '../../../shared/constants/time'; +import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; +import { isManifestV3 } from '../../../shared/modules/mv3.utils'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { isBeta } from '../../../ui/helpers/utils/build-types'; +import { + ENVIRONMENT_TYPE_BACKGROUND, + POLLING_TOKEN_ENVIRONMENT_TYPES, + ORIGIN_METAMASK, +} from '../../../shared/constants/app'; +import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; +import { LastInteractedConfirmationInfo } from '../../../shared/types/confirm'; +import { SecurityAlertResponse } from '../lib/ppom/types'; +import type { + Preferences, + PreferencesControllerGetStateAction, + PreferencesControllerStateChangeEvent, +} from './preferences-controller'; + +export type AppStateControllerState = { + timeoutMinutes: number; + connectedStatusPopoverHasBeenShown: boolean; + defaultHomeActiveTabName: string | null; + browserEnvironment: Record; + popupGasPollTokens: string[]; + notificationGasPollTokens: string[]; + fullScreenGasPollTokens: string[]; + recoveryPhraseReminderHasBeenShown: boolean; + recoveryPhraseReminderLastShown: number; + outdatedBrowserWarningLastShown: number | null; + nftsDetectionNoticeDismissed: boolean; + showTestnetMessageInDropdown: boolean; + showBetaHeader: boolean; + showPermissionsTour: boolean; + showNetworkBanner: boolean; + showAccountBanner: boolean; + trezorModel: string | null; + currentPopupId?: number; + onboardingDate: number | null; + lastViewedUserSurvey: number | null; + newPrivacyPolicyToastClickedOrClosed: boolean | null; + newPrivacyPolicyToastShownDate: number | null; + // This key is only used for checking if the user had set advancedGasFee + // prior to Migration 92.3 where we split out the setting to support + // multiple networks. + hadAdvancedGasFeesSetPriorToMigration92_3: boolean; + qrHardware: Json; + nftsDropdownState: Json; + usedNetworks: Record; + surveyLinkLastClickedOrClosed: number | null; + signatureSecurityAlertResponses: Record; + // States used for displaying the changed network toast + switchedNetworkDetails: Record | null; + switchedNetworkNeverShowMessage: boolean; + currentExtensionPopupId: number; + lastInteractedConfirmationInfo?: LastInteractedConfirmationInfo; + termsOfUseLastAgreed?: number; + snapsInstallPrivacyWarningShown?: boolean; + interactiveReplacementToken?: { url: string; oldRefreshToken: string }; + noteToTraderMessage?: string; + custodianDeepLink?: { fromAddress: string; custodyId: string }; +}; + +const controllerName = 'AppStateController'; + +/** + * Returns the state of the {@link AppStateController}. + */ +export type AppStateControllerGetStateAction = { + type: 'AppStateController:getState'; + handler: () => AppStateControllerState; +}; + +/** + * Actions exposed by the {@link AppStateController}. + */ +export type AppStateControllerActions = AppStateControllerGetStateAction; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | AddApprovalRequest + | AcceptRequest + | PreferencesControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AppStateController} changes. + */ +export type AppStateControllerStateChangeEvent = { + type: 'AppStateController:stateChange'; + payload: [AppStateControllerState, []]; +}; + +/** + * Events emitted by {@link AppStateController}. + */ +export type AppStateControllerEvents = AppStateControllerStateChangeEvent; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = + | PreferencesControllerStateChangeEvent + | KeyringControllerQRKeyringStateChangeEvent; + +export type AppStateControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AppStateControllerActions | AllowedActions, + AppStateControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +type PollingTokenType = + | 'popupGasPollTokens' + | 'notificationGasPollTokens' + | 'fullScreenGasPollTokens'; + +type AppStateControllerInitState = Partial< + Omit< + AppStateControllerState, + | 'qrHardware' + | 'nftsDropdownState' + | 'usedNetworks' + | 'surveyLinkLastClickedOrClosed' + | 'signatureSecurityAlertResponses' + | 'switchedNetworkDetails' + | 'switchedNetworkNeverShowMessage' + | 'currentExtensionPopupId' + > +>; + +type AppStateControllerOptions = { + addUnlockListener: (callback: () => void) => void; + isUnlocked: () => boolean; + initState?: AppStateControllerInitState; + onInactiveTimeout?: () => void; + messenger: AppStateControllerMessenger; + extension: Browser; +}; + +const getDefaultAppStateControllerState = ( + initState?: AppStateControllerInitState, +): AppStateControllerState => ({ + timeoutMinutes: DEFAULT_AUTO_LOCK_TIME_LIMIT, + connectedStatusPopoverHasBeenShown: true, + defaultHomeActiveTabName: null, + browserEnvironment: {}, + popupGasPollTokens: [], + notificationGasPollTokens: [], + fullScreenGasPollTokens: [], + recoveryPhraseReminderHasBeenShown: false, + recoveryPhraseReminderLastShown: new Date().getTime(), + outdatedBrowserWarningLastShown: null, + nftsDetectionNoticeDismissed: false, + showTestnetMessageInDropdown: true, + showBetaHeader: isBeta(), + showPermissionsTour: true, + showNetworkBanner: true, + showAccountBanner: true, + trezorModel: null, + onboardingDate: null, + lastViewedUserSurvey: null, + newPrivacyPolicyToastClickedOrClosed: null, + newPrivacyPolicyToastShownDate: null, + hadAdvancedGasFeesSetPriorToMigration92_3: false, + ...initState, + qrHardware: {}, + nftsDropdownState: {}, + usedNetworks: { + '0x1': true, + '0x5': true, + '0x539': true, + }, + surveyLinkLastClickedOrClosed: null, + signatureSecurityAlertResponses: {}, + switchedNetworkDetails: null, + switchedNetworkNeverShowMessage: false, + currentExtensionPopupId: 0, +}); + +export class AppStateController extends EventEmitter { + private readonly extension: AppStateControllerOptions['extension']; + + private readonly onInactiveTimeout: () => void; + + store: ObservableStore; + + private timer: NodeJS.Timeout | null; + + isUnlocked: () => boolean; + + private readonly waitingForUnlock: { resolve: () => void }[]; + + private readonly messagingSystem: AppStateControllerMessenger; + + #approvalRequestId: string | null; + + constructor(opts: AppStateControllerOptions) { + const { + addUnlockListener, + isUnlocked, + initState, + onInactiveTimeout, + messenger, + extension, + } = opts; + super(); + + this.extension = extension; + this.onInactiveTimeout = onInactiveTimeout || (() => undefined); + this.store = new ObservableStore( + getDefaultAppStateControllerState(initState), + ); + this.timer = null; + + this.isUnlocked = isUnlocked; + this.waitingForUnlock = []; + addUnlockListener(this.handleUnlock.bind(this)); + + messenger.subscribe( + 'PreferencesController:stateChange', + ({ preferences }: { preferences: Partial }) => { + const currentState = this.store.getState(); + if ( + typeof preferences?.autoLockTimeLimit === 'number' && + currentState.timeoutMinutes !== preferences.autoLockTimeLimit + ) { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + }, + ); + + messenger.subscribe( + 'KeyringController:qrKeyringStateChange', + (qrHardware: Json) => + this.store.updateState({ + qrHardware, + }), + ); + + const { preferences } = messenger.call('PreferencesController:getState'); + if (typeof preferences.autoLockTimeLimit === 'number') { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + + this.messagingSystem = messenger; + this.messagingSystem.registerActionHandler( + 'AppStateController:getState', + () => this.store.getState(), + ); + this.store.subscribe((state: AppStateControllerState) => { + this.messagingSystem.publish('AppStateController:stateChange', state, []); + }); + this.#approvalRequestId = null; + } + + /** + * Get a Promise that resolves when the extension is unlocked. + * This Promise will never reject. + * + * @param shouldShowUnlockRequest - Whether the extension notification + * popup should be opened. + * @returns A promise that resolves when the extension is + * unlocked, or immediately if the extension is already unlocked. + */ + getUnlockPromise(shouldShowUnlockRequest: boolean): Promise { + return new Promise((resolve) => { + if (this.isUnlocked()) { + resolve(); + } else { + this.waitForUnlock(resolve, shouldShowUnlockRequest); + } + }); + } + + /** + * Adds a Promise's resolve function to the waitingForUnlock queue. + * Also opens the extension popup if specified. + * + * @param resolve - A Promise's resolve function that will + * be called when the extension is unlocked. + * @param shouldShowUnlockRequest - Whether the extension notification + * popup should be opened. + */ + waitForUnlock(resolve: () => void, shouldShowUnlockRequest: boolean): void { + this.waitingForUnlock.push({ resolve }); + this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); + if (shouldShowUnlockRequest) { + this._requestApproval(); + } + } + + /** + * Drains the waitingForUnlock queue, resolving all the related Promises. + */ + handleUnlock(): void { + if (this.waitingForUnlock.length > 0) { + while (this.waitingForUnlock.length > 0) { + this.waitingForUnlock.shift()?.resolve(); + } + this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); + } + + this._acceptApproval(); + } + + /** + * Sets the default home tab + * + * @param defaultHomeActiveTabName - the tab name + */ + setDefaultHomeActiveTabName(defaultHomeActiveTabName: string | null): void { + this.store.updateState({ + defaultHomeActiveTabName, + }); + } + + /** + * Record that the user has seen the connected status info popover + */ + setConnectedStatusPopoverHasBeenShown(): void { + this.store.updateState({ + connectedStatusPopoverHasBeenShown: true, + }); + } + + /** + * Record that the user has been shown the recovery phrase reminder. + */ + setRecoveryPhraseReminderHasBeenShown(): void { + this.store.updateState({ + recoveryPhraseReminderHasBeenShown: true, + }); + } + + setSurveyLinkLastClickedOrClosed(time: number): void { + this.store.updateState({ + surveyLinkLastClickedOrClosed: time, + }); + } + + setOnboardingDate(): void { + this.store.updateState({ + onboardingDate: Date.now(), + }); + } + + setLastViewedUserSurvey(id: number) { + this.store.updateState({ + lastViewedUserSurvey: id, + }); + } + + setNewPrivacyPolicyToastClickedOrClosed(): void { + this.store.updateState({ + newPrivacyPolicyToastClickedOrClosed: true, + }); + } + + setNewPrivacyPolicyToastShownDate(time: number): void { + this.store.updateState({ + newPrivacyPolicyToastShownDate: time, + }); + } + + /** + * Record the timestamp of the last time the user has seen the recovery phrase reminder + * + * @param lastShown - timestamp when user was last shown the reminder. + */ + setRecoveryPhraseReminderLastShown(lastShown: number): void { + this.store.updateState({ + recoveryPhraseReminderLastShown: lastShown, + }); + } + + /** + * Record the timestamp of the last time the user has acceoted the terms of use + * + * @param lastAgreed - timestamp when user last accepted the terms of use + */ + setTermsOfUseLastAgreed(lastAgreed: number): void { + this.store.updateState({ + termsOfUseLastAgreed: lastAgreed, + }); + } + + /** + * Record if popover for snaps privacy warning has been shown + * on the first install of a snap. + * + * @param shown - shown status + */ + setSnapsInstallPrivacyWarningShownStatus(shown: boolean): void { + this.store.updateState({ + snapsInstallPrivacyWarningShown: shown, + }); + } + + /** + * Record the timestamp of the last time the user has seen the outdated browser warning + * + * @param lastShown - Timestamp (in milliseconds) of when the user was last shown the warning. + */ + setOutdatedBrowserWarningLastShown(lastShown: number): void { + this.store.updateState({ + outdatedBrowserWarningLastShown: lastShown, + }); + } + + /** + * Sets the last active time to the current time. + */ + setLastActiveTime(): void { + this._resetTimer(); + } + + /** + * Sets the inactive timeout for the app + * + * @param timeoutMinutes - The inactive timeout in minutes. + */ + private _setInactiveTimeout(timeoutMinutes: number): void { + this.store.updateState({ + timeoutMinutes, + }); + + this._resetTimer(); + } + + /** + * Resets the internal inactive timer + * + * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new + * timer will not be created. + * + */ + private _resetTimer(): void { + const { timeoutMinutes } = this.store.getState(); + + if (this.timer) { + clearTimeout(this.timer); + } else if (isManifestV3) { + this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); + } + + if (!timeoutMinutes) { + return; + } + + // This is a temporary fix until we add a state migration. + // Due to a bug in ui/pages/settings/advanced-tab/advanced-tab.component.js, + // it was possible for timeoutMinutes to be saved as a string, as explained + // in PR 25109. `alarms.create` will fail in that case. We are + // converting this to a number here to prevent that failure. Once + // we add a migration to update the malformed state to the right type, + // we will remove this conversion. + const timeoutToSet = Number(timeoutMinutes); + + if (isManifestV3) { + this.extension.alarms.create(AUTO_LOCK_TIMEOUT_ALARM, { + delayInMinutes: timeoutToSet, + periodInMinutes: timeoutToSet, + }); + this.extension.alarms.onAlarm.addListener( + (alarmInfo: { name: string }) => { + if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) { + this.onInactiveTimeout(); + this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); + } + }, + ); + } else { + this.timer = setTimeout( + () => this.onInactiveTimeout(), + timeoutToSet * MINUTE, + ); + } + } + + /** + * Sets the current browser and OS environment + * + * @param os + * @param browser + */ + setBrowserEnvironment(os: string, browser: string): void { + this.store.updateState({ browserEnvironment: { os, browser } }); + } + + /** + * Adds a pollingToken for a given environmentType + * + * @param pollingToken + * @param pollingTokenType + */ + addPollingToken( + pollingToken: string, + pollingTokenType: PollingTokenType, + ): void { + if ( + pollingTokenType.toString() !== + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] + ) { + if (this.#isValidPollingTokenType(pollingTokenType)) { + this.#updatePollingTokens(pollingToken, pollingTokenType); + } + } + } + + /** + * Updates the polling token in the state. + * + * @param pollingToken + * @param pollingTokenType + */ + #updatePollingTokens( + pollingToken: string, + pollingTokenType: PollingTokenType, + ) { + const currentTokens: string[] = this.store.getState()[pollingTokenType]; + this.store.updateState({ + [pollingTokenType]: [...currentTokens, pollingToken], + }); + } + + /** + * removes a pollingToken for a given environmentType + * + * @param pollingToken + * @param pollingTokenType + */ + removePollingToken( + pollingToken: string, + pollingTokenType: PollingTokenType, + ): void { + if ( + pollingTokenType.toString() !== + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] + ) { + const currentTokens: string[] = this.store.getState()[pollingTokenType]; + if (this.#isValidPollingTokenType(pollingTokenType)) { + this.store.updateState({ + [pollingTokenType]: currentTokens.filter( + (token: string) => token !== pollingToken, + ), + }); + } + } + } + + /** + * Validates whether the given polling token type is a valid one. + * + * @param pollingTokenType + * @returns true if valid, false otherwise. + */ + #isValidPollingTokenType(pollingTokenType: PollingTokenType): boolean { + const validTokenTypes: PollingTokenType[] = [ + 'popupGasPollTokens', + 'notificationGasPollTokens', + 'fullScreenGasPollTokens', + ]; + + return validTokenTypes.includes(pollingTokenType); + } + + /** + * clears all pollingTokens + */ + clearPollingTokens(): void { + this.store.updateState({ + popupGasPollTokens: [], + notificationGasPollTokens: [], + fullScreenGasPollTokens: [], + }); + } + + /** + * Sets whether the testnet dismissal link should be shown in the network dropdown + * + * @param showTestnetMessageInDropdown + */ + setShowTestnetMessageInDropdown(showTestnetMessageInDropdown: boolean): void { + this.store.updateState({ showTestnetMessageInDropdown }); + } + + /** + * Sets whether the beta notification heading on the home page + * + * @param showBetaHeader + */ + setShowBetaHeader(showBetaHeader: boolean): void { + this.store.updateState({ showBetaHeader }); + } + + /** + * Sets whether the permissions tour should be shown to the user + * + * @param showPermissionsTour + */ + setShowPermissionsTour(showPermissionsTour: boolean): void { + this.store.updateState({ showPermissionsTour }); + } + + /** + * Sets whether the Network Banner should be shown + * + * @param showNetworkBanner + */ + setShowNetworkBanner(showNetworkBanner: boolean): void { + this.store.updateState({ showNetworkBanner }); + } + + /** + * Sets whether the Account Banner should be shown + * + * @param showAccountBanner + */ + setShowAccountBanner(showAccountBanner: boolean): void { + this.store.updateState({ showAccountBanner }); + } + + /** + * Sets a unique ID for the current extension popup + * + * @param currentExtensionPopupId + */ + setCurrentExtensionPopupId(currentExtensionPopupId: number): void { + this.store.updateState({ currentExtensionPopupId }); + } + + /** + * Sets an object with networkName and appName + * or `null` if the message is meant to be cleared + * + * @param switchedNetworkDetails - Details about the network that MetaMask just switched to. + */ + setSwitchedNetworkDetails( + switchedNetworkDetails: { origin: string; networkClientId: string } | null, + ): void { + this.store.updateState({ switchedNetworkDetails }); + } + + /** + * Clears the switched network details in state + */ + clearSwitchedNetworkDetails(): void { + this.store.updateState({ switchedNetworkDetails: null }); + } + + /** + * Remembers if the user prefers to never see the + * network switched message again + * + * @param switchedNetworkNeverShowMessage + */ + setSwitchedNetworkNeverShowMessage( + switchedNetworkNeverShowMessage: boolean, + ): void { + this.store.updateState({ + switchedNetworkDetails: null, + switchedNetworkNeverShowMessage, + }); + } + + /** + * Sets a property indicating the model of the user's Trezor hardware wallet + * + * @param trezorModel - The Trezor model. + */ + setTrezorModel(trezorModel: string | null): void { + this.store.updateState({ trezorModel }); + } + + /** + * A setter for the `nftsDropdownState` property + * + * @param nftsDropdownState + */ + updateNftDropDownState(nftsDropdownState: Json): void { + this.store.updateState({ + nftsDropdownState, + }); + } + + /** + * Updates the array of the first time used networks + * + * @param chainId + */ + setFirstTimeUsedNetwork(chainId: string): void { + const currentState = this.store.getState(); + const { usedNetworks } = currentState; + usedNetworks[chainId] = true; + + this.store.updateState({ usedNetworks }); + } + + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + /** + * Set the interactive replacement token with a url and the old refresh token + * + * @param opts + * @param opts.url + * @param opts.oldRefreshToken + */ + showInteractiveReplacementTokenBanner({ + url, + oldRefreshToken, + }: { + url: string; + oldRefreshToken: string; + }): void { + this.store.updateState({ + interactiveReplacementToken: { + url, + oldRefreshToken, + }, + }); + } + + /** + * Set the setCustodianDeepLink with the fromAddress and custodyId + * + * @param opts + * @param opts.fromAddress + * @param opts.custodyId + */ + setCustodianDeepLink({ + fromAddress, + custodyId, + }: { + fromAddress: string; + custodyId: string; + }): void { + this.store.updateState({ + custodianDeepLink: { fromAddress, custodyId }, + }); + } + + setNoteToTraderMessage(message: string): void { + this.store.updateState({ + noteToTraderMessage: message, + }); + } + + ///: END:ONLY_INCLUDE_IF + + getSignatureSecurityAlertResponse( + securityAlertId: string, + ): SecurityAlertResponse { + return this.store.getState().signatureSecurityAlertResponses[ + securityAlertId + ]; + } + + addSignatureSecurityAlertResponse( + securityAlertResponse: SecurityAlertResponse, + ): void { + const currentState = this.store.getState(); + const { signatureSecurityAlertResponses } = currentState; + if (securityAlertResponse.securityAlertId) { + this.store.updateState({ + signatureSecurityAlertResponses: { + ...signatureSecurityAlertResponses, + [String(securityAlertResponse.securityAlertId)]: + securityAlertResponse, + }, + }); + } + } + + /** + * A setter for the currentPopupId which indicates the id of popup window that's currently active + * + * @param currentPopupId + */ + setCurrentPopupId(currentPopupId: number): void { + this.store.updateState({ + currentPopupId, + }); + } + + /** + * The function returns information about the last confirmation user interacted with + */ + getLastInteractedConfirmationInfo(): + | LastInteractedConfirmationInfo + | undefined { + return this.store.getState().lastInteractedConfirmationInfo; + } + + /** + * Update the information about the last confirmation user interacted with + * + * @param lastInteractedConfirmationInfo + */ + setLastInteractedConfirmationInfo( + lastInteractedConfirmationInfo: LastInteractedConfirmationInfo | undefined, + ): void { + this.store.updateState({ + lastInteractedConfirmationInfo, + }); + } + + /** + * A getter to retrieve currentPopupId saved in the appState + */ + getCurrentPopupId(): number | undefined { + return this.store.getState().currentPopupId; + } + + private _requestApproval(): void { + // If we already have a pending request this is a no-op + if (this.#approvalRequestId) { + return; + } + this.#approvalRequestId = uuid(); + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id: this.#approvalRequestId, + origin: ORIGIN_METAMASK, + type: ApprovalType.Unlock, + }, + true, + ) + .catch(() => { + // If the promise fails, we allow a new popup to be triggered + this.#approvalRequestId = null; + }); + } + + // Override emit method to provide strong typing for events + emit(event: string) { + return super.emit(event); + } + + private _acceptApproval(): void { + if (!this.#approvalRequestId) { + return; + } + try { + this.messagingSystem.call( + 'ApprovalController:acceptRequest', + this.#approvalRequestId, + ); + } catch (error) { + log.error('Failed to unlock approval request', error); + } + + this.#approvalRequestId = null; + } +} diff --git a/app/scripts/controllers/app-state.d.ts b/app/scripts/controllers/app-state.d.ts deleted file mode 100644 index aa7ffc92eb3c..000000000000 --- a/app/scripts/controllers/app-state.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SecurityAlertResponse } from '../lib/ppom/types'; - -export type AppStateController = { - addSignatureSecurityAlertResponse( - securityAlertResponse: SecurityAlertResponse, - ): void; - getUnlockPromise(shouldShowUnlockRequest: boolean): Promise; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - setCustodianDeepLink({ - fromAddress, - custodyId, - }: { - fromAddress: string; - custodyId: string; - }): void; - showInteractiveReplacementTokenBanner({ - oldRefreshToken, - url, - }: { - oldRefreshToken: string; - url: string; - }): void; - ///: END:ONLY_INCLUDE_IF -}; diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js deleted file mode 100644 index 9dabf2313e57..000000000000 --- a/app/scripts/controllers/app-state.js +++ /dev/null @@ -1,651 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { v4 as uuid } from 'uuid'; -import log from 'loglevel'; -import { ApprovalType } from '@metamask/controller-utils'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import { MINUTE } from '../../../shared/constants/time'; -import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; -import { isManifestV3 } from '../../../shared/modules/mv3.utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { isBeta } from '../../../ui/helpers/utils/build-types'; -import { - ENVIRONMENT_TYPE_BACKGROUND, - POLLING_TOKEN_ENVIRONMENT_TYPES, - ORIGIN_METAMASK, -} from '../../../shared/constants/app'; -import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; - -/** @typedef {import('../../../shared/types/confirm').LastInteractedConfirmationInfo} LastInteractedConfirmationInfo */ - -export default class AppStateController extends EventEmitter { - /** - * @param {object} opts - */ - constructor(opts = {}) { - const { - addUnlockListener, - isUnlocked, - initState, - onInactiveTimeout, - preferencesController, - messenger, - extension, - } = opts; - super(); - - this.extension = extension; - this.onInactiveTimeout = onInactiveTimeout || (() => undefined); - this.store = new ObservableStore({ - timeoutMinutes: DEFAULT_AUTO_LOCK_TIME_LIMIT, - connectedStatusPopoverHasBeenShown: true, - defaultHomeActiveTabName: null, - browserEnvironment: {}, - popupGasPollTokens: [], - notificationGasPollTokens: [], - fullScreenGasPollTokens: [], - recoveryPhraseReminderHasBeenShown: false, - recoveryPhraseReminderLastShown: new Date().getTime(), - outdatedBrowserWarningLastShown: null, - nftsDetectionNoticeDismissed: false, - showTestnetMessageInDropdown: true, - showBetaHeader: isBeta(), - showPermissionsTour: true, - showNetworkBanner: true, - showAccountBanner: true, - trezorModel: null, - currentPopupId: undefined, - onboardingDate: null, - lastViewedUserSurvey: null, - newPrivacyPolicyToastClickedOrClosed: null, - newPrivacyPolicyToastShownDate: null, - // This key is only used for checking if the user had set advancedGasFee - // prior to Migration 92.3 where we split out the setting to support - // multiple networks. - hadAdvancedGasFeesSetPriorToMigration92_3: false, - ...initState, - qrHardware: {}, - nftsDropdownState: {}, - usedNetworks: { - '0x1': true, - '0x5': true, - '0x539': true, - }, - surveyLinkLastClickedOrClosed: null, - signatureSecurityAlertResponses: {}, - // States used for displaying the changed network toast - switchedNetworkDetails: null, - switchedNetworkNeverShowMessage: false, - currentExtensionPopupId: 0, - lastInteractedConfirmationInfo: undefined, - }); - this.timer = null; - - this.isUnlocked = isUnlocked; - this.waitingForUnlock = []; - addUnlockListener(this.handleUnlock.bind(this)); - - messenger.subscribe( - 'PreferencesController:stateChange', - ({ preferences }) => { - const currentState = this.store.getState(); - if ( - preferences && - currentState.timeoutMinutes !== preferences.autoLockTimeLimit - ) { - this._setInactiveTimeout(preferences.autoLockTimeLimit); - } - }, - ); - - messenger.subscribe( - 'KeyringController:qrKeyringStateChange', - (qrHardware) => - this.store.updateState({ - qrHardware, - }), - ); - - const { preferences } = preferencesController.state; - - this._setInactiveTimeout(preferences.autoLockTimeLimit); - - this.messagingSystem = messenger; - this._approvalRequestId = null; - } - - /** - * Get a Promise that resolves when the extension is unlocked. - * This Promise will never reject. - * - * @param {boolean} shouldShowUnlockRequest - Whether the extension notification - * popup should be opened. - * @returns {Promise} A promise that resolves when the extension is - * unlocked, or immediately if the extension is already unlocked. - */ - getUnlockPromise(shouldShowUnlockRequest) { - return new Promise((resolve) => { - if (this.isUnlocked()) { - resolve(); - } else { - this.waitForUnlock(resolve, shouldShowUnlockRequest); - } - }); - } - - /** - * Adds a Promise's resolve function to the waitingForUnlock queue. - * Also opens the extension popup if specified. - * - * @param {Promise.resolve} resolve - A Promise's resolve function that will - * be called when the extension is unlocked. - * @param {boolean} shouldShowUnlockRequest - Whether the extension notification - * popup should be opened. - */ - waitForUnlock(resolve, shouldShowUnlockRequest) { - this.waitingForUnlock.push({ resolve }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - if (shouldShowUnlockRequest) { - this._requestApproval(); - } - } - - /** - * Drains the waitingForUnlock queue, resolving all the related Promises. - */ - handleUnlock() { - if (this.waitingForUnlock.length > 0) { - while (this.waitingForUnlock.length > 0) { - this.waitingForUnlock.shift().resolve(); - } - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } - - this._acceptApproval(); - } - - /** - * Sets the default home tab - * - * @param {string} [defaultHomeActiveTabName] - the tab name - */ - setDefaultHomeActiveTabName(defaultHomeActiveTabName) { - this.store.updateState({ - defaultHomeActiveTabName, - }); - } - - /** - * Record that the user has seen the connected status info popover - */ - setConnectedStatusPopoverHasBeenShown() { - this.store.updateState({ - connectedStatusPopoverHasBeenShown: true, - }); - } - - /** - * Record that the user has been shown the recovery phrase reminder. - */ - setRecoveryPhraseReminderHasBeenShown() { - this.store.updateState({ - recoveryPhraseReminderHasBeenShown: true, - }); - } - - setSurveyLinkLastClickedOrClosed(time) { - this.store.updateState({ - surveyLinkLastClickedOrClosed: time, - }); - } - - setOnboardingDate() { - this.store.updateState({ - onboardingDate: Date.now(), - }); - } - - setLastViewedUserSurvey(id) { - this.store.updateState({ - lastViewedUserSurvey: id, - }); - } - - setNewPrivacyPolicyToastClickedOrClosed() { - this.store.updateState({ - newPrivacyPolicyToastClickedOrClosed: true, - }); - } - - setNewPrivacyPolicyToastShownDate(time) { - this.store.updateState({ - newPrivacyPolicyToastShownDate: time, - }); - } - - /** - * Record the timestamp of the last time the user has seen the recovery phrase reminder - * - * @param {number} lastShown - timestamp when user was last shown the reminder. - */ - setRecoveryPhraseReminderLastShown(lastShown) { - this.store.updateState({ - recoveryPhraseReminderLastShown: lastShown, - }); - } - - /** - * Record the timestamp of the last time the user has acceoted the terms of use - * - * @param {number} lastAgreed - timestamp when user last accepted the terms of use - */ - setTermsOfUseLastAgreed(lastAgreed) { - this.store.updateState({ - termsOfUseLastAgreed: lastAgreed, - }); - } - - /** - * Record if popover for snaps privacy warning has been shown - * on the first install of a snap. - * - * @param {boolean} shown - shown status - */ - setSnapsInstallPrivacyWarningShownStatus(shown) { - this.store.updateState({ - snapsInstallPrivacyWarningShown: shown, - }); - } - - /** - * Record the timestamp of the last time the user has seen the outdated browser warning - * - * @param {number} lastShown - Timestamp (in milliseconds) of when the user was last shown the warning. - */ - setOutdatedBrowserWarningLastShown(lastShown) { - this.store.updateState({ - outdatedBrowserWarningLastShown: lastShown, - }); - } - - /** - * Sets the last active time to the current time. - */ - setLastActiveTime() { - this._resetTimer(); - } - - /** - * Sets the inactive timeout for the app - * - * @private - * @param {number} timeoutMinutes - The inactive timeout in minutes. - */ - _setInactiveTimeout(timeoutMinutes) { - this.store.updateState({ - timeoutMinutes, - }); - - this._resetTimer(); - } - - /** - * Resets the internal inactive timer - * - * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new - * timer will not be created. - * - * @private - */ - /* eslint-disable no-undef */ - _resetTimer() { - const { timeoutMinutes } = this.store.getState(); - - if (this.timer) { - clearTimeout(this.timer); - } else if (isManifestV3) { - this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); - } - - if (!timeoutMinutes) { - return; - } - - // This is a temporary fix until we add a state migration. - // Due to a bug in ui/pages/settings/advanced-tab/advanced-tab.component.js, - // it was possible for timeoutMinutes to be saved as a string, as explained - // in PR 25109. `alarms.create` will fail in that case. We are - // converting this to a number here to prevent that failure. Once - // we add a migration to update the malformed state to the right type, - // we will remove this conversion. - const timeoutToSet = Number(timeoutMinutes); - - if (isManifestV3) { - this.extension.alarms.create(AUTO_LOCK_TIMEOUT_ALARM, { - delayInMinutes: timeoutToSet, - periodInMinutes: timeoutToSet, - }); - this.extension.alarms.onAlarm.addListener((alarmInfo) => { - if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) { - this.onInactiveTimeout(); - this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); - } - }); - } else { - this.timer = setTimeout( - () => this.onInactiveTimeout(), - timeoutToSet * MINUTE, - ); - } - } - - /** - * Sets the current browser and OS environment - * - * @param os - * @param browser - */ - setBrowserEnvironment(os, browser) { - this.store.updateState({ browserEnvironment: { os, browser } }); - } - - /** - * Adds a pollingToken for a given environmentType - * - * @param pollingToken - * @param pollingTokenType - */ - addPollingToken(pollingToken, pollingTokenType) { - if ( - pollingTokenType !== - POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] - ) { - const prevState = this.store.getState()[pollingTokenType]; - this.store.updateState({ - [pollingTokenType]: [...prevState, pollingToken], - }); - } - } - - /** - * removes a pollingToken for a given environmentType - * - * @param pollingToken - * @param pollingTokenType - */ - removePollingToken(pollingToken, pollingTokenType) { - if ( - pollingTokenType !== - POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] - ) { - const prevState = this.store.getState()[pollingTokenType]; - this.store.updateState({ - [pollingTokenType]: prevState.filter((token) => token !== pollingToken), - }); - } - } - - /** - * clears all pollingTokens - */ - clearPollingTokens() { - this.store.updateState({ - popupGasPollTokens: [], - notificationGasPollTokens: [], - fullScreenGasPollTokens: [], - }); - } - - /** - * Sets whether the testnet dismissal link should be shown in the network dropdown - * - * @param showTestnetMessageInDropdown - */ - setShowTestnetMessageInDropdown(showTestnetMessageInDropdown) { - this.store.updateState({ showTestnetMessageInDropdown }); - } - - /** - * Sets whether the beta notification heading on the home page - * - * @param showBetaHeader - */ - setShowBetaHeader(showBetaHeader) { - this.store.updateState({ showBetaHeader }); - } - - /** - * Sets whether the permissions tour should be shown to the user - * - * @param showPermissionsTour - */ - setShowPermissionsTour(showPermissionsTour) { - this.store.updateState({ showPermissionsTour }); - } - - /** - * Sets whether the Network Banner should be shown - * - * @param showNetworkBanner - */ - setShowNetworkBanner(showNetworkBanner) { - this.store.updateState({ showNetworkBanner }); - } - - /** - * Sets whether the Account Banner should be shown - * - * @param showAccountBanner - */ - setShowAccountBanner(showAccountBanner) { - this.store.updateState({ showAccountBanner }); - } - - /** - * Sets a unique ID for the current extension popup - * - * @param currentExtensionPopupId - */ - setCurrentExtensionPopupId(currentExtensionPopupId) { - this.store.updateState({ currentExtensionPopupId }); - } - - /** - * Sets an object with networkName and appName - * or `null` if the message is meant to be cleared - * - * @param {{ origin: string, networkClientId: string } | null} switchedNetworkDetails - Details about the network that MetaMask just switched to. - */ - setSwitchedNetworkDetails(switchedNetworkDetails) { - this.store.updateState({ switchedNetworkDetails }); - } - - /** - * Clears the switched network details in state - */ - clearSwitchedNetworkDetails() { - this.store.updateState({ switchedNetworkDetails: null }); - } - - /** - * Remembers if the user prefers to never see the - * network switched message again - * - * @param {boolean} switchedNetworkNeverShowMessage - */ - setSwitchedNetworkNeverShowMessage(switchedNetworkNeverShowMessage) { - this.store.updateState({ - switchedNetworkDetails: null, - switchedNetworkNeverShowMessage, - }); - } - - /** - * Sets a property indicating the model of the user's Trezor hardware wallet - * - * @param trezorModel - The Trezor model. - */ - setTrezorModel(trezorModel) { - this.store.updateState({ trezorModel }); - } - - /** - * A setter for the `nftsDropdownState` property - * - * @param nftsDropdownState - */ - updateNftDropDownState(nftsDropdownState) { - this.store.updateState({ - nftsDropdownState, - }); - } - - /** - * Updates the array of the first time used networks - * - * @param chainId - * @returns {void} - */ - setFirstTimeUsedNetwork(chainId) { - const currentState = this.store.getState(); - const { usedNetworks } = currentState; - usedNetworks[chainId] = true; - - this.store.updateState({ usedNetworks }); - } - - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - /** - * Set the interactive replacement token with a url and the old refresh token - * - * @param {object} opts - * @param opts.url - * @param opts.oldRefreshToken - * @returns {void} - */ - showInteractiveReplacementTokenBanner({ url, oldRefreshToken }) { - this.store.updateState({ - interactiveReplacementToken: { - url, - oldRefreshToken, - }, - }); - } - - /** - * Set the setCustodianDeepLink with the fromAddress and custodyId - * - * @param {object} opts - * @param opts.fromAddress - * @param opts.custodyId - * @returns {void} - */ - setCustodianDeepLink({ fromAddress, custodyId }) { - this.store.updateState({ - custodianDeepLink: { fromAddress, custodyId }, - }); - } - - setNoteToTraderMessage(message) { - this.store.updateState({ - noteToTraderMessage: message, - }); - } - - ///: END:ONLY_INCLUDE_IF - - getSignatureSecurityAlertResponse(securityAlertId) { - return this.store.getState().signatureSecurityAlertResponses[ - securityAlertId - ]; - } - - addSignatureSecurityAlertResponse(securityAlertResponse) { - const currentState = this.store.getState(); - const { signatureSecurityAlertResponses } = currentState; - this.store.updateState({ - signatureSecurityAlertResponses: { - ...signatureSecurityAlertResponses, - [securityAlertResponse.securityAlertId]: securityAlertResponse, - }, - }); - } - - /** - * A setter for the currentPopupId which indicates the id of popup window that's currently active - * - * @param currentPopupId - */ - setCurrentPopupId(currentPopupId) { - this.store.updateState({ - currentPopupId, - }); - } - - /** - * The function returns information about the last confirmation user interacted with - * - * @type {LastInteractedConfirmationInfo}: Information about the last confirmation user interacted with. - */ - getLastInteractedConfirmationInfo() { - return this.store.getState().lastInteractedConfirmationInfo; - } - - /** - * Update the information about the last confirmation user interacted with - * - * @type {LastInteractedConfirmationInfo} - information about transaction user last interacted with. - */ - setLastInteractedConfirmationInfo(lastInteractedConfirmationInfo) { - this.store.updateState({ - lastInteractedConfirmationInfo, - }); - } - - /** - * A getter to retrieve currentPopupId saved in the appState - */ - getCurrentPopupId() { - return this.store.getState().currentPopupId; - } - - _requestApproval() { - // If we already have a pending request this is a no-op - if (this._approvalRequestId) { - return; - } - this._approvalRequestId = uuid(); - - this.messagingSystem - .call( - 'ApprovalController:addRequest', - { - id: this._approvalRequestId, - origin: ORIGIN_METAMASK, - type: ApprovalType.Unlock, - }, - true, - ) - .catch(() => { - // If the promise fails, we allow a new popup to be triggered - this._approvalRequestId = null; - }); - } - - _acceptApproval() { - if (!this._approvalRequestId) { - return; - } - try { - this.messagingSystem.call( - 'ApprovalController:acceptRequest', - this._approvalRequestId, - ); - } catch (error) { - log.error('Failed to unlock approval request', error); - } - - this._approvalRequestId = null; - } -} diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js deleted file mode 100644 index 46fe87d29add..000000000000 --- a/app/scripts/controllers/app-state.test.js +++ /dev/null @@ -1,396 +0,0 @@ -import { ObservableStore } from '@metamask/obs-store'; -import { ORIGIN_METAMASK } from '../../../shared/constants/app'; -import AppStateController from './app-state'; - -let appStateController, mockStore; - -describe('AppStateController', () => { - mockStore = new ObservableStore(); - const createAppStateController = (initState = {}) => { - return new AppStateController({ - addUnlockListener: jest.fn(), - isUnlocked: jest.fn(() => true), - initState, - onInactiveTimeout: jest.fn(), - showUnlockRequest: jest.fn(), - preferencesController: { - state: { - preferences: { - autoLockTimeLimit: 0, - }, - }, - }, - messenger: { - call: jest.fn(() => ({ - catch: jest.fn(), - })), - subscribe: jest.fn(), - }, - }); - }; - - beforeEach(() => { - appStateController = createAppStateController({ store: mockStore }); - }); - - describe('setOutdatedBrowserWarningLastShown', () => { - it('sets the last shown time', () => { - appStateController = createAppStateController(); - const date = new Date(); - - appStateController.setOutdatedBrowserWarningLastShown(date); - - expect( - appStateController.store.getState().outdatedBrowserWarningLastShown, - ).toStrictEqual(date); - }); - - it('sets outdated browser warning last shown timestamp', () => { - const lastShownTimestamp = Date.now(); - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - outdatedBrowserWarningLastShown: lastShownTimestamp, - }); - - updateStateSpy.mockRestore(); - }); - }); - - describe('getUnlockPromise', () => { - it('waits for unlock if the extension is locked', async () => { - appStateController = createAppStateController(); - const isUnlockedMock = jest - .spyOn(appStateController, 'isUnlocked') - .mockReturnValue(false); - const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock'); - - appStateController.getUnlockPromise(true); - expect(isUnlockedMock).toHaveBeenCalled(); - expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true); - }); - - it('resolves immediately if the extension is already unlocked', async () => { - appStateController = createAppStateController(); - const isUnlockedMock = jest - .spyOn(appStateController, 'isUnlocked') - .mockReturnValue(true); - - await expect( - appStateController.getUnlockPromise(false), - ).resolves.toBeUndefined(); - - expect(isUnlockedMock).toHaveBeenCalled(); - }); - }); - - describe('waitForUnlock', () => { - it('resolves immediately if already unlocked', async () => { - const emitSpy = jest.spyOn(appStateController, 'emit'); - const resolveFn = jest.fn(); - appStateController.waitForUnlock(resolveFn, false); - expect(emitSpy).toHaveBeenCalledWith('updateBadge'); - expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0); - }); - - it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => { - jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); - - const resolveFn = jest.fn(); - appStateController.waitForUnlock(resolveFn, true); - - expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(1); - expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( - 'ApprovalController:addRequest', - expect.objectContaining({ - id: expect.any(String), - origin: ORIGIN_METAMASK, - type: 'unlock', - }), - true, - ); - }); - }); - - describe('handleUnlock', () => { - beforeEach(() => { - jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('accepts approval request revolving all the related promises', async () => { - const emitSpy = jest.spyOn(appStateController, 'emit'); - const resolveFn = jest.fn(); - appStateController.waitForUnlock(resolveFn, true); - - appStateController.handleUnlock(); - - expect(emitSpy).toHaveBeenCalled(); - expect(emitSpy).toHaveBeenCalledWith('updateBadge'); - expect(appStateController.messagingSystem.call).toHaveBeenCalled(); - expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( - 'ApprovalController:acceptRequest', - expect.any(String), - ); - }); - }); - - describe('setDefaultHomeActiveTabName', () => { - it('sets the default home tab name', () => { - appStateController.setDefaultHomeActiveTabName('testTabName'); - expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( - 'testTabName', - ); - }); - }); - - describe('setConnectedStatusPopoverHasBeenShown', () => { - it('sets connected status popover as shown', () => { - appStateController.setConnectedStatusPopoverHasBeenShown(); - expect( - appStateController.store.getState().connectedStatusPopoverHasBeenShown, - ).toBe(true); - }); - }); - - describe('setRecoveryPhraseReminderHasBeenShown', () => { - it('sets recovery phrase reminder as shown', () => { - appStateController.setRecoveryPhraseReminderHasBeenShown(); - expect( - appStateController.store.getState().recoveryPhraseReminderHasBeenShown, - ).toBe(true); - }); - }); - - describe('setRecoveryPhraseReminderLastShown', () => { - it('sets the last shown time of recovery phrase reminder', () => { - const timestamp = Date.now(); - appStateController.setRecoveryPhraseReminderLastShown(timestamp); - - expect( - appStateController.store.getState().recoveryPhraseReminderLastShown, - ).toBe(timestamp); - }); - }); - - describe('setLastActiveTime', () => { - it('sets the last active time to the current time', () => { - const spy = jest.spyOn(appStateController, '_resetTimer'); - appStateController.setLastActiveTime(); - - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('setBrowserEnvironment', () => { - it('sets the current browser and OS environment', () => { - appStateController.setBrowserEnvironment('Windows', 'Chrome'); - expect( - appStateController.store.getState().browserEnvironment, - ).toStrictEqual({ - os: 'Windows', - browser: 'Chrome', - }); - }); - }); - - describe('addPollingToken', () => { - it('adds a pollingToken for a given environmentType', () => { - const pollingTokenType = 'popupGasPollTokens'; - appStateController.addPollingToken('token1', pollingTokenType); - expect(appStateController.store.getState()[pollingTokenType]).toContain( - 'token1', - ); - }); - }); - - describe('removePollingToken', () => { - it('removes a pollingToken for a given environmentType', () => { - const pollingTokenType = 'popupGasPollTokens'; - appStateController.addPollingToken('token1', pollingTokenType); - appStateController.removePollingToken('token1', pollingTokenType); - expect( - appStateController.store.getState()[pollingTokenType], - ).not.toContain('token1'); - }); - }); - - describe('clearPollingTokens', () => { - it('clears all pollingTokens', () => { - appStateController.addPollingToken('token1', 'popupGasPollTokens'); - appStateController.addPollingToken('token2', 'notificationGasPollTokens'); - appStateController.addPollingToken('token3', 'fullScreenGasPollTokens'); - appStateController.clearPollingTokens(); - - expect( - appStateController.store.getState().popupGasPollTokens, - ).toStrictEqual([]); - expect( - appStateController.store.getState().notificationGasPollTokens, - ).toStrictEqual([]); - expect( - appStateController.store.getState().fullScreenGasPollTokens, - ).toStrictEqual([]); - }); - }); - - describe('setShowTestnetMessageInDropdown', () => { - it('sets whether the testnet dismissal link should be shown in the network dropdown', () => { - appStateController.setShowTestnetMessageInDropdown(true); - expect( - appStateController.store.getState().showTestnetMessageInDropdown, - ).toBe(true); - - appStateController.setShowTestnetMessageInDropdown(false); - expect( - appStateController.store.getState().showTestnetMessageInDropdown, - ).toBe(false); - }); - }); - - describe('setShowBetaHeader', () => { - it('sets whether the beta notification heading on the home page', () => { - appStateController.setShowBetaHeader(true); - expect(appStateController.store.getState().showBetaHeader).toBe(true); - - appStateController.setShowBetaHeader(false); - expect(appStateController.store.getState().showBetaHeader).toBe(false); - }); - }); - - describe('setCurrentPopupId', () => { - it('sets the currentPopupId in the appState', () => { - const popupId = 'popup1'; - - appStateController.setCurrentPopupId(popupId); - expect(appStateController.store.getState().currentPopupId).toBe(popupId); - }); - }); - - describe('getCurrentPopupId', () => { - it('retrieves the currentPopupId saved in the appState', () => { - const popupId = 'popup1'; - - appStateController.setCurrentPopupId(popupId); - expect(appStateController.getCurrentPopupId()).toBe(popupId); - }); - }); - - describe('setFirstTimeUsedNetwork', () => { - it('updates the array of the first time used networks', () => { - const chainId = '0x1'; - - appStateController.setFirstTimeUsedNetwork(chainId); - expect(appStateController.store.getState().usedNetworks[chainId]).toBe( - true, - ); - }); - }); - - describe('setLastInteractedConfirmationInfo', () => { - it('sets information about last confirmation user has interacted with', () => { - const lastInteractedConfirmationInfo = { - id: '123', - chainId: '0x1', - timestamp: new Date().getTime(), - }; - appStateController.setLastInteractedConfirmationInfo( - lastInteractedConfirmationInfo, - ); - expect(appStateController.getLastInteractedConfirmationInfo()).toBe( - lastInteractedConfirmationInfo, - ); - - appStateController.setLastInteractedConfirmationInfo(undefined); - expect(appStateController.getLastInteractedConfirmationInfo()).toBe( - undefined, - ); - }); - }); - - describe('setSnapsInstallPrivacyWarningShownStatus', () => { - it('updates the status of snaps install privacy warning', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - appStateController.setSnapsInstallPrivacyWarningShownStatus(true); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - snapsInstallPrivacyWarningShown: true, - }); - - updateStateSpy.mockRestore(); - }); - }); - - describe('institutional', () => { - it('set the interactive replacement token with a url and the old refresh token', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - const mockParams = { url: 'https://example.com', oldRefreshToken: 'old' }; - - appStateController.showInteractiveReplacementTokenBanner(mockParams); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - interactiveReplacementToken: mockParams, - }); - - updateStateSpy.mockRestore(); - }); - - it('set the setCustodianDeepLink with the fromAddress and custodyId', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - const mockParams = { fromAddress: '0x', custodyId: 'custodyId' }; - - appStateController.setCustodianDeepLink(mockParams); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - custodianDeepLink: mockParams, - }); - - updateStateSpy.mockRestore(); - }); - - it('set the setNoteToTraderMessage with a message', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - const mockParams = 'some message'; - - appStateController.setNoteToTraderMessage(mockParams); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - noteToTraderMessage: mockParams, - }); - - updateStateSpy.mockRestore(); - }); - }); -}); diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 0c4aa2d5d874..7fb87c6d143b 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -18,7 +18,7 @@ import { TEST_NETWORK_TICKER_MAP, } from '../../../shared/constants/network'; import MMIController from './mmi-controller'; -import AppStateController from './app-state'; +import { AppStateController } from './app-state-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; @@ -246,14 +246,14 @@ describe('MMIController', function () { initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesController: { - state: { + messenger: { + ...mockMessenger, + call: jest.fn().mockReturnValue({ preferences: { autoLockTimeLimit: 0, }, - }, - }, - messenger: mockMessenger, + }) + } }), networkController, permissionController, diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 571c000106b1..65cdac69ba0b 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -47,9 +47,9 @@ import { import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; -import { PreferencesController } from './preferences-controller'; import AccountTrackerController from './account-tracker-controller'; -import { AppStateController } from './app-state'; +import { AppStateController } from './app-state-controller'; +import { PreferencesController } from './preferences-controller'; type UpdateCustodianTransactionsParameters = { keyring: CustodyKeyring; diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 3b393897b2e0..7eb8dc0cc5a2 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -12,7 +12,7 @@ import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import { PreferencesController } from '../../controllers/preferences-controller'; -import { AppStateController } from '../../controllers/app-state'; +import { AppStateController } from '../../controllers/app-state-controller'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; // eslint-disable-next-line import/no-restricted-paths import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index f6a0d3a1213c..ea62c3b88533 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -15,7 +15,7 @@ import { BlockaidResultType, SecurityAlertSource, } from '../../../../shared/constants/security-provider'; -import { AppStateController } from '../../controllers/app-state'; +import { AppStateController } from '../../controllers/app-state-controller'; import { generateSecurityAlertId, isChainSupported, diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 73999061a910..7662c364b651 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -15,7 +15,7 @@ import { SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; -import { AppStateController } from '../../controllers/app-state'; +import { AppStateController } from '../../controllers/app-state-controller'; import { SecurityAlertResponse } from './types'; import { getSecurityAlertsAPISupportedChainIds, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 75a0da28157e..fae6eedc2ab8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -297,7 +297,7 @@ import { AccountOrderController } from './controllers/account-order'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { isStreamWritable, setupMultiplex } from './lib/stream-utils'; import { PreferencesController } from './controllers/preferences-controller'; -import AppStateController from './controllers/app-state'; +import { AppStateController } from './controllers/app-state-controller'; import { AlertController } from './controllers/alert-controller'; import OnboardingController from './controllers/onboarding'; import Backup from './lib/backup'; @@ -846,12 +846,12 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - preferencesController: this.preferencesController, messenger: this.controllerMessenger.getRestricted({ name: 'AppStateController', allowedActions: [ `${this.approvalController.name}:addRequest`, `${this.approvalController.name}:acceptRequest`, + `PreferencesController:getState`, ], allowedEvents: [ `KeyringController:qrKeyringStateChange`, diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index e21cc6b03a0c..5de1f953bb87 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -4,7 +4,6 @@ "app/scripts/constants/contracts.js", "app/scripts/constants/on-ramp.js", "app/scripts/contentscript.js", - "app/scripts/controllers/app-state.js", "app/scripts/controllers/cached-balances.js", "app/scripts/controllers/cached-balances.test.js", "app/scripts/controllers/ens/ens.js", diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index a57a1eea2109..67be9f72cee6 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -9,7 +9,7 @@ import { NetworkController } from '@metamask/network-controller'; import { PreferencesController } from '../../app/scripts/controllers/preferences-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { AppStateController } from '../../app/scripts/controllers/app-state'; +import { AppStateController } from '../../app/scripts/controllers/app-state-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import AccountTrackerController from '../../app/scripts/controllers/account-tracker-controller'; From 9b370bd06d3937ed43b1e770a249fe6178f0a8bf Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:06:01 +0800 Subject: [PATCH 40/51] =?UTF-8?q?feat:=20add=20=E2=80=9CIncomplete=20Asset?= =?UTF-8?q?=20Displayed=E2=80=9D=20metric=20&=20fix:=20should=20only=20set?= =?UTF-8?q?=20default=20decimals=20if=20ERC20=20(#27494)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ### Overall - Adds "Incomplete Asset Displayed" metric when token detail decimals are not found for Permit Simulations of ERC20 tokens - Fixes issue where decimals may default to 18 when the token is not identified as an ERC20 token ### Details - Refactors fetchErc20Decimals - Defines types in token.ts - Create new useGetTokenStandardAndDetails - Includes new `decimalsNumber` value in return to be used instead of `decimals` (string type) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27333 ## **Manual testing steps** NA ## **Screenshots/Recordings** NA ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Jyoti Puri --- .../confirmations/signatures/permit.spec.ts | 2 +- .../confirmations/signatures/permit.test.tsx | 2 +- .../permit-simulation.test.tsx | 12 ++- .../__snapshots__/value-display.test.tsx.snap | 46 --------- .../value-display/value-display.test.tsx | 21 ++-- .../value-display/value-display.tsx | 21 ++-- .../components/confirm/row/dataTree.tsx | 18 ++-- .../typedSignDataV1.test.tsx | 12 ++- .../useBalanceChanges.test.ts | 4 +- .../confirmations/confirm/confirm.test.tsx | 12 +-- .../useGetTokenStandardAndDetails.test.ts | 50 ++++++++++ .../hooks/useGetTokenStandardAndDetails.ts | 42 ++++++++ ...rackERC20WithoutDecimalInformation.test.ts | 40 ++++++++ .../useTrackERC20WithoutDecimalInformation.ts | 58 +++++++++++ ui/pages/confirmations/utils/token.test.ts | 7 +- ui/pages/confirmations/utils/token.ts | 97 +++++++++++++++---- 16 files changed, 336 insertions(+), 108 deletions(-) create mode 100644 ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts create mode 100644 ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts create mode 100644 ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts create mode 100644 ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 5c52d1f029ee..8da5e411a2f4 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -126,7 +126,7 @@ async function assertInfoValues(driver: Driver) { css: '.name__value', text: '0x5B38D...eddC4', }); - const value = driver.findElement({ text: '<0.000001' }); + const value = driver.findElement({ text: '3,000' }); const nonce = driver.findElement({ text: '0' }); const deadline = driver.findElement({ text: '09 June 3554, 16:53' }); diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index e11f206d1996..8e9c979562f2 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -73,7 +73,7 @@ describe('Permit Confirmation', () => { jest.resetAllMocks(); mockedBackgroundConnection.submitRequestToBackground.mockImplementation( createMockImplementation({ - getTokenStandardAndDetails: { decimals: '2' }, + getTokenStandardAndDetails: { decimals: '2', standard: 'ERC20' }, }), ); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx index e89efb3c0dc1..0d67715867d9 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx @@ -8,15 +8,25 @@ import { permitNFTSignatureMsg, permitSignatureMsg, } from '../../../../../../../../test/data/confirmations/typed_sign'; +import { memoizedGetTokenStandardAndDetails } from '../../../../../utils/token'; import PermitSimulation from './permit-simulation'; jest.mock('../../../../../../../store/actions', () => { return { - getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 2 }), + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 2, standard: 'ERC20' }), }; }); describe('PermitSimulation', () => { + afterEach(() => { + jest.clearAllMocks(); + + /** Reset memoized function using getTokenStandardAndDetails for each test */ + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); + }); + it('renders component correctly', async () => { const state = getMockTypedSignConfirmStateForRequest(permitSignatureMsg); const mockStore = configureMockStore([])(state); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap index 9c4134aa1b2d..26def806c6fa 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap @@ -56,49 +56,3 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = `
    `; - -exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = ` -
    -
    -
    -
    -
    -

    - #4321 -

    -
    -
    -
    -
    - -

    - 0xA0b86...6eB48 -

    -
    -
    -
    -
    -
    -
    -`; diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx index da86d497aac1..e8e48c1ca6f9 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx @@ -4,14 +4,24 @@ import configureMockStore from 'redux-mock-store'; import mockState from '../../../../../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../../../../../test/lib/render-helpers'; +import useTrackERC20WithoutDecimalInformation from '../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; import PermitSimulationValueDisplay from './value-display'; jest.mock('../../../../../../../../store/actions', () => { return { - getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 4 }), + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 4, standard: 'ERC20' }), }; }); +jest.mock( + '../../../../../../hooks/useTrackERC20WithoutDecimalInformation', + () => { + return jest.fn(); + }, +); + describe('PermitSimulationValueDisplay', () => { it('renders component correctly', async () => { const mockStore = configureMockStore([])(mockState); @@ -30,20 +40,19 @@ describe('PermitSimulationValueDisplay', () => { }); }); - it('renders component correctly for NFT token', async () => { + it('should invoke method to track missing decimal information for ERC20 tokens', async () => { const mockStore = configureMockStore([])(mockState); await act(async () => { - const { container, findByText } = renderWithProvider( + renderWithProvider( , mockStore, ); - expect(await findByText('#4321')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); + expect(useTrackERC20WithoutDecimalInformation).toHaveBeenCalled(); }); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx index 360559493596..e95edc03087b 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx @@ -2,8 +2,9 @@ import React, { useMemo } from 'react'; import { NameType } from '@metamask/name-controller'; import { Hex } from '@metamask/utils'; import { captureException } from '@sentry/browser'; -import { shortenString } from '../../../../../../../../helpers/utils/util'; +import { MetaMetricsEventLocation } from '../../../../../../../../../shared/constants/metametrics'; +import { shortenString } from '../../../../../../../../helpers/utils/util'; import { calcTokenAmount } from '../../../../../../../../../shared/lib/transactions-controller-utils'; import useTokenExchangeRate from '../../../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; import { IndividualFiatDisplay } from '../../../../../simulation-details/fiat-display'; @@ -11,7 +12,8 @@ import { formatAmount, formatAmountMaxPrecision, } from '../../../../../simulation-details/formatAmount'; -import { useAsyncResult } from '../../../../../../../../hooks/useAsyncResult'; +import { useGetTokenStandardAndDetails } from '../../../../../../hooks/useGetTokenStandardAndDetails'; +import useTrackERC20WithoutDecimalInformation from '../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; import { Box, @@ -27,7 +29,7 @@ import { TextAlign, } from '../../../../../../../../helpers/constants/design-system'; import Name from '../../../../../../../../components/app/name/name'; -import { fetchErc20Decimals } from '../../../../../../utils/token'; +import { TokenDetailsERC20 } from '../../../../../../utils/token'; type PermitSimulationValueDisplayParams = { /** The primaryType of the typed sign message */ @@ -52,12 +54,13 @@ const PermitSimulationValueDisplay: React.FC< > = ({ primaryType, tokenContract, value, tokenId }) => { const exchangeRate = useTokenExchangeRate(tokenContract); - const { value: tokenDecimals } = useAsyncResult(async () => { - if (tokenId) { - return undefined; - } - return await fetchErc20Decimals(tokenContract); - }, [tokenContract]); + const tokenDetails = useGetTokenStandardAndDetails(tokenContract); + useTrackERC20WithoutDecimalInformation( + tokenContract, + tokenDetails as TokenDetailsERC20, + MetaMetricsEventLocation.SignatureConfirmation, + ); + const { decimalsNumber: tokenDecimals } = tokenDetails; const fiatValue = useMemo(() => { if (exchangeRate && value && !tokenId) { diff --git a/ui/pages/confirmations/components/confirm/row/dataTree.tsx b/ui/pages/confirmations/components/confirm/row/dataTree.tsx index 26c91baed3a6..b295f337deb4 100644 --- a/ui/pages/confirmations/components/confirm/row/dataTree.tsx +++ b/ui/pages/confirmations/components/confirm/row/dataTree.tsx @@ -11,7 +11,6 @@ import { isValidHexAddress } from '../../../../../../shared/modules/hexstring-ut import { sanitizeString } from '../../../../../helpers/utils/util'; import { Box } from '../../../../../components/component-library'; import { BlockSize } from '../../../../../helpers/constants/design-system'; -import { useAsyncResult } from '../../../../../hooks/useAsyncResult'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { ConfirmInfoRow, @@ -20,7 +19,7 @@ import { ConfirmInfoRowText, ConfirmInfoRowTextTokenUnits, } from '../../../../../components/app/confirm/info/row'; -import { fetchErc20Decimals } from '../../../utils/token'; +import { useGetTokenStandardAndDetails } from '../../../hooks/useGetTokenStandardAndDetails'; type ValueType = string | Record | TreeData[]; @@ -78,9 +77,9 @@ const NONE_DATE_VALUE = -1; * * @param dataTreeData */ -const getTokenDecimalsOfDataTree = async ( +const getTokenContractInDataTree = ( dataTreeData: Record | TreeData[], -): Promise => { +): Hex | undefined => { if (Array.isArray(dataTreeData)) { return undefined; } @@ -91,7 +90,7 @@ const getTokenDecimalsOfDataTree = async ( return undefined; } - return await fetchErc20Decimals(tokenContract); + return tokenContract; }; export const DataTree = ({ @@ -103,13 +102,10 @@ export const DataTree = ({ primaryType?: PrimaryType; tokenDecimals?: number; }) => { - const { value: decimalsResponse } = useAsyncResult( - async () => await getTokenDecimalsOfDataTree(data), - [data], - ); - + const tokenContract = getTokenContractInDataTree(data); + const { decimalsNumber } = useGetTokenStandardAndDetails(tokenContract); const tokenDecimals = - typeof decimalsResponse === 'number' ? decimalsResponse : tokenDecimalsProp; + typeof decimalsNumber === 'number' ? decimalsNumber : tokenDecimalsProp; return ( diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx index 9563b5523f39..ecf55e3b574d 100644 --- a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx +++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx @@ -1,22 +1,28 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../../../test/lib/render-helpers'; import { unapprovedTypedSignMsgV1 } from '../../../../../../../test/data/confirmations/typed_sign'; import { TypedSignDataV1Type } from '../../../../types/confirm'; import { ConfirmInfoRowTypedSignDataV1 } from './typedSignDataV1'; +const mockStore = configureMockStore([])(mockState); + describe('ConfirmInfoRowTypedSignData', () => { it('should match snapshot', () => { - const { container } = render( + const { container } = renderWithProvider( , + mockStore, ); expect(container).toMatchSnapshot(); }); it('should return null if data is not defined', () => { - const { container } = render( + const { container } = renderWithProvider( , + mockStore, ); expect(container).toBeEmptyDOMElement(); }); diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts index 10e4cca518b7..5dc0be870538 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts @@ -9,7 +9,7 @@ import { TokenStandard } from '../../../../../shared/constants/transaction'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { getTokenStandardAndDetails } from '../../../../store/actions'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; -import { fetchErc20Decimals } from '../../utils/token'; +import { memoizedGetTokenStandardAndDetails } from '../../utils/token'; import { useBalanceChanges } from './useBalanceChanges'; import { FIAT_UNAVAILABLE } from './types'; @@ -92,7 +92,7 @@ describe('useBalanceChanges', () => { afterEach(() => { /** Reset memoized function for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); describe('pending states', () => { diff --git a/ui/pages/confirmations/confirm/confirm.test.tsx b/ui/pages/confirmations/confirm/confirm.test.tsx index d6b2dd704fb8..939ca8768afe 100644 --- a/ui/pages/confirmations/confirm/confirm.test.tsx +++ b/ui/pages/confirmations/confirm/confirm.test.tsx @@ -17,7 +17,7 @@ import mockState from '../../../../test/data/mock-state.json'; import { renderWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers'; import * as actions from '../../../store/actions'; import { SignatureRequestType } from '../types/confirm'; -import { fetchErc20Decimals } from '../utils/token'; +import { memoizedGetTokenStandardAndDetails } from '../utils/token'; import Confirm from './confirm'; jest.mock('react-router-dom', () => ({ @@ -34,7 +34,7 @@ describe('Confirm', () => { jest.resetAllMocks(); /** Reset memoized function using getTokenStandardAndDetails for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); it('should render', () => { @@ -59,7 +59,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); @@ -103,7 +103,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); @@ -146,7 +146,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); await act(async () => { @@ -170,7 +170,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); await act(async () => { diff --git a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts new file mode 100644 index 000000000000..7cd217db3a85 --- /dev/null +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts @@ -0,0 +1,50 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + +import * as TokenActions from '../utils/token'; +import { useGetTokenStandardAndDetails } from './useGetTokenStandardAndDetails'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: () => 0x1, +})); + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + +jest.mock('../../../store/actions', () => { + return { + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 2, standard: 'ERC20' }), + }; +}); + +describe('useGetTokenStandardAndDetails', () => { + it('should return token details', () => { + const { result } = renderHook(() => useGetTokenStandardAndDetails('0x5')); + expect(result.current).toEqual({ decimalsNumber: undefined }); + }); + + it('should return token details obtained from getTokenStandardAndDetails action', async () => { + jest + .spyOn(TokenActions, 'memoizedGetTokenStandardAndDetails') + .mockResolvedValue({ + standard: 'ERC20', + } as TokenActions.TokenDetailsERC20); + const { result, rerender } = renderHook(() => + useGetTokenStandardAndDetails('0x5'), + ); + + rerender(); + + await waitFor(() => { + expect(result.current).toEqual({ + decimalsNumber: 18, + standard: 'ERC20', + }); + }); + }); +}); diff --git a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts new file mode 100644 index 000000000000..88dfb0a12b9d --- /dev/null +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts @@ -0,0 +1,42 @@ +import { Hex } from '@metamask/utils'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAsyncResult } from '../../../hooks/useAsyncResult'; +import { + ERC20_DEFAULT_DECIMALS, + parseTokenDetailDecimals, + memoizedGetTokenStandardAndDetails, + TokenDetailsERC20, +} from '../utils/token'; + +/** + * Returns token details for a given token contract + * + * @param tokenAddress + * @returns + */ +export const useGetTokenStandardAndDetails = ( + tokenAddress: Hex | string | undefined, +) => { + const { value: details } = useAsyncResult( + async () => + (await memoizedGetTokenStandardAndDetails( + tokenAddress, + )) as TokenDetailsERC20, + [tokenAddress], + ); + + if (!details) { + return { decimalsNumber: undefined }; + } + + const { decimals, standard } = details || {}; + + if (standard === TokenStandard.ERC20) { + const parsedDecimals = + parseTokenDetailDecimals(decimals) ?? ERC20_DEFAULT_DECIMALS; + details.decimalsNumber = parsedDecimals; + } + + return details; +}; diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts new file mode 100644 index 000000000000..dff0103fbe21 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts @@ -0,0 +1,40 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useContext } from 'react'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { TokenDetailsERC20 } from '../utils/token'; +import useTrackERC20WithoutDecimalInformation from './useTrackERC20WithoutDecimalInformation'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: () => 0x1, +})); + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + +describe('useTrackERC20WithoutDecimalInformation', () => { + const useContextMock = jest.mocked(useContext); + + const trackEventMock = jest.fn(); + + it('should invoke trackEvent method', () => { + useContextMock.mockImplementation((context) => { + if (context === MetaMetricsContext) { + return trackEventMock; + } + return undefined; + }); + + renderHook(() => + useTrackERC20WithoutDecimalInformation('0x5', { + standard: TokenStandard.ERC20, + } as TokenDetailsERC20), + ); + + expect(trackEventMock).toHaveBeenCalled(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts new file mode 100644 index 000000000000..fa6a5e620fc4 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts @@ -0,0 +1,58 @@ +import { useSelector } from 'react-redux'; +import { useContext, useEffect } from 'react'; +import { Hex } from '@metamask/utils'; + +import { + MetaMetricsEventCategory, + MetaMetricsEventLocation, + MetaMetricsEventName, + MetaMetricsEventUiCustomization, +} from '../../../../shared/constants/metametrics'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { getCurrentChainId } from '../../../selectors'; +import { parseTokenDetailDecimals, TokenDetailsERC20 } from '../utils/token'; + +/** + * Track event that number of decimals in ERC20 is not obtained + * + * @param tokenAddress + * @param tokenDetails + * @param metricLocation + */ +const useTrackERC20WithoutDecimalInformation = ( + tokenAddress: Hex | string | undefined, + tokenDetails?: TokenDetailsERC20, + metricLocation = MetaMetricsEventLocation.SignatureConfirmation, +) => { + const trackEvent = useContext(MetaMetricsContext); + const chainId = useSelector(getCurrentChainId); + + useEffect(() => { + if (chainId === undefined || tokenDetails === undefined) { + return; + } + const { decimals, standard } = tokenDetails || {}; + if (standard === TokenStandard.ERC20) { + const parsedDecimals = parseTokenDetailDecimals(decimals); + if (parsedDecimals === undefined) { + trackEvent({ + event: MetaMetricsEventName.SimulationIncompleteAssetDisplayed, + category: MetaMetricsEventCategory.Confirmations, + properties: { + token_decimals_available: false, + asset_address: tokenAddress, + asset_type: TokenStandard.ERC20, + chain_id: chainId, + location: metricLocation, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + }, + }); + } + } + }, [tokenDetails, chainId, tokenAddress, trackEvent]); +}; + +export default useTrackERC20WithoutDecimalInformation; diff --git a/ui/pages/confirmations/utils/token.test.ts b/ui/pages/confirmations/utils/token.test.ts index e71813713d79..250bff90c07c 100644 --- a/ui/pages/confirmations/utils/token.test.ts +++ b/ui/pages/confirmations/utils/token.test.ts @@ -1,6 +1,9 @@ import { getTokenStandardAndDetails } from '../../../store/actions'; import { ERC20_DEFAULT_DECIMALS } from '../constants/token'; -import { fetchErc20Decimals } from './token'; +import { + fetchErc20Decimals, + memoizedGetTokenStandardAndDetails, +} from './token'; const MOCK_ADDRESS = '0x514910771af9ca656af840dff83e8264ecf986ca'; const MOCK_DECIMALS = 36; @@ -14,7 +17,7 @@ describe('fetchErc20Decimals', () => { jest.clearAllMocks(); /** Reset memoized function using getTokenStandardAndDetails for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); it(`should return the default number, ${ERC20_DEFAULT_DECIMALS}, if no decimals were found from details`, async () => { diff --git a/ui/pages/confirmations/utils/token.ts b/ui/pages/confirmations/utils/token.ts index 1f94280129a9..3a8c3a2a671e 100644 --- a/ui/pages/confirmations/utils/token.ts +++ b/ui/pages/confirmations/utils/token.ts @@ -1,32 +1,89 @@ import { memoize } from 'lodash'; import { Hex } from '@metamask/utils'; +import { AssetsContractController } from '@metamask/assets-controllers'; import { getTokenStandardAndDetails } from '../../../store/actions'; +export type TokenDetailsERC20 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +> & { decimalsNumber: number }; + +export type TokenDetailsERC721 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +>; + +export type TokenDetailsERC1155 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +>; + +export type TokenDetails = + | TokenDetailsERC20 + | TokenDetailsERC721 + | TokenDetailsERC1155; + export const ERC20_DEFAULT_DECIMALS = 18; -/** - * Fetches the decimals for the given token address. - * - * @param {Hex | string} address - The ethereum token contract address. It is expected to be in hex format. - * We currently accept strings since we have a patch that accepts a custom string - * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch} - */ -export const fetchErc20Decimals = memoize( - async (address: Hex | string): Promise => { +export const parseTokenDetailDecimals = ( + decStr?: string, +): number | undefined => { + if (!decStr) { + return undefined; + } + + for (const radix of [10, 16]) { + const parsedDec = parseInt(decStr, radix); + if (isFinite(parsedDec)) { + return parsedDec; + } + } + return undefined; +}; + +export const memoizedGetTokenStandardAndDetails = memoize( + async ( + tokenAddress?: Hex | string, + userAddress?: string, + tokenId?: string, + ): Promise> => { try { - const { decimals: decStr } = await getTokenStandardAndDetails(address); - if (!decStr) { - return ERC20_DEFAULT_DECIMALS; - } - for (const radix of [10, 16]) { - const parsedDec = parseInt(decStr, radix); - if (isFinite(parsedDec)) { - return parsedDec; - } + if (!tokenAddress) { + return {}; } - return ERC20_DEFAULT_DECIMALS; + + return (await getTokenStandardAndDetails( + tokenAddress, + userAddress, + tokenId, + )) as TokenDetails; } catch { - return ERC20_DEFAULT_DECIMALS; + return {}; } }, ); + +/** + * Fetches the decimals for the given token address. + * + * @param address - The ethereum token contract address. It is expected to be in hex format. + * We currently accept strings since we have a patch that accepts a custom string + * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch} + */ +export const fetchErc20Decimals = async ( + address: Hex | string, +): Promise => { + try { + const { decimals: decStr } = (await memoizedGetTokenStandardAndDetails( + address, + )) as TokenDetailsERC20; + const decimals = parseTokenDetailDecimals(decStr); + + return decimals ?? ERC20_DEFAULT_DECIMALS; + } catch { + return ERC20_DEFAULT_DECIMALS; + } +}; From c44fb0b2d507e42de099e4fcbd5c360c7d812da1 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:30:07 +0200 Subject: [PATCH 41/51] fix: lint-lockfile flaky job by changing resources from medium to medium-plus (#27950) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The lint-lockfile job is flaky. It seems it's under-resourced peaking max cpu and ram values, as @Gudahtt pointed out: ![Screenshot from 2024-10-18 09-14-33](https://github.com/user-attachments/assets/bf9c4d06-31c5-47e0-885a-dde85e49def2) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27950?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27806 ## **Manual testing steps** 1. Check lint-lockfile job ## **Screenshots/Recordings** Increasing resources from medium to medium-plus https://circleci.com/pricing/price-list/ ![Screenshot from 2024-10-18 09-39-13](https://github.com/user-attachments/assets/d473d8f5-8c9d-442e-8267-71deca2834dc) Resource usage after the change: ![Screenshot from 2024-10-18 09-33-41](https://github.com/user-attachments/assets/0e8dfd50-275c-4c1e-a33e-169923cda987) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2bf244b9bf8a..74815818582f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1008,7 +1008,7 @@ jobs: command: ./development/shellcheck.sh test-lint-lockfile: - executor: node-browsers-medium + executor: node-browsers-medium-plus steps: - run: *shallow-git-clone-and-enable-vnc - run: sudo corepack enable From 9ac03643934f42c138bdbca0345809942a8a1ea0 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:36:43 +0200 Subject: [PATCH 42/51] fix: flaky test `Confirmation Redesign ERC721 Approve Component Submit an Approve transaction @no-mmi Sends a type 2 transaction (EIP1559)` (#27928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removing anti-patterns from erc721 tests. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27928?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../erc721-approve-redesign.spec.ts | 14 +++++------ .../tokens/nft/erc721-interaction.spec.js | 23 ++++++------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts index f91b1e8ba1d2..c7ceb6c42c94 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ import { MockttpServer } from 'mockttp'; -import { veryLargeDelayMs, WINDOW_TITLES } from '../../../helpers'; +import { WINDOW_TITLES } from '../../../helpers'; import { Driver } from '../../../webdriver/driver'; import { scrollAndConfirmAndAssertConfirm } from '../helpers'; import { @@ -119,8 +119,6 @@ async function createMintTransaction(driver: Driver) { } export async function confirmMintTransaction(driver: Driver) { - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ @@ -129,6 +127,12 @@ export async function confirmMintTransaction(driver: Driver) { }); await scrollAndConfirmAndAssertConfirm(driver); + + // Verify Mint Transaction is Confirmed before proceeding + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + await driver.clickElement('[data-testid="account-overview__activity-tab"]'); + await driver.waitForSelector('.transaction-status-label--confirmed'); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); } async function createApproveTransaction(driver: Driver) { @@ -137,8 +141,6 @@ async function createApproveTransaction(driver: Driver) { } async function assertApproveDetails(driver: Driver) { - await driver.delay(veryLargeDelayMs); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ @@ -191,8 +193,6 @@ async function assertApproveDetails(driver: Driver) { async function confirmApproveTransaction(driver: Driver) { await scrollAndConfirmAndAssertConfirm(driver); - - await driver.delay(veryLargeDelayMs); await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index 9ebc247ea795..35750bae6d2c 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -51,19 +51,17 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const nftsMintStatus = await driver.findElement({ + await driver.waitForSelector({ css: '#nftsStatus', text: 'Mint completed', }); - assert.equal(await nftsMintStatus.isDisplayed(), true); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, @@ -116,11 +114,10 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -138,7 +135,6 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.fill('#watchNFTInput', '3'); await driver.clickElement({ text: 'Watch NFT', tag: 'button' }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // avoid race condition @@ -251,11 +247,10 @@ describe('ERC721 NFTs testdapp interaction', function () { }); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const nftsMintStatus = await driver.findElement({ + await driver.waitForSelector({ css: '#nftsStatus', text: 'Mint completed', }); - assert.equal(await nftsMintStatus.isDisplayed(), true); // watch all nfts await driver.clickElement({ text: 'Watch all NFTs', tag: 'button' }); @@ -322,7 +317,6 @@ describe('ERC721 NFTs testdapp interaction', function () { // Click Transfer await driver.fill('#transferTokenInput', '1'); await driver.clickElement('#transferFromButton'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm transfer @@ -407,11 +401,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Approve TDN spending cap', }); - assert.equal(await completedTx.isDisplayed(), true); }, ); }); @@ -474,11 +467,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Approve TDN with no spend limit', }); - assert.equal(await completedTx.isDisplayed(), true); }, ); }); @@ -544,11 +536,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Approve TDN with no spend limit', }); - assert.equal(await completedTx.isDisplayed(), true); }, ); }); From 7ae2c9409531cab077182c5e1c020c731e475e1b Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 18 Oct 2024 23:38:01 +0800 Subject: [PATCH 43/51] feat: add BTC send flow (#27964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR enables the send feature for `@metamask/bitcoin-wallet-snap` ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/564 ## **Manual testing steps** 1. Create a btc account 2. Switch to the btc account 3. Click send ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- shared/lib/accounts/bitcoin-wallet-snap.ts | 1 - .../flask/btc/btc-account-overview.spec.ts | 2 +- .../app/wallet-overview/btc-overview.test.tsx | 9 +- .../app/wallet-overview/btc-overview.tsx | 2 +- .../app/wallet-overview/coin-buttons.tsx | 96 +++++++++++++++---- ui/store/actions.ts | 20 ++++ yarn.lock | 10 +- 8 files changed, 109 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 6c52604d6b84..d01735a7e755 100644 --- a/package.json +++ b/package.json @@ -302,7 +302,7 @@ "@metamask/approval-controller": "^7.0.0", "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", - "@metamask/bitcoin-wallet-snap": "^0.7.0", + "@metamask/bitcoin-wallet-snap": "^0.8.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", diff --git a/shared/lib/accounts/bitcoin-wallet-snap.ts b/shared/lib/accounts/bitcoin-wallet-snap.ts index 58f367b173e1..c068e4e8e35c 100644 --- a/shared/lib/accounts/bitcoin-wallet-snap.ts +++ b/shared/lib/accounts/bitcoin-wallet-snap.ts @@ -3,7 +3,6 @@ import { SnapId } from '@metamask/snaps-sdk'; // the Snap is being pre-installed only for Flask build (for the moment). import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; -// export const BITCOIN_WALLET_SNAP_ID: SnapId = 'local:http://localhost:8080'; export const BITCOIN_WALLET_SNAP_ID: SnapId = BitcoinWalletSnap.snapId as SnapId; diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts index 24eedb60b6a2..f32a48d9c4a8 100644 --- a/test/e2e/flask/btc/btc-account-overview.spec.ts +++ b/test/e2e/flask/btc/btc-account-overview.spec.ts @@ -16,7 +16,7 @@ describe('BTC Account - Overview', function (this: Suite) { await driver.waitForSelector({ text: 'Send', tag: 'button', - css: '[disabled]', + css: '[data-testid="coin-overview-send"]', }); await driver.waitForSelector({ diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index c7bb501ee98f..abff2cb2b239 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -19,7 +19,6 @@ const BTC_OVERVIEW_BUY = 'coin-overview-buy'; const BTC_OVERVIEW_BRIDGE = 'coin-overview-bridge'; const BTC_OVERVIEW_RECEIVE = 'coin-overview-receive'; const BTC_OVERVIEW_SWAP = 'token-overview-button-swap'; -const BTC_OVERVIEW_SEND = 'coin-overview-send'; const BTC_OVERVIEW_PRIMARY_CURRENCY = 'coin-overview__primary-currency'; const mockMetaMetricsId = 'deadbeef'; @@ -158,14 +157,10 @@ describe('BtcOverview', () => { expect(spinner).toBeInTheDocument(); }); - it('buttons Send/Swap/Bridge are disabled', () => { + it('buttons Swap/Bridge are disabled', () => { const { queryByTestId } = renderWithProvider(, getStore()); - for (const buttonTestId of [ - BTC_OVERVIEW_SEND, - BTC_OVERVIEW_SWAP, - BTC_OVERVIEW_BRIDGE, - ]) { + for (const buttonTestId of [BTC_OVERVIEW_SWAP, BTC_OVERVIEW_BRIDGE]) { const button = queryByTestId(buttonTestId); expect(button).toBeInTheDocument(); expect(button).toBeDisabled(); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx index e5d0b0103805..dc47df7567b5 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -27,7 +27,7 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { balanceIsCached={false} className={className} chainId={chainId} - isSigningEnabled={false} + isSigningEnabled={true} isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 63bcdd2f58e6..bac7872c79e3 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -1,4 +1,11 @@ -import React, { useCallback, useContext, useState } from 'react'; +import React, { + useCallback, + useContext, + useState, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + useEffect, + ///: END:ONLY_INCLUDE_IF +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, @@ -16,6 +23,9 @@ import { CaipChainId, } from '@metamask/utils'; +///: BEGIN:ONLY_INCLUDE_IF(build-flask) +import { BtcAccountType } from '@metamask/keyring-api'; +///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { ChainId } from '../../../../shared/constants/network'; ///: END:ONLY_INCLUDE_IF @@ -27,6 +37,9 @@ import { ///: END:ONLY_INCLUDE_IF import { I18nContext } from '../../../contexts/i18n'; import { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + CONFIRMATION_V_NEXT_ROUTE, + ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) PREPARE_SWAP_ROUTE, ///: END:ONLY_INCLUDE_IF @@ -39,6 +52,9 @@ import { ///: END:ONLY_INCLUDE_IF getUseExternalServices, getSelectedAccount, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + getMemoizedUnapprovedTemplatedConfirmations, + ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; import Tooltip from '../../ui/tooltip'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -67,6 +83,13 @@ import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import useBridging from '../../../hooks/bridge/useBridging'; ///: END:ONLY_INCLUDE_IF import { ReceiveModal } from '../../multichain/receive-modal'; +///: BEGIN:ONLY_INCLUDE_IF(build-flask) +import { + sendMultichainTransaction, + setDefaultHomeActiveTabName, +} from '../../../store/actions'; +import { BITCOIN_WALLET_SNAP_ID } from '../../../../shared/lib/accounts/bitcoin-wallet-snap'; +///: END:ONLY_INCLUDE_IF const CoinButtons = ({ chainId, @@ -99,7 +122,8 @@ const CoinButtons = ({ const trackEvent = useContext(MetaMetricsContext); const [showReceiveModal, setShowReceiveModal] = useState(false); - const { address: selectedAddress } = useSelector(getSelectedAccount); + const account = useSelector(getSelectedAccount); + const { address: selectedAddress } = account; const history = useHistory(); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const location = useLocation(); @@ -230,23 +254,61 @@ const CoinButtons = ({ const { openBridgeExperience } = useBridging(); ///: END:ONLY_INCLUDE_IF - const handleSendOnClick = useCallback(async () => { - trackEvent( - { - event: MetaMetricsEventName.NavSendButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - token_symbol: 'ETH', - location: 'Home', - text: 'Send', - chain_id: chainId, - }, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + const unapprovedTemplatedConfirmations = useSelector( + getMemoizedUnapprovedTemplatedConfirmations, + ); + + useEffect(() => { + const templatedSnapApproval = unapprovedTemplatedConfirmations.find( + (approval) => { + return ( + approval.type === 'snap_dialog' && + approval.origin === BITCOIN_WALLET_SNAP_ID + ); }, - { excludeMetaMetricsId: false }, ); - await dispatch(startNewDraftTransaction({ type: AssetType.native })); - history.push(SEND_ROUTE); - }, [chainId]); + + if (templatedSnapApproval) { + history.push(`${CONFIRMATION_V_NEXT_ROUTE}/${templatedSnapApproval.id}`); + } + }, [unapprovedTemplatedConfirmations, history]); + ///: END:ONLY_INCLUDE_IF + + const handleSendOnClick = useCallback(async () => { + switch (account.type) { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + case BtcAccountType.P2wpkh: { + await sendMultichainTransaction( + BITCOIN_WALLET_SNAP_ID, + account.id, + chainId as CaipChainId, + ); + + // We automatically switch to the activity tab once the transaction has been sent. + dispatch(setDefaultHomeActiveTabName('activity')); + break; + } + ///: END:ONLY_INCLUDE_IF + default: { + trackEvent( + { + event: MetaMetricsEventName.NavSendButtonClicked, + category: MetaMetricsEventCategory.Navigation, + properties: { + token_symbol: 'ETH', + location: 'Home', + text: 'Send', + chain_id: chainId, + }, + }, + { excludeMetaMetricsId: false }, + ); + await dispatch(startNewDraftTransaction({ type: AssetType.native })); + history.push(SEND_ROUTE); + } + } + }, [chainId, account]); const handleSwapOnClick = useCallback(async () => { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) diff --git a/ui/store/actions.ts b/ui/store/actions.ts index a81dabb5e5c6..f7d839946635 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -43,6 +43,7 @@ import { InterfaceState } from '@metamask/snaps-sdk'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { NotificationServicesController } from '@metamask/notification-services-controller'; import { Patch } from 'immer'; +import { HandlerType } from '@metamask/snaps-utils'; import switchDirection from '../../shared/lib/switch-direction'; import { ENVIRONMENT_TYPE_NOTIFICATION, @@ -5841,3 +5842,22 @@ function applyPatches( return newState; } + +export async function sendMultichainTransaction( + snapId: string, + account: string, + scope: string, +) { + await handleSnapRequest({ + snapId, + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + method: 'startSendTransactionFlow', + params: { + account, + scope, + }, + }, + }); +} diff --git a/yarn.lock b/yarn.lock index 52894233bfab..af059f8960e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4982,10 +4982,10 @@ __metadata: languageName: node linkType: hard -"@metamask/bitcoin-wallet-snap@npm:^0.7.0": - version: 0.7.0 - resolution: "@metamask/bitcoin-wallet-snap@npm:0.7.0" - checksum: 10/be4eceef1715c5e6d33d095d5b4aaa974656d945ff0ed0304fdc1244eb8940eb8978f304378367642aa8fd60d6b375eecc2a4653c38ba62ec306c03955c96682 +"@metamask/bitcoin-wallet-snap@npm:^0.8.1": + version: 0.8.1 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.8.1" + checksum: 10/0fff706a98c6f798ae0ae78bf9a8913c0b056b18aff64f994e521c5005ab7e326fafe1d383b2b7c248456948eaa263df3b31a081d620d82ed7c266857c94a955 languageName: node linkType: hard @@ -26098,7 +26098,7 @@ __metadata: "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" - "@metamask/bitcoin-wallet-snap": "npm:^0.7.0" + "@metamask/bitcoin-wallet-snap": "npm:^0.8.1" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" From e8bc6a5abb2b1ffda5bd645d9ce283508a05c6eb Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Fri, 18 Oct 2024 13:21:15 -0400 Subject: [PATCH 44/51] perf: Create custom trace to measure performance of opening the account list (#27907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27907?quickstart=1) Adds custom Sentry trace "`Account List`" that starts when the `AccountPicker` component is clicked, and ends when the `AccountListMenu` component has finished rendering. - Baseline performance: Screenshot 2024-10-16 at 9 45 28 AM - Load testing via revert of https://github.com/MetaMask/metamask-extension/pull/23933: Screenshot 2024-10-16 at 9 45 40 AM ## **Related issues** - Closes https://github.com/MetaMask/MetaMask-planning/issues/3399 ## **Manual testing steps** ## **Screenshots/Recordings** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/lib/trace.ts | 1 + .../account-list-menu/account-list-menu.tsx | 12 +++++++++++- .../multichain/account-picker/account-picker.js | 6 +++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index ab1deefd1cc5..1dd50b736222 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -9,6 +9,7 @@ import { log as sentryLogger } from '../../app/scripts/lib/setupSentry'; * The supported trace names. */ export enum TraceName { + AccountList = 'Account List', BackgroundConnect = 'Background Connect', DeveloperTest = 'Developer Test', FirstRender = 'First Render', diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index 2e5925dbf9cf..19d313aedf54 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -1,4 +1,10 @@ -import React, { useContext, useState, useMemo, useCallback } from 'react'; +import React, { + useContext, + useState, + useMemo, + useCallback, + useEffect, +} from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import Fuse from 'fuse.js'; @@ -99,6 +105,7 @@ import { AccountConnections, MergedInternalAccount, } from '../../../selectors/selectors.types'; +import { endTrace, TraceName } from '../../../../shared/lib/trace'; import { HiddenAccountList } from './hidden-account-list'; const ACTION_MODES = { @@ -198,6 +205,9 @@ export const AccountListMenu = ({ }: AccountListMenuProps) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); + useEffect(() => { + endTrace({ name: TraceName.AccountList }); + }, []); const accounts: InternalAccountWithBalance[] = useSelector( getMetaMaskAccountsOrdered, ); diff --git a/ui/components/multichain/account-picker/account-picker.js b/ui/components/multichain/account-picker/account-picker.js index 18c516cf3cab..20d382923190 100644 --- a/ui/components/multichain/account-picker/account-picker.js +++ b/ui/components/multichain/account-picker/account-picker.js @@ -31,6 +31,7 @@ import { shortenAddress } from '../../../helpers/utils/util'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { getCustodianIconForAddress } from '../../../selectors/institutional/selectors'; ///: END:ONLY_INCLUDE_IF +import { trace, TraceName } from '../../../../shared/lib/trace'; export const AccountPicker = ({ address, @@ -58,7 +59,10 @@ export const AccountPicker = ({ { + trace({ name: TraceName.AccountList }); + onClick(); + }} backgroundColor={BackgroundColor.transparent} borderRadius={BorderRadius.LG} ellipsis From a9df78b942df10c8eb25572feff22f045bee0a62 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Fri, 18 Oct 2024 13:44:34 -0500 Subject: [PATCH 45/51] test: Remove delays from onboarding tests (#27961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** .delay() calls were errantly left in the onboarding tests. Apologies to @seaona for not having addressed these sooner! [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27961?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/tests/onboarding/onboarding.spec.js | 8 ------- .../tests/privacy/basic-functionality.spec.js | 21 +++++++++---------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index b5e273b7e978..de040f825ee6 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -20,8 +20,6 @@ const { onboardingCompleteWalletCreation, regularDelayMs, unlockWallet, - tinyDelayMs, - largeDelayMs, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { @@ -287,7 +285,6 @@ describe('MetaMask onboarding @no-mmi', function () { await driver.clickElement({ text: 'General', }); - await driver.delay(largeDelayMs); await driver.clickElement({ text: 'Add a network' }); await driver.waitForSelector( @@ -311,9 +308,7 @@ describe('MetaMask onboarding @no-mmi', function () { const rpcUrlInputDropDown = await driver.waitForSelector( '[data-testid="test-add-rpc-drop-down"]', ); - await driver.delay(tinyDelayMs); await rpcUrlInputDropDown.click(); - await driver.delay(tinyDelayMs); await driver.clickElement({ text: 'Add RPC URL', tag: 'button', @@ -371,7 +366,6 @@ describe('MetaMask onboarding @no-mmi', function () { await driver.clickElement( `[data-rbd-draggable-id="${toHex(chainId)}"]`, ); - await driver.delay(largeDelayMs); // Check localhost 8546 is selected and its balance value is correct await driver.findElement({ css: '[data-testid="network-display"]', @@ -530,8 +524,6 @@ describe('MetaMask onboarding @no-mmi', function () { // pin extension walkthrough screen await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.delay(regularDelayMs); - for (let i = 0; i < mockedEndpoints.length; i += 1) { const mockedEndpoint = await mockedEndpoints[i]; const isPending = await mockedEndpoint.isPending(); diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index 6ae14ca660be..674ba8772e29 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -4,9 +4,6 @@ const { withFixtures, importSRPOnboardingFlow, WALLET_PASSWORD, - tinyDelayMs, - regularDelayMs, - largeDelayMs, defaultGanacheOptions, } = require('../../helpers'); const { METAMASK_STALELIST_URL } = require('../phishing-controller/helpers'); @@ -65,8 +62,6 @@ describe('MetaMask onboarding @no-mmi', function () { }); await driver.clickElement('[data-testid="category-item-General"]'); - await driver.delay(regularDelayMs); - await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); @@ -74,9 +69,7 @@ describe('MetaMask onboarding @no-mmi', function () { await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="category-item-Assets"]'); - await driver.delay(regularDelayMs); await driver.clickElement( '[data-testid="currency-rate-check-toggle"] .toggle-button', ); @@ -114,7 +107,6 @@ describe('MetaMask onboarding @no-mmi', function () { await driver.clickElement('[data-testid="network-display"]'); await driver.clickElement({ text: 'Ethereum Mainnet', tag: 'p' }); - await driver.delay(tinyDelayMs); // Wait until network is fully switched and refresh tokens before asserting to mitigate flakiness await driver.assertElementNotPresent('.loading-overlay'); @@ -154,13 +146,20 @@ describe('MetaMask onboarding @no-mmi', function () { tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); - await driver.delay(largeDelayMs); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="category-back-button"]', + ); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.delay(largeDelayMs); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await driver.delay(largeDelayMs); await driver.clickElement({ text: 'Done', tag: 'button' }); await driver.clickElement('[data-testid="pin-extension-next"]'); await driver.clickElement({ text: 'Done', tag: 'button' }); From 6794a109454fedb734c6d2debf6f7eed2f79024a Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Sat, 19 Oct 2024 02:33:30 -0230 Subject: [PATCH 46/51] chore: Disable account syncing in prod (#27943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR disables account syncing in prod until we have e2e tests and documentation of proper testing [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27943?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. `yarn build` 2. Account syncing should not occur ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Mark Stacey --- app/scripts/metamask-controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fae6eedc2ab8..0566afc5067a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -156,6 +156,7 @@ import { NotificationServicesPushController, NotificationServicesController, } from '@metamask/notification-services-controller'; +import { isProduction } from '../../shared/modules/environment'; import { methodsRequiringNetworkSwitch } from '../../shared/constants/methods-tags'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -1561,7 +1562,7 @@ export default class MetamaskController extends EventEmitter { }, }, env: { - isAccountSyncingEnabled: isManifestV3, + isAccountSyncingEnabled: !isProduction() && isManifestV3, }, messenger: this.controllerMessenger.getRestricted({ name: 'UserStorageController', From 4eaeb686acc34bda3d78cbcde357869ef7cadb76 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Sat, 19 Oct 2024 12:09:35 +0200 Subject: [PATCH 47/51] fix(snaps): Remove arrows of custom UI inputs (#27953) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR removes the HTML arrows in custom UI inputs of type `number`. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27953?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** ```ts import { Box, Button, Container, Footer, Heading, Input, Text, type SnapComponent, } from '@metamask/snaps-sdk/jsx'; /** * A custom dialog component. * * @returns The custom dialog component. */ export const CustomDialog: SnapComponent = () => (
    ); ``` ## **Screenshots/Recordings** ### **Before** ![Screenshot from 2024-10-18 13-00-54](https://github.com/user-attachments/assets/eb96f33d-0cd9-4b26-ad42-ccfae207d0f5) ### **After** ![Screenshot from 2024-10-18 12-57-41](https://github.com/user-attachments/assets/08a0cf66-369f-49f1-a51a-7b93cf70e016) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/components/app/snaps/snap-ui-input/index.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/components/app/snaps/snap-ui-input/index.scss b/ui/components/app/snaps/snap-ui-input/index.scss index 8dfc06f10fcc..abe966e822a1 100644 --- a/ui/components/app/snaps/snap-ui-input/index.scss +++ b/ui/components/app/snaps/snap-ui-input/index.scss @@ -3,6 +3,17 @@ gap: 8px; } + & .mm-text-field > input { + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &[type=number] { + -moz-appearance: textfield; + } + } & .snap-ui-renderer__image { From f416f1e20b9f505d4b906e92358d9cd257ab4765 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 21 Oct 2024 16:51:53 +0800 Subject: [PATCH 48/51] feat: add migration 131 (#27364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces a new migration that updates the `selectedAccount` to the first account if available or the default state. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/26377 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Charly Chevalier --- app/scripts/migrations/131.test.ts | 244 +++++++++++++++++++++++++++++ app/scripts/migrations/131.ts | 147 +++++++++++++++++ app/scripts/migrations/index.js | 1 + 3 files changed, 392 insertions(+) create mode 100644 app/scripts/migrations/131.test.ts create mode 100644 app/scripts/migrations/131.ts diff --git a/app/scripts/migrations/131.test.ts b/app/scripts/migrations/131.test.ts new file mode 100644 index 000000000000..ab359ff7283c --- /dev/null +++ b/app/scripts/migrations/131.test.ts @@ -0,0 +1,244 @@ +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { cloneDeep } from 'lodash'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; +import { migrate, version } from './131'; + +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + +const oldVersion = 130; + +const mockInternalAccount = createMockInternalAccount(); +const mockAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: { + [mockInternalAccount.id]: mockInternalAccount, + }, + selectedAccount: mockInternalAccount.id, + }, +}; + +describe(`migration #${version}`, () => { + afterEach(() => jest.resetAllMocks()); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('updates selected account if it is not found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: 'unknown id', + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data.AccountsController).toStrictEqual({ + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: mockInternalAccount.id, + }, + }); + }); + + it('does nothing if the selectedAccount is found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if AccountsController state is missing', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + OtherController: {}, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('sets selected account to default state if there are no accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + accounts: {}, + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data.AccountsController).toStrictEqual({ + ...oldStorage.data.AccountsController, + internalAccounts: { + ...oldStorage.data.AccountsController.internalAccounts, + selectedAccount: '', + }, + }); + }); + + it('does nothing if selectedAccount is unset', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: '', + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + const invalidState = [ + { + errorMessage: `Migration ${version}: Invalid AccountsController state of type 'string'`, + label: 'AccountsController type', + state: { AccountsController: 'invalid' }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController state, missing internalAccounts`, + label: 'Missing internalAccounts', + state: { AccountsController: {} }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state of type 'string'`, + label: 'Invalid internalAccounts', + state: { AccountsController: { internalAccounts: 'invalid' } }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state, missing selectedAccount`, + label: 'Missing selectedAccount', + state: { AccountsController: { internalAccounts: { accounts: {} } } }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.selectedAccount state of type 'object'`, + label: 'Invalid selectedAccount', + state: { + AccountsController: { + internalAccounts: { accounts: {}, selectedAccount: {} }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state, missing accounts`, + label: 'Missing accounts', + state: { + AccountsController: { + internalAccounts: { selectedAccount: '' }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state of type 'string'`, + label: 'Invalid accounts', + state: { + AccountsController: { + internalAccounts: { accounts: 'invalid', selectedAccount: '' }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found of type 'string'`, + label: 'Invalid accounts entry', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: 'invalid' }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found that is missing an id`, + label: 'Missing ID in accounts entry', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: {} }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found with an id of type 'object'`, + label: 'Invalid ID for accounts entry', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: { id: {} } }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + ]; + + // @ts-expect-error 'each' function missing from type definitions, but it does exist + it.each(invalidState)( + 'captures error when state is invalid due to: $label', + async ({ + errorMessage, + state, + }: { + errorMessage: string; + state: Record; + }) => { + const oldStorage = { + meta: { version: oldVersion }, + data: state, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(errorMessage), + ); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }, + ); +}); diff --git a/app/scripts/migrations/131.ts b/app/scripts/migrations/131.ts new file mode 100644 index 000000000000..9d2ebf970fbd --- /dev/null +++ b/app/scripts/migrations/131.ts @@ -0,0 +1,147 @@ +import { hasProperty } from '@metamask/utils'; +import { cloneDeep, isObject } from 'lodash'; +import log from 'loglevel'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 131; + +/** + * Fix AccountsController state corruption, where the `selectedAccount` state is set to an invalid + * ID. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record): void { + if (!hasProperty(state, 'AccountsController')) { + return; + } + + const accountsControllerState = state.AccountsController; + + if (!isObject(accountsControllerState)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController state of type '${typeof accountsControllerState}'`, + ), + ); + return; + } else if (!hasProperty(accountsControllerState, 'internalAccounts')) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController state, missing internalAccounts`, + ), + ); + return; + } else if (!isObject(accountsControllerState.internalAccounts)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state of type '${typeof accountsControllerState.internalAccounts}'`, + ), + ); + return; + } else if ( + !hasProperty(accountsControllerState.internalAccounts, 'selectedAccount') + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state, missing selectedAccount`, + ), + ); + return; + } else if ( + typeof accountsControllerState.internalAccounts.selectedAccount !== 'string' + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.selectedAccount state of type '${typeof accountsControllerState + .internalAccounts.selectedAccount}'`, + ), + ); + return; + } else if ( + !hasProperty(accountsControllerState.internalAccounts, 'accounts') + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state, missing accounts`, + ), + ); + return; + } else if (!isObject(accountsControllerState.internalAccounts.accounts)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state of type '${typeof accountsControllerState + .internalAccounts.accounts}'`, + ), + ); + return; + } + + if ( + Object.keys(accountsControllerState.internalAccounts.accounts).length === 0 + ) { + // In this case since there aren't any accounts, we set the selected account to the default state to unblock the extension. + accountsControllerState.internalAccounts.selectedAccount = ''; + return; + } else if (accountsControllerState.internalAccounts.selectedAccount === '') { + log.warn(`Migration ${version}: Skipping, no selected account set`); + return; + } + + // Safe to use index 0, we already check for the length before. + const firstAccount = Object.values( + accountsControllerState.internalAccounts.accounts, + )[0]; + if (!isObject(firstAccount)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found of type '${typeof firstAccount}'`, + ), + ); + return; + } else if (!hasProperty(firstAccount, 'id')) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found that is missing an id`, + ), + ); + return; + } else if (typeof firstAccount.id !== 'string') { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found with an id of type '${typeof firstAccount.id}'`, + ), + ); + return; + } + + // If the currently selected account ID is not on the `accounts` object, then + // we fallback to first account of the wallet. + if ( + !hasProperty( + accountsControllerState.internalAccounts.accounts, + accountsControllerState.internalAccounts.selectedAccount, + ) + ) { + accountsControllerState.internalAccounts.selectedAccount = firstAccount.id; + } +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index a72fd34c3c28..d2c63eb2e35c 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -151,6 +151,7 @@ const migrations = [ require('./128'), require('./129'), require('./130'), + require('./131'), ]; export default migrations; From 15962f7fa05e5185401abeb1ad6fe01ae2940c90 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang <7315988+dawnseeker8@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:28:12 +0800 Subject: [PATCH 49/51] feat(metametrics): use specific `account_hardware_type` for OneKey devices (#27296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Currently extension supports connecting to OneKey via Trezor, but we don't have specific metrics to log this when importing the accounts. Now, the `account_hardware_type` will be set to `OneKey via Trezor` for `AccountAdded` metric when using OneKey devices. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27296?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/586 ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Charly Chevalier --- .../trezor-offscreen-bridge.ts | 5 +- app/scripts/metamask-controller.js | 28 +++++++- app/scripts/metamask-controller.test.js | 71 ++++++++++++++++++- offscreen/scripts/trezor.ts | 5 +- shared/constants/hardware-wallets.ts | 1 + .../create-account/connect-hardware/index.js | 19 +++-- .../connect-hardware/index.test.tsx | 2 + ui/store/actions.test.js | 44 ++++++++++++ ui/store/actions.ts | 27 +++++++ 9 files changed, 194 insertions(+), 8 deletions(-) diff --git a/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts b/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts index 83272015ae2d..0f94627f2836 100644 --- a/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts +++ b/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts @@ -30,6 +30,8 @@ import { export class TrezorOffscreenBridge implements TrezorBridge { model: string | undefined; + minorVersion: number | undefined; + init( settings: { manifest: Manifest; @@ -40,7 +42,8 @@ export class TrezorOffscreenBridge implements TrezorBridge { msg.target === OffscreenCommunicationTarget.extension && msg.event === OffscreenCommunicationEvents.trezorDeviceConnect ) { - this.model = msg.payload; + this.model = msg.payload.model; + this.minorVersion = msg.payload.minorVersion; } }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0566afc5067a..c33485f665b7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -387,6 +387,9 @@ export const METAMASK_CONTROLLER_EVENTS = { // stream channels const PHISHING_SAFELIST = 'metamask-phishing-safelist'; +// OneKey devices can connect to Metamask using Trezor USB transport. They use a specific device minor version (99) to differentiate between genuine Trezor and OneKey devices. +export const ONE_KEY_VIA_TREZOR_MINOR_VERSION = 99; + export default class MetamaskController extends EventEmitter { /** * @param {object} opts @@ -3400,6 +3403,7 @@ export default class MetamaskController extends EventEmitter { connectHardware: this.connectHardware.bind(this), forgetDevice: this.forgetDevice.bind(this), checkHardwareStatus: this.checkHardwareStatus.bind(this), + getDeviceNameForMetric: this.getDeviceNameForMetric.bind(this), unlockHardwareWalletAccount: this.unlockHardwareWalletAccount.bind(this), attemptLedgerTransportCreation: this.attemptLedgerTransportCreation.bind(this), @@ -4684,6 +4688,26 @@ export default class MetamaskController extends EventEmitter { return keyring.isUnlocked(); } + /** + * Get hardware device name for metric logging. + * + * @param deviceName - HardwareDeviceNames + * @param hdPath - string + * @returns {Promise} + */ + async getDeviceNameForMetric(deviceName, hdPath) { + if (deviceName === HardwareDeviceNames.trezor) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath); + const { minorVersion } = keyring.bridge; + // Specific case for OneKey devices, see `ONE_KEY_VIA_TREZOR_MINOR_VERSION` for further details. + if (minorVersion && minorVersion === ONE_KEY_VIA_TREZOR_MINOR_VERSION) { + return HardwareDeviceNames.oneKeyViaTrezor; + } + } + + return deviceName; + } + /** * Clear * @@ -4756,9 +4780,11 @@ export default class MetamaskController extends EventEmitter { /** * get hardware account label * + * @param name + * @param index + * @param hdPathDescription * @returns string label */ - getAccountLabel(name, index, hdPathDescription) { return `${name[0].toUpperCase()}${name.slice(1)} ${ parseInt(index, 10) + 1 diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 77b062bcfdc7..750f8771568b 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -45,7 +45,9 @@ import { } from './lib/accounts/BalancesController'; import { BalancesTracker as MultichainBalancesTracker } from './lib/accounts/BalancesTracker'; import { deferredPromise } from './lib/util'; -import MetaMaskController from './metamask-controller'; +import MetaMaskController, { + ONE_KEY_VIA_TREZOR_MINOR_VERSION, +} from './metamask-controller'; const { Ganache } = require('../../test/e2e/seeder/ganache'); @@ -894,6 +896,73 @@ describe('MetaMaskController', () => { ); }); + describe('getHardwareDeviceName', () => { + const hdPath = "m/44'/60'/0'/0/0"; + + it('should return the correct device name for Ledger', async () => { + const deviceName = 'ledger'; + + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('ledger'); + }); + + it('should return the correct device name for Lattice', async () => { + const deviceName = 'lattice'; + + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('lattice'); + }); + + it('should return the correct device name for Trezor', async () => { + const deviceName = 'trezor'; + jest + .spyOn(metamaskController, 'getKeyringForDevice') + .mockResolvedValue({ + bridge: { + minorVersion: 1, + model: 'T', + }, + }); + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('trezor'); + }); + + it('should return undefined for unknown device name', async () => { + const deviceName = 'unknown'; + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe(deviceName); + }); + + it('should handle special case for OneKeyDevice via Trezor', async () => { + const deviceName = 'trezor'; + jest + .spyOn(metamaskController, 'getKeyringForDevice') + .mockResolvedValue({ + bridge: { + model: 'T', + minorVersion: ONE_KEY_VIA_TREZOR_MINOR_VERSION, + }, + }); + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('OneKey via Trezor'); + }); + }); + describe('forgetDevice', () => { it('should throw if it receives an unknown device name', async () => { const result = metamaskController.forgetDevice( diff --git a/offscreen/scripts/trezor.ts b/offscreen/scripts/trezor.ts index a6c1b5b2788e..22e03048fb93 100644 --- a/offscreen/scripts/trezor.ts +++ b/offscreen/scripts/trezor.ts @@ -40,7 +40,10 @@ export default function init() { chrome.runtime.sendMessage({ target: OffscreenCommunicationTarget.extension, event: OffscreenCommunicationEvents.trezorDeviceConnect, - payload: event.payload.features.model, + payload: { + model: event.payload.features.model, + minorVersion: event.payload.features.minor_version, + }, }); } }); diff --git a/shared/constants/hardware-wallets.ts b/shared/constants/hardware-wallets.ts index 96e50ed7c17e..6fdfbedd9c04 100644 --- a/shared/constants/hardware-wallets.ts +++ b/shared/constants/hardware-wallets.ts @@ -18,6 +18,7 @@ export enum HardwareKeyringNames { export enum HardwareDeviceNames { ledger = 'ledger', trezor = 'trezor', + oneKeyViaTrezor = 'OneKey via Trezor', lattice = 'lattice', qr = 'QR Hardware', } diff --git a/ui/pages/create-account/connect-hardware/index.js b/ui/pages/create-account/connect-hardware/index.js index 2884dacb77b6..85464baccb69 100644 --- a/ui/pages/create-account/connect-hardware/index.js +++ b/ui/pages/create-account/connect-hardware/index.js @@ -28,8 +28,8 @@ import { } from '../../../components/component-library'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { TextColor } from '../../../helpers/constants/design-system'; -import SelectHardware from './select-hardware'; import AccountList from './account-list'; +import SelectHardware from './select-hardware'; const U2F_ERROR = 'U2F'; const LEDGER_ERRORS_CODES = { @@ -277,7 +277,7 @@ class ConnectHardwareForm extends Component { }); }; - onUnlockAccounts = (device, path) => { + onUnlockAccounts = async (device, path) => { const { history, mostRecentOverviewPage, unlockHardwareWalletAccounts } = this.props; const { selectedAccounts } = this.state; @@ -290,6 +290,13 @@ class ConnectHardwareForm extends Component { MEW_PATH === path ? this.context.t('hardwareWalletLegacyDescription') : ''; + + // Get preferred device name for metrics. + const metricDeviceName = await this.props.getDeviceNameForMetric( + device, + path, + ); + return unlockHardwareWalletAccounts( selectedAccounts, device, @@ -302,7 +309,7 @@ class ConnectHardwareForm extends Component { event: MetaMetricsEventName.AccountAdded, properties: { account_type: MetaMetricsEventAccountType.Hardware, - account_hardware_type: device, + account_hardware_type: metricDeviceName, }, }); history.push(mostRecentOverviewPage); @@ -313,7 +320,7 @@ class ConnectHardwareForm extends Component { event: MetaMetricsEventName.AccountAddFailed, properties: { account_type: MetaMetricsEventAccountType.Hardware, - account_hardware_type: device, + account_hardware_type: metricDeviceName, error: e.message, }, }); @@ -439,6 +446,7 @@ class ConnectHardwareForm extends Component { ConnectHardwareForm.propTypes = { connectHardware: PropTypes.func, checkHardwareStatus: PropTypes.func, + getDeviceNameForMetric: PropTypes.func, forgetDevice: PropTypes.func, showAlert: PropTypes.func, hideAlert: PropTypes.func, @@ -472,6 +480,9 @@ const mapDispatchToProps = (dispatch) => { connectHardware: (deviceName, page, hdPath, t) => { return dispatch(actions.connectHardware(deviceName, page, hdPath, t)); }, + getDeviceNameForMetric: (deviceName, hdPath) => { + return dispatch(actions.getDeviceNameForMetric(deviceName, hdPath)); + }, checkHardwareStatus: (deviceName, hdPath) => { return dispatch(actions.checkHardwareStatus(deviceName, hdPath)); }, diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx index 0b8585fd0b5c..3f7782c1416d 100644 --- a/ui/pages/create-account/connect-hardware/index.test.tsx +++ b/ui/pages/create-account/connect-hardware/index.test.tsx @@ -13,10 +13,12 @@ import ConnectHardwareForm from '.'; const mockConnectHardware = jest.fn(); const mockCheckHardwareStatus = jest.fn().mockResolvedValue(false); +const mockGetgetDeviceNameForMetric = jest.fn().mockResolvedValue('ledger'); jest.mock('../../../store/actions', () => ({ connectHardware: () => mockConnectHardware, checkHardwareStatus: () => mockCheckHardwareStatus, + getDeviceNameForMetric: () => mockGetgetDeviceNameForMetric, })); jest.mock('../../../selectors', () => ({ diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 8d72ce63e32d..d86ea20f845c 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -483,6 +483,50 @@ describe('Actions', () => { }); }); + describe('#getDeviceNameForMetric', () => { + const deviceName = 'ledger'; + const hdPath = "m/44'/60'/0'/0/0"; + + afterEach(() => { + sinon.restore(); + }); + + it('calls getDeviceNameForMetric in background', async () => { + const store = mockStore(); + + const mockGetDeviceName = background.getDeviceNameForMetric.callsFake( + (_, __, cb) => cb(), + ); + + setBackgroundConnection(background); + + await store.dispatch(actions.getDeviceNameForMetric(deviceName, hdPath)); + expect(mockGetDeviceName.callCount).toStrictEqual(1); + }); + + it('shows loading indicator and displays error', async () => { + const store = mockStore(); + + background.getDeviceNameForMetric.callsFake((_, __, cb) => + cb(new Error('error')), + ); + + setBackgroundConnection(background); + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', payload: undefined }, + { type: 'DISPLAY_WARNING', payload: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, + ]; + + await expect( + store.dispatch(actions.getDeviceNameForMetric(deviceName, hdPath)), + ).rejects.toThrow('error'); + + expect(store.getActions()).toStrictEqual(expectedActions); + }); + }); + describe('#forgetDevice', () => { afterEach(() => { sinon.restore(); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index f7d839946635..a051bb15d5fd 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -508,6 +508,33 @@ export function checkHardwareStatus( }; } +export function getDeviceNameForMetric( + deviceName: HardwareDeviceNames, + hdPath: string, +): ThunkAction { + log.debug(`background.getDeviceNameForMetric`, deviceName, hdPath); + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(showLoadingIndication()); + + let result: string; + try { + result = await submitRequestToBackground( + 'getDeviceNameForMetric', + [deviceName, hdPath], + ); + } catch (error) { + logErrorWithMessage(error); + dispatch(displayWarning(error)); + throw error; + } finally { + dispatch(hideLoadingIndication()); + } + + await forceUpdateMetamaskState(dispatch); + return result; + }; +} + export function forgetDevice( deviceName: HardwareDeviceNames, ): ThunkAction { From 29e1c5b31f1decbc3e6d50eac82d972727b6d81d Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 21 Oct 2024 12:13:05 +0200 Subject: [PATCH 50/51] fix: Automatically expand first insight (#27872) ## **Description** Automatically expands the first insight on the confirmation page, if any. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27872?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27869 ## **Screenshots/Recordings** ![image](https://github.com/user-attachments/assets/a23f811f-ab9d-456d-9572-183e953b8802) --- test/e2e/snaps/test-snap-siginsights.spec.js | 44 ++----------------- .../snaps/snaps-section/snap-insight.tsx | 3 ++ .../snaps-section/snaps-section.test.tsx | 5 --- .../snaps/snaps-section/snaps-section.tsx | 3 +- 4 files changed, 8 insertions(+), 47 deletions(-) diff --git a/test/e2e/snaps/test-snap-siginsights.spec.js b/test/e2e/snaps/test-snap-siginsights.spec.js index d40cbc83ae35..b72d6e248ff0 100644 --- a/test/e2e/snaps/test-snap-siginsights.spec.js +++ b/test/e2e/snaps/test-snap-siginsights.spec.js @@ -79,22 +79,15 @@ describe('Test Snap Signature Insights', function () { tag: 'p', }); - // click down arrow - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765', tag: 'p', }); + // Click down arrow + await driver.clickElementSafe('[aria-label="Scroll down"]'); + // click sign button await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-button"]', @@ -125,22 +118,11 @@ describe('Test Snap Signature Insights', function () { }); // click down arrow - // await driver.waitForSelector('[aria-label="Scroll down"]'); await driver.clickElementSafe('[aria-label="Scroll down"]'); // required: delay for scroll to render await driver.delay(500); - // click down arrow - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '1', @@ -188,16 +170,6 @@ describe('Test Snap Signature Insights', function () { // required: delay for scroll to render await driver.delay(500); - // click signature insights - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC has been identified as a malicious verifying contract.', @@ -245,16 +217,6 @@ describe('Test Snap Signature Insights', function () { // required: delay for scroll to render await driver.delay(500); - // click signature insights - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC has been identified as a malicious verifying contract.', diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx index b428ee8d158d..7102970b4f84 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx @@ -16,12 +16,14 @@ export type SnapInsightProps = { snapId: string; interfaceId: string; loading: boolean; + isExpanded?: boolean | undefined; }; export const SnapInsight: React.FunctionComponent = ({ snapId, interfaceId, loading, + isExpanded, }) => { const t = useI18nContext(); const { name: snapName } = useSelector((state) => @@ -57,6 +59,7 @@ export const SnapInsight: React.FunctionComponent = ({ { mockStore, ); - fireEvent.click(getByText('Insights from')); - expect(container).toMatchSnapshot(); expect(getByText('Hello world!')).toBeDefined(); }); @@ -79,8 +76,6 @@ describe('SnapsSection', () => { mockStore, ); - fireEvent.click(getByText('Insights from')); - expect(container).toMatchSnapshot(); expect(getByText('Hello world again!')).toBeDefined(); }); diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx index 896411ad4dec..b838255237e6 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx @@ -23,12 +23,13 @@ export const SnapsSection = () => { gap={4} marginBottom={4} > - {data.map(({ snapId, interfaceId, loading }) => ( + {data.map(({ snapId, interfaceId, loading }, index) => ( ))}
    From 81f46786467b9475b1f4ca6b08e0ed40323c102e Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Mon, 21 Oct 2024 18:17:24 +0530 Subject: [PATCH 51/51] fix: error in navigating between transaction when one of the transaction is approve all (#27985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Navigation in transaction header was broken when one of the transaction is of type Approve All ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27913 ## **Manual testing steps** 1. Go to test dapp 2. Submit any re-designed transaction followed by an approve all transaction 3. Try navigating between transaction using header buttons ## **Screenshots/Recordings** TODO ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../info/hooks/useDecodedTransactionData.ts | 14 ++++++++++---- .../components/confirm/title/title.tsx | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts index 6934f893378d..5276e02eaad1 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts @@ -10,18 +10,24 @@ import { DecodedTransactionDataResponse } from '../../../../../../../shared/type import { useConfirmContext } from '../../../../context/confirm'; import { hasTransactionData } from '../../../../../../../shared/modules/transaction.utils'; -export function useDecodedTransactionData(): AsyncResult< - DecodedTransactionDataResponse | undefined -> { +export function useDecodedTransactionData( + transactionTypeFilter?: string, +): AsyncResult { const { currentConfirmation } = useConfirmContext(); + const currentTransactionType = currentConfirmation?.type; const chainId = currentConfirmation?.chainId as Hex; const contractAddress = currentConfirmation?.txParams?.to as Hex; const transactionData = currentConfirmation?.txParams?.data as Hex; const transactionTo = currentConfirmation?.txParams?.to as Hex; return useAsyncResult(async () => { - if (!hasTransactionData(transactionData) || !transactionTo) { + if ( + !hasTransactionData(transactionData) || + !transactionTo || + (transactionTypeFilter && + currentTransactionType !== transactionTypeFilter) + ) { return undefined; } diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 969e9c05518d..a926c0f6b482 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -174,11 +174,12 @@ const ConfirmTitle: React.FC = memo(() => { let isRevokeSetApprovalForAll = false; let revokePending = false; + const decodedResponse = useDecodedTransactionData( + TransactionType.tokenMethodSetApprovalForAll, + ); if ( currentConfirmation?.type === TransactionType.tokenMethodSetApprovalForAll ) { - const decodedResponse = useDecodedTransactionData(); - isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll( decodedResponse.value, );