From 0e822767d90ce5ba5fecd0e6a6bb835bcc8e5960 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 26 Sep 2024 09:29:18 +0100 Subject: [PATCH 001/226] feat: ERC20 Revoke Allowance (#26906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Includes e2e test. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26906?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3004 ## **Manual testing steps** 1. Go to https://etherscan.io/token/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599#writeContract 2. Connect your wallet 3. Go to approve 4. Input an address under spender 5. Input 0 under value 6. Click write 7. Notice MM confirmation ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-09-04 at 17 00 32 ## **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: Priya Narayanaswamy --- app/_locales/en/messages.json | 6 + .../erc20-approve-redesign.spec.ts | 5 +- .../increase-token-allowance-redesign.spec.ts | 39 +++---- .../revoke-allowance-redesign.spec.ts | 110 ++++++++++++++++++ .../__snapshots__/approve.test.tsx.snap | 4 +- .../approve-static-simulation.tsx | 4 +- .../confirm/info/approve/approve.test.tsx | 8 ++ .../confirm/info/approve/approve.tsx | 34 +++++- .../edit-spending-cap-modal.test.tsx | 21 ++++ .../hooks/use-approve-token-simulation.ts | 24 +++- .../approve/revoke-details/revoke-details.tsx | 11 ++ .../revoke-static-simulation.tsx | 60 ++++++++++ .../spending-cap/spending-cap.test.tsx | 8 ++ .../title/hooks/useCurrentSpendingCap.test.ts | 23 ++++ .../title/hooks/useCurrentSpendingCap.ts | 49 ++++++++ .../components/confirm/title/title.test.tsx | 16 +++ .../components/confirm/title/title.tsx | 19 ++- ui/pages/confirmations/confirm/confirm.tsx | 4 +- .../confirmations/hooks/useAssetDetails.js | 8 ++ 19 files changed, 414 insertions(+), 39 deletions(-) create mode 100644 test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts create mode 100644 ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx create mode 100644 ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts create mode 100644 ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index e312be4794e5..80f9d2090814 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1068,6 +1068,9 @@ "confirmTitlePermitTokens": { "message": "Spending cap request" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Remove permission" + }, "confirmTitleSIWESignature": { "message": "Sign-in request" }, @@ -4513,6 +4516,9 @@ "revokePermission": { "message": "Revoke permission" }, + "revokeSimulationDetailsDesc": { + "message": "You're removing someone's permission to spend tokens from your account." + }, "revokeSpendingCap": { "message": "Revoke spending cap for your $1", "description": "$1 is a token symbol" diff --git a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts index 60a141144833..baa3638330b6 100644 --- a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts @@ -87,9 +87,10 @@ describe('Confirmation Redesign ERC20 Approve Component', function () { }); }); -async function mocked4Bytes(mockServer: MockttpServer) { +export async function mocked4BytesApprove(mockServer: MockttpServer) { return await mockServer .forGet('https://www.4byte.directory/api/v1/signatures/') + .always() .withQuery({ hex_signature: '0x095ea7b3' }) .thenCallback(() => ({ statusCode: 200, @@ -111,7 +112,7 @@ async function mocked4Bytes(mockServer: MockttpServer) { } async function mocks(server: MockttpServer) { - return [await mocked4Bytes(server)]; + return [await mocked4BytesApprove(server)]; } export async function importTST(driver: Driver) { diff --git a/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts index 2571a69107b3..4eed23b20f44 100644 --- a/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts @@ -108,11 +108,27 @@ function generateFixtureOptionsForEIP1559Tx(mochaContext: Mocha.Context) { }; } +async function createAndAssertIncreaseAllowanceSubmission( + driver: Driver, + newSpendingCap: string, + contractRegistry?: GanacheContractAddressRegistry, +) { + await openDAppWithContract(driver, contractRegistry, SMART_CONTRACTS.HST); + + await createERC20IncreaseAllowanceTransaction(driver); + + await editSpendingCap(driver, newSpendingCap); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, newSpendingCap); +} + async function mocks(server: Mockttp) { return [await mocked4BytesIncreaseAllowance(server)]; } -async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { +export async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { return await mockServer .forGet('https://www.4byte.directory/api/v1/signatures/') .always() @@ -131,7 +147,6 @@ async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { text_signature: 'increaseAllowance(address,uint256)', hex_signature: '0x39509351', bytes_signature: '9P“Q', - test: 'Priya', }, ], }, @@ -139,28 +154,12 @@ async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { }); } -async function createAndAssertIncreaseAllowanceSubmission( - driver: Driver, - newSpendingCap: string, - contractRegistry?: GanacheContractAddressRegistry, -) { - await openDAppWithContract(driver, contractRegistry, SMART_CONTRACTS.HST); - - await createERC20IncreaseAllowanceTransaction(driver); - - await editSpendingCap(driver, newSpendingCap); - - await scrollAndConfirmAndAssertConfirm(driver); - - await assertChangedSpendingCap(driver, newSpendingCap); -} - async function createERC20IncreaseAllowanceTransaction(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#increaseTokenAllowance'); } -async function editSpendingCap(driver: Driver, newSpendingCap: string) { +export async function editSpendingCap(driver: Driver, newSpendingCap: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement('[data-testid="edit-spending-cap-icon"'); @@ -177,7 +176,7 @@ async function editSpendingCap(driver: Driver, newSpendingCap: string) { await driver.delay(veryLargeDelayMs * 2); } -async function assertChangedSpendingCap( +export async function assertChangedSpendingCap( driver: Driver, newSpendingCap: string, ) { diff --git a/test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts new file mode 100644 index 000000000000..ba97d9cda4cd --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { MockttpServer } from 'mockttp'; +import { WINDOW_TITLES } from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; +import { scrollAndConfirmAndAssertConfirm } from '../helpers'; +import { mocked4BytesApprove } from './erc20-approve-redesign.spec'; +import { + assertChangedSpendingCap, + editSpendingCap, +} from './increase-token-allowance-redesign.spec'; +import { openDAppWithContract, TestSuiteArguments } from './shared'; + +const { + defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + withFixtures, +} = require('../../../helpers'); +const FixtureBuilder = require('../../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); + +describe('Confirmation Redesign ERC20 Revoke Allowance', function () { + const smartContract = SMART_CONTRACTS.HST; + + describe('Submit an revoke transaction @no-mmi', function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + testSpecificMock: mocks, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createERC20ApproveTransaction(driver); + + const NEW_SPENDING_CAP = '0'; + await editSpendingCap(driver, NEW_SPENDING_CAP); + + await driver.waitForSelector({ + css: 'h2', + text: 'Remove permission', + }); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, NEW_SPENDING_CAP); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + testSpecificMock: mocks, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createERC20ApproveTransaction(driver); + + const NEW_SPENDING_CAP = '0'; + await editSpendingCap(driver, NEW_SPENDING_CAP); + + await driver.waitForSelector({ + css: 'h2', + text: 'Remove permission', + }); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, NEW_SPENDING_CAP); + }, + ); + }); + }); +}); + +async function mocks(server: MockttpServer) { + return [await mocked4BytesApprove(server)]; +} + +async function createERC20ApproveTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#approveTokens'); +} diff --git a/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap index 648ecff92c1a..9e7ff1b8db31 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/__snapshots__/approve.test.tsx.snap @@ -80,7 +80,7 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl" data-testid="simulation-token-value" > - 0 + 1000

@@ -414,7 +414,7 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - 0 + 1000

@@ -313,7 +308,7 @@ export function AlertModal({ customDetails, customAcknowledgeCheckbox, customAcknowledgeButton, - enableProvider = true, + showCloseIcon = true, }: AlertModalProps) { const { isAlertConfirmed, setAlertConfirmed, alerts } = useAlerts(ownerId); const { trackAlertRender } = useAlertMetrics(); @@ -348,13 +343,14 @@ export function AlertModal({ @@ -373,13 +369,6 @@ export function AlertModal({ onCheckboxClick={handleCheckboxClick} /> )} - {enableProvider ? ( - - ) : null} { mockStore, ); - expect(getByText('Your assets may be at risk')).toBeInTheDocument(); + expect(getByText('This request is suspicious')).toBeInTheDocument(); }); it('disables submit button when confirm modal is not acknowledged', () => { @@ -101,41 +101,37 @@ describe('ConfirmAlertModal', () => { expect(onSubmitMock).toHaveBeenCalledTimes(1); }); - // todo: following 2 tests have been temporarily commented out - // we can un-comment as we add more alert providers - - // it('calls open multiple alert modal when review alerts link is clicked', () => { - // const { getByTestId } = renderWithProvider( - // , - // mockStore, - // ); - - // fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts')); - // expect(getByTestId('alert-modal-button')).toBeInTheDocument(); - // }); - - // describe('when there are multiple alerts', () => { - // it('renders the next alert when the "Got it" button is clicked', () => { - // const mockStoreAcknowledgeAlerts = configureMockStore([])({ - // ...STATE_MOCK, - // confirmAlerts: { - // alerts: { [OWNER_ID_MOCK]: alertsMock }, - // confirmed: { - // [OWNER_ID_MOCK]: { - // [FROM_ALERT_KEY_MOCK]: true, - // [DATA_ALERT_KEY_MOCK]: false, - // }, - // }, - // }, - // }); - // const { getByTestId, getByText } = renderWithProvider( - // , - // mockStoreAcknowledgeAlerts, - // ); - // fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts')); - // fireEvent.click(getByTestId('alert-modal-button')); - - // expect(getByText(DATA_ALERT_MESSAGE_MOCK)).toBeInTheDocument(); - // }); - // }); + it('calls open multiple alert modal when review alerts link is clicked', () => { + const { getByTestId } = renderWithProvider( + , + mockStore, + ); + + fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts')); + expect(getByTestId('alert-modal-button')).toBeInTheDocument(); + }); + + describe('when there are multiple alerts', () => { + it('renders the next alert when the "Got it" button is clicked', () => { + const mockStoreAcknowledgeAlerts = configureMockStore([])({ + ...STATE_MOCK, + confirmAlerts: { + alerts: { [OWNER_ID_MOCK]: alertsMock }, + confirmed: { + [OWNER_ID_MOCK]: { + [FROM_ALERT_KEY_MOCK]: true, + [DATA_ALERT_KEY_MOCK]: false, + }, + }, + }, + }); + const { getByTestId, getByText } = renderWithProvider( + , + mockStoreAcknowledgeAlerts, + ); + fireEvent.click(getByTestId('alert-modal-button')); + + expect(getByText(DATA_ALERT_MESSAGE_MOCK)).toBeInTheDocument(); + }); + }); }); diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx index d46595e6b6be..96bcebab9953 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { SecurityProvider } from '../../../../../shared/constants/security-provider'; import { Box, Button, @@ -15,6 +14,7 @@ import { } from '../../../component-library'; import { AlignItems, + Severity, TextAlign, TextVariant, } from '../../../../helpers/constants/design-system'; @@ -87,7 +87,7 @@ function ConfirmDetails({ <> - {t('confirmAlertModalDetails')} + {t('confirmationAlertModalDetails')} (false); - // if there are multiple alerts, show the multiple alert modal + const hasDangerBlockingAlerts = fieldAlerts.some( + (alert) => alert.severity === Severity.Danger && alert.isBlocking, + ); + + // if there are unconfirmed danger alerts, show the multiple alert modal const [multipleAlertModalVisible, setMultipleAlertModalVisible] = - useState(unconfirmedDangerAlerts.length > 1); + useState(hasUnconfirmedFieldDangerAlerts); const handleCloseMultipleAlertModal = useCallback( (request?: { recursive?: boolean }) => { setMultipleAlertModalVisible(false); - if (request?.recursive) { + if ( + request?.recursive || + hasUnconfirmedFieldDangerAlerts || + hasDangerBlockingAlerts + ) { onClose(); } }, - [onClose], + [onClose, hasUnconfirmedFieldDangerAlerts, hasDangerBlockingAlerts], ); const handleOpenMultipleAlertModal = useCallback(() => { @@ -155,6 +164,7 @@ export function ConfirmAlertModal({ ownerId={ownerId} onFinalAcknowledgeClick={handleCloseMultipleAlertModal} onClose={handleCloseMultipleAlertModal} + showCloseIcon={false} /> ); } @@ -171,13 +181,9 @@ export function ConfirmAlertModal({ onAcknowledgeClick={onClose} alertKey={selectedAlert.key} onClose={onClose} - customTitle={t('confirmAlertModalTitle')} + customTitle={t('confirmationAlertModalTitle')} customDetails={ - selectedAlert.provider === SecurityProvider.Blockaid ? ( - SecurityProvider.Blockaid - ) : ( - - ) + } customAcknowledgeCheckbox={ } - enableProvider={false} /> ); } diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx index 0c3e810a5657..c4b79fb28b7c 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx @@ -70,6 +70,20 @@ describe('MultipleAlertModal', () => { onClose: onCloseMock, }; + const mockStoreAcknowledgeAlerts = configureMockStore([])({ + ...STATE_MOCK, + confirmAlerts: { + alerts: { [OWNER_ID_MOCK]: alertsMock }, + confirmed: { + [OWNER_ID_MOCK]: { + [FROM_ALERT_KEY_MOCK]: true, + [DATA_ALERT_KEY_MOCK]: true, + [CONTRACT_ALERT_KEY_MOCK]: false, + }, + }, + }, + }); + it('renders the multiple alert modal', () => { const { getByTestId } = renderWithProvider( , @@ -80,19 +94,6 @@ describe('MultipleAlertModal', () => { }); it('invokes the onFinalAcknowledgeClick when the button is clicked', () => { - const mockStoreAcknowledgeAlerts = configureMockStore([])({ - ...STATE_MOCK, - confirmAlerts: { - alerts: { [OWNER_ID_MOCK]: alertsMock }, - confirmed: { - [OWNER_ID_MOCK]: { - [FROM_ALERT_KEY_MOCK]: true, - [DATA_ALERT_KEY_MOCK]: true, - [CONTRACT_ALERT_KEY_MOCK]: true, - }, - }, - }, - }); const { getByTestId } = renderWithProvider( { }); it('render the next alert when the "Got it" button is clicked', () => { - const mockStoreAcknowledgeAlerts = configureMockStore([])({ - ...STATE_MOCK, - confirmAlerts: { - alerts: { [OWNER_ID_MOCK]: alertsMock }, - confirmed: { - [OWNER_ID_MOCK]: { - [FROM_ALERT_KEY_MOCK]: true, - [DATA_ALERT_KEY_MOCK]: true, - [CONTRACT_ALERT_KEY_MOCK]: false, - }, - }, - }, - }); const { getByTestId, getByText } = renderWithProvider( , mockStoreAcknowledgeAlerts, @@ -127,7 +115,23 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-button')); - expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + }); + + it('closes modal when the "Got it" button is clicked', () => { + onAcknowledgeClickMock.mockReset(); + const { getByTestId } = renderWithProvider( + , + mockStoreAcknowledgeAlerts, + ); + + fireEvent.click(getByTestId('alert-modal-button')); + + expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); }); describe('Navigation', () => { @@ -139,11 +143,14 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-next-button')); - expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); }); it('calls previous alert when the previous button is clicked', () => { - const selectSecondAlertMock = { ...defaultProps, alertKey: 'data' }; + const selectSecondAlertMock = { + ...defaultProps, + alertKey: CONTRACT_ALERT_KEY_MOCK, + }; const { getByTestId, getByText } = renderWithProvider( , mockStore, @@ -151,7 +158,7 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-back-button')); - expect(getByText(alertsMock[0].message)).toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); }); }); }); diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx index ae3e285efa00..d3b289343d00 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx @@ -30,6 +30,10 @@ export type MultipleAlertModalProps = { onClose: (request?: { recursive?: boolean }) => void; /** The unique identifier of the entity that owns the alert. */ ownerId: string; + /** Whether to show the close icon in the modal header. */ + showCloseIcon?: boolean; + /** Whether to skip the unconfirmed alerts validation and close the modal directly. */ + skipAlertNavigation?: boolean; }; function PreviousButton({ @@ -145,8 +149,10 @@ export function MultipleAlertModal({ onClose, onFinalAcknowledgeClick, ownerId, + showCloseIcon = true, + skipAlertNavigation = false, }: MultipleAlertModalProps) { - const { isAlertConfirmed, alerts } = useAlerts(ownerId); + const { isAlertConfirmed, fieldAlerts: alerts } = useAlerts(ownerId); const initialAlertIndex = alerts.findIndex( (alert: Alert) => alert.key === alertKey, @@ -173,6 +179,11 @@ export function MultipleAlertModal({ }, []); const handleAcknowledgeClick = useCallback(() => { + if (skipAlertNavigation) { + onFinalAcknowledgeClick(); + return; + } + if (selectedIndex + 1 === alerts.length) { if (!hasUnconfirmedAlerts) { onFinalAcknowledgeClick(); @@ -189,6 +200,7 @@ export function MultipleAlertModal({ selectedIndex, alerts.length, hasUnconfirmedAlerts, + skipAlertNavigation, ]); return ( @@ -205,6 +217,7 @@ export function MultipleAlertModal({ selectedIndex={selectedIndex} /> } + showCloseIcon={showCloseIcon} /> ); } diff --git a/ui/components/app/confirm/info/row/alert-row/alert-row.tsx b/ui/components/app/confirm/info/row/alert-row/alert-row.tsx index 532bd5987c7c..3956cc3095eb 100644 --- a/ui/components/app/confirm/info/row/alert-row/alert-row.tsx +++ b/ui/components/app/confirm/info/row/alert-row/alert-row.tsx @@ -85,6 +85,8 @@ export const ConfirmInfoAlertRow = ({ ownerId={ownerId} onFinalAcknowledgeClick={handleModalClose} onClose={handleModalClose} + showCloseIcon={false} + skipAlertNavigation={true} /> )} diff --git a/ui/components/app/confirm/info/row/row.tsx b/ui/components/app/confirm/info/row/row.tsx index e2b16b00e37d..5e74552e62e2 100644 --- a/ui/components/app/confirm/info/row/row.tsx +++ b/ui/components/app/confirm/info/row/row.tsx @@ -117,7 +117,7 @@ export const ConfirmInfoRow: React.FC = ({ {label} {labelChildren} - {tooltip && tooltip.length > 0 && ( + {!labelChildren && tooltip?.length && ( { const ownerId2Mock = '321'; const fromAlertKeyMock = 'from'; const dataAlertKeyMock = 'data'; + const toAlertKeyMock = 'to'; const alertsMock = [ { - key: fromAlertKeyMock, - field: fromAlertKeyMock, - severity: Severity.Danger as AlertSeverity, - message: 'Alert 1', + key: toAlertKeyMock, + field: toAlertKeyMock, + severity: Severity.Info as AlertSeverity, + message: 'Alert 3', }, { key: dataAlertKeyMock, severity: Severity.Warning as AlertSeverity, message: 'Alert 2', }, + { + key: fromAlertKeyMock, + field: fromAlertKeyMock, + severity: Severity.Danger as AlertSeverity, + message: 'Alert 1', + }, ]; const mockState = { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, [ownerId2Mock]: [alertsMock[0]] }, confirmed: { - [ownerIdMock]: { [fromAlertKeyMock]: true, [dataAlertKeyMock]: false }, + [ownerIdMock]: { + [fromAlertKeyMock]: true, + [dataAlertKeyMock]: false, + [toAlertKeyMock]: false, + }, [ownerId2Mock]: { [fromAlertKeyMock]: false }, }, }, @@ -54,6 +65,11 @@ describe('useAlerts', () => { expect(result.current.hasDangerAlerts).toEqual(true); expect(result.current.hasUnconfirmedDangerAlerts).toEqual(false); }); + + it('returns alerts ordered by severity', () => { + const orderedAlerts = result.current.alerts; + expect(orderedAlerts[0].severity).toEqual(Severity.Danger); + }); }); describe('unconfirmedDangerAlerts', () => { @@ -73,6 +89,77 @@ describe('useAlerts', () => { }); }); + describe('unconfirmedFieldDangerAlerts', () => { + it('returns all unconfirmed field danger alerts', () => { + const { result: result1 } = renderHookUseAlert(undefined, { + confirmAlerts: { + alerts: { + [ownerIdMock]: alertsMock, + [ownerId2Mock]: [alertsMock[0]], + }, + confirmed: { + [ownerIdMock]: { + [fromAlertKeyMock]: false, + [dataAlertKeyMock]: false, + [toAlertKeyMock]: false, + }, + [ownerId2Mock]: { [fromAlertKeyMock]: false }, + }, + }, + }); + const expectedFieldDangerAlert = alertsMock.find( + (alert) => + alert.field === fromAlertKeyMock && + alert.severity === Severity.Danger, + ); + expect(result1.current.unconfirmedFieldDangerAlerts).toEqual([ + expectedFieldDangerAlert, + ]); + }); + }); + + describe('hasUnconfirmedFieldDangerAlerts', () => { + it('returns true if there are unconfirmed field danger alerts', () => { + const { result: result1 } = renderHookUseAlert(undefined, { + confirmAlerts: { + alerts: { + [ownerIdMock]: alertsMock, + [ownerId2Mock]: [alertsMock[0]], + }, + confirmed: { + [ownerIdMock]: { + [fromAlertKeyMock]: false, + [dataAlertKeyMock]: false, + [toAlertKeyMock]: false, + }, + [ownerId2Mock]: { [fromAlertKeyMock]: false }, + }, + }, + }); + expect(result1.current.hasUnconfirmedFieldDangerAlerts).toEqual(true); + }); + + it('returns false if there are no unconfirmed field danger alerts', () => { + const { result: result1 } = renderHookUseAlert(undefined, { + confirmAlerts: { + alerts: { + [ownerIdMock]: alertsMock, + [ownerId2Mock]: [alertsMock[0]], + }, + confirmed: { + [ownerIdMock]: { + [fromAlertKeyMock]: true, + [dataAlertKeyMock]: false, + [toAlertKeyMock]: false, + }, + [ownerId2Mock]: { [fromAlertKeyMock]: false }, + }, + }, + }); + expect(result1.current.hasUnconfirmedFieldDangerAlerts).toEqual(false); + }); + }); + describe('generalAlerts', () => { it('returns general alerts', () => { const expectedGeneralAlerts = alertsMock.find( @@ -103,10 +190,10 @@ describe('useAlerts', () => { describe('fieldAlerts', () => { it('returns all alerts with field property', () => { - const expectedFieldAlerts = alertsMock.find( - (alert) => alert.field === fromAlertKeyMock, - ); - expect(result.current.fieldAlerts).toEqual([expectedFieldAlerts]); + expect(result.current.fieldAlerts).toEqual([ + alertsMock[0], + alertsMock[2], + ]); }); it('returns empty array if no alerts with field property', () => { diff --git a/ui/hooks/useAlerts.ts b/ui/hooks/useAlerts.ts index 92440822bbdd..06d79800f634 100644 --- a/ui/hooks/useAlerts.ts +++ b/ui/hooks/useAlerts.ts @@ -16,8 +16,8 @@ import { Severity } from '../helpers/constants/design-system'; const useAlerts = (ownerId: string) => { const dispatch = useDispatch(); - const alerts: Alert[] = useSelector((state) => - selectAlerts(state as AlertsState, ownerId), + const alerts: Alert[] = sortAlertsBySeverity( + useSelector((state) => selectAlerts(state as AlertsState, ownerId)), ); const confirmedAlertKeys = useSelector((state) => @@ -28,8 +28,8 @@ const useAlerts = (ownerId: string) => { selectGeneralAlerts(state as AlertsState, ownerId), ); - const fieldAlerts = useSelector((state) => - selectFieldAlerts(state as AlertsState, ownerId), + const fieldAlerts = sortAlertsBySeverity( + useSelector((state) => selectFieldAlerts(state as AlertsState, ownerId)), ); const getFieldAlerts = useCallback( @@ -61,12 +61,20 @@ const useAlerts = (ownerId: string) => { (alert) => !isAlertConfirmed(alert.key) && alert.severity === Severity.Danger, ); + const hasAlerts = alerts.length > 0; + const dangerAlerts = alerts.filter( (alert) => alert.severity === Severity.Danger, ); + const hasUnconfirmedDangerAlerts = unconfirmedDangerAlerts.length > 0; + const unconfirmedFieldDangerAlerts = fieldAlerts.filter( + (alert) => + !isAlertConfirmed(alert.key) && alert.severity === Severity.Danger, + ); + return { alerts, fieldAlerts, @@ -79,7 +87,21 @@ const useAlerts = (ownerId: string) => { isAlertConfirmed, setAlertConfirmed, unconfirmedDangerAlerts, + unconfirmedFieldDangerAlerts, + hasUnconfirmedFieldDangerAlerts: unconfirmedFieldDangerAlerts.length > 0, }; }; +function sortAlertsBySeverity(alerts: Alert[]): Alert[] { + const severityOrder = { + [Severity.Danger]: 3, + [Severity.Warning]: 2, + [Severity.Info]: 1, + }; + + return alerts.sort( + (a, b) => severityOrder[b.severity] - severityOrder[a.severity], + ); +} + export default useAlerts; diff --git a/ui/pages/confirmations/components/confirm/footer/footer.test.tsx b/ui/pages/confirmations/components/confirm/footer/footer.test.tsx index 79346a754a36..09d1fdf5753b 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.test.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.test.tsx @@ -342,15 +342,31 @@ describe('ConfirmFooter', () => { expect(getByText('Review alerts')).toBeDisabled(); }); - it('sets the alert modal visible when the review alerts button is clicked', () => { - const { getByTestId } = render(stateWithAlertsMock); - fireEvent.click(getByTestId('confirm-footer-button')); - expect(getByTestId('confirm-alert-modal-submit-button')).toBeDefined(); + it('renders the "review alert" button when there are unconfirmed alerts', () => { + const { getByText } = render(stateWithAlertsMock); + expect(getByText('Review alert')).toBeInTheDocument(); + }); + + it('renders the "confirm" button when there are confirmed danger alerts', () => { + const stateWithConfirmedDangerAlertMock = createStateWithAlerts( + alertsMock, + { + [KEY_ALERT_KEY_MOCK]: true, + }, + ); + const { getByText } = render(stateWithConfirmedDangerAlertMock); + expect(getByText('Confirm')).toBeInTheDocument(); }); it('renders the "confirm" button when there are no alerts', () => { const { getByText } = render(); expect(getByText('Confirm')).toBeInTheDocument(); }); + + it('sets the alert modal visible when the review alerts button is clicked', () => { + const { getByTestId } = render(stateWithAlertsMock); + fireEvent.click(getByTestId('confirm-footer-button')); + expect(getByTestId('alert-modal-button')).toBeDefined(); + }); }); }); diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index d40e72144612..cc9b39609030 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -39,6 +39,7 @@ import { import { useConfirmContext } from '../../../context/confirm'; import { getConfirmationSender } from '../utils'; import { MetaMetricsEventLocation } from '../../../../../../shared/constants/metametrics'; +import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; import { Severity } from '../../../../../helpers/constants/design-system'; export type OnCancelHandler = ({ @@ -47,6 +48,21 @@ export type OnCancelHandler = ({ location: MetaMetricsEventLocation; }) => void; +function reviewAlertButtonText( + unconfirmedDangerAlerts: Alert[], + t: ReturnType, +) { + if (unconfirmedDangerAlerts.length === 1) { + return t('reviewAlert'); + } + + if (unconfirmedDangerAlerts.length > 1) { + return t('reviewAlerts'); + } + + return t('confirm'); +} + function getButtonDisabledState( hasUnconfirmedDangerAlerts: boolean, hasBlockingAlerts: boolean, @@ -79,10 +95,15 @@ const ConfirmButton = ({ const [confirmModalVisible, setConfirmModalVisible] = useState(false); - const { dangerAlerts, hasDangerAlerts, hasUnconfirmedDangerAlerts } = - useAlerts(alertOwnerId); + const { + hasDangerAlerts, + hasUnconfirmedDangerAlerts, + fieldAlerts, + hasUnconfirmedFieldDangerAlerts, + unconfirmedFieldDangerAlerts, + } = useAlerts(alertOwnerId); - const hasDangerBlockingAlerts = dangerAlerts.some( + const hasDangerBlockingAlerts = fieldAlerts.some( (alert) => alert.severity === Severity.Danger && alert.isBlocking, ); @@ -116,9 +137,13 @@ const ConfirmButton = ({ )} onClick={handleOpenConfirmModal} size={ButtonSize.Lg} - startIconName={IconName.Danger} + startIconName={ + hasUnconfirmedFieldDangerAlerts + ? IconName.SecuritySearch + : IconName.Danger + } > - {dangerAlerts?.length > 0 ? t('reviewAlerts') : t('confirm')} + {reviewAlertButtonText(unconfirmedFieldDangerAlerts, t)} ) : ( +
+
+

+

+ + +
+

+
+ +
+
+

+ MetaMask isn’t connected to this site +

+

+ Select an account you want to use on this site to continue. +

+
+
+ + + + +`; diff --git a/ui/components/multichain/pages/review-permissions-page/index.js b/ui/components/multichain/pages/review-permissions-page/index.js new file mode 100644 index 000000000000..e2da178368f1 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/index.js @@ -0,0 +1,2 @@ +export { ReviewPermissions } from './review-permissions-page'; +export { SiteCell } from './site-cell/site-cell'; diff --git a/ui/components/multichain/pages/review-permissions-page/review-permission.types.tsx b/ui/components/multichain/pages/review-permissions-page/review-permission.types.tsx new file mode 100644 index 000000000000..6111dd8d946f --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/review-permission.types.tsx @@ -0,0 +1,36 @@ +import { type InternalAccount } from '@metamask/keyring-api'; + +// Define ConnectedSite interface +export type ConnectedSite = { + iconUrl: string; + name: string; + origin: string; + subjectType: string; + extensionId: string | null; + // Add other properties as needed +}; + +// Define ConnectedSites interface +export type ConnectedSites = { + [address: string]: ConnectedSite[]; // Index signature +}; + +// Define KeyringType interface +export type KeyringType = { + type: string; +}; + +// Define AccountType interface +export type AccountType = InternalAccount & { + name: string; + balance: string; + keyring: KeyringType; + label: string; +}; + +export type Subject = { + permissions: { parentCapability: string }[]; +}; +export type SubjectsType = { + [key: string]: Subject; +}; diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx new file mode 100644 index 000000000000..b2da4553ce50 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { ReviewPermissions } from '.'; + +export default { + title: 'Components/Multichain/ReviewPermissions', +}; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx new file mode 100644 index 000000000000..b644c16b6440 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../../test/jest/rendering'; +import mockState from '../../../../../test/data/mock-state.json'; +import configureStore from '../../../../store/store'; +import { ReviewPermissions } from '.'; + +const render = (state = {}) => { + const store = configureStore({ + ...mockState, + metamask: { + ...mockState.metamask, + ...state, + permissionHistory: { + 'https://test.dapp': { + eth_accounts: { + accounts: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1709225290848, + }, + }, + }, + }, + }, + activeTab: { + origin: 'https://test.dapp', + }, + }); + return renderWithProvider(, store); +}; +describe('ReviewPermissions', () => { + it('should render correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 000000000000..303d9dc2df4a --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx @@ -0,0 +1,281 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useParams } from 'react-router-dom'; +import { NonEmptyArray } from '@metamask/utils'; +import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { + BlockSize, + Display, + FlexDirection, +} from '../../../../helpers/constants/design-system'; +import { getURLHost } from '../../../../helpers/utils/util'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + getConnectedSitesList, + getInternalAccounts, + getNetworkConfigurationsByChainId, + getPermissionSubjects, + getPermittedAccountsForSelectedTab, + getPermittedChainsForSelectedTab, + getShowPermittedNetworkToastOpen, + getUpdatedAndSortedAccounts, +} from '../../../../selectors'; +import { + addPermittedAccounts, + addPermittedChains, + hidePermittedNetworkToast, + removePermissionsFor, + removePermittedAccount, + removePermittedChain, + requestAccountsAndChainPermissionsWithId, +} from '../../../../store/actions'; +import { + AvatarFavicon, + AvatarFaviconSize, + Box, + Button, + ButtonPrimary, + ButtonPrimarySize, + ButtonSize, + ButtonVariant, + IconName, +} from '../../../component-library'; +import { ToastContainer, Toast } from '../..'; +import { NoConnectionContent } from '../connections/components/no-connection'; +import { Content, Footer, Page } from '../page'; +import { SubjectsType } from '../connections/components/connections.types'; +import { CONNECT_ROUTE } from '../../../../helpers/constants/routes'; +import { + DisconnectAllModal, + DisconnectType, +} from '../../disconnect-all-modal/disconnect-all-modal'; +import { PermissionsHeader } from '../../permissions-header/permissions-header'; +import { mergeAccounts } from '../../account-list-menu/account-list-menu'; +import { MergedInternalAccount } from '../../../../selectors/selectors.types'; +import { TEST_CHAINS } from '../../../../../shared/constants/network'; +import { SiteCell } from '.'; + +export const ReviewPermissions = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const history = useHistory(); + const urlParams: { origin: string } = useParams(); + const securedOrigin = decodeURIComponent(urlParams.origin); + const [showAccountToast, setShowAccountToast] = useState(false); + const [showNetworkToast, setShowNetworkToast] = useState(false); + const [showDisconnectAllModal, setShowDisconnectAllModal] = useState(false); + const activeTabOrigin: string = securedOrigin; + + const showPermittedNetworkToastOpen = useSelector( + getShowPermittedNetworkToastOpen, + ); + + useEffect(() => { + if (showPermittedNetworkToastOpen) { + setShowNetworkToast(showPermittedNetworkToastOpen); + dispatch(hidePermittedNetworkToast()); + } + }, [showPermittedNetworkToastOpen]); + + const requestAccountsAndChainPermissions = async () => { + const requestId = await dispatch( + requestAccountsAndChainPermissionsWithId(activeTabOrigin), + ); + history.push(`${CONNECT_ROUTE}/${requestId}`); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const subjectMetadata: { [key: string]: any } = useSelector( + getConnectedSitesList, + ); + const connectedSubjectsMetadata = subjectMetadata[activeTabOrigin]; + const subjects = useSelector(getPermissionSubjects); + + const disconnectAllPermissions = () => { + const subject = (subjects as SubjectsType)[activeTabOrigin]; + + if (subject) { + const permissionMethodNames = Object.values(subject.permissions).map( + ({ parentCapability }: { parentCapability: string }) => + parentCapability, + ) as string[]; + if (permissionMethodNames.length > 0) { + const permissionsRecord = { + [activeTabOrigin]: permissionMethodNames as NonEmptyArray, + }; + + dispatch(removePermissionsFor(permissionsRecord)); + } + } + dispatch(hidePermittedNetworkToast()); + }; + + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const [nonTestNetworks, testNetworks] = useMemo( + () => + Object.entries(networkConfigurations).reduce( + ([nonTestNetworksList, testNetworksList], [chainId, network]) => { + const isTest = (TEST_CHAINS as string[]).includes(chainId); + (isTest ? testNetworksList : nonTestNetworksList).push(network); + return [nonTestNetworksList, testNetworksList]; + }, + [[] as NetworkConfiguration[], [] as NetworkConfiguration[]], + ), + [networkConfigurations], + ); + const connectedChainIds = useSelector((state) => + getPermittedChainsForSelectedTab(state, activeTabOrigin), + ) as string[]; + + const handleSelectChainIds = async (chainIds: string[]) => { + if (chainIds.length === 0) { + setShowDisconnectAllModal(true); + return; + } + + dispatch(addPermittedChains(activeTabOrigin, chainIds)); + + connectedChainIds.forEach((chainId: string) => { + if (!chainIds.includes(chainId)) { + dispatch(removePermittedChain(activeTabOrigin, chainId)); + } + }); + + setShowNetworkToast(true); + }; + + const accounts = useSelector(getUpdatedAndSortedAccounts); + const internalAccounts = useSelector(getInternalAccounts); + const mergedAccounts: MergedInternalAccount[] = useMemo(() => { + return mergeAccounts(accounts, internalAccounts).filter( + (account: InternalAccount) => isEvmAccountType(account.type), + ); + }, [accounts, internalAccounts]); + + const connectedAccountAddresses = useSelector((state) => + getPermittedAccountsForSelectedTab(state, activeTabOrigin), + ) as string[]; + + const handleSelectAccountAddresses = (addresses: string[]) => { + if (addresses.length === 0) { + setShowDisconnectAllModal(true); + return; + } + + dispatch(addPermittedAccounts(activeTabOrigin, addresses)); + + connectedAccountAddresses.forEach((address: string) => { + if (!addresses.includes(address)) { + dispatch(removePermittedAccount(activeTabOrigin, address)); + } + }); + + setShowAccountToast(true); + }; + + const hostName = getURLHost(securedOrigin); + + return ( + + <> + + + {connectedAccountAddresses.length > 0 ? ( + + ) : ( + + )} + {showDisconnectAllModal ? ( + setShowDisconnectAllModal(false)} + onClick={() => { + disconnectAllPermissions(); + setShowDisconnectAllModal(false); + }} + /> + ) : null} + +
+ <> + {connectedAccountAddresses.length > 0 ? ( + + {showAccountToast ? ( + + setShowAccountToast(false)} + startAdornment={ + + } + /> + + ) : null} + {showNetworkToast ? ( + + setShowNetworkToast(false)} + startAdornment={ + + } + /> + + ) : null} + + + ) : ( + + {t('connectAccounts')} + + )} + +
+ +
+ ); +}; diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap new file mode 100644 index 000000000000..5dc31c8e210a --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SiteCellConnectionListItem renders correctly with required props 1`] = ` +
+
+
+ +
+
+

+ Title +

+
+ + Unconnected Message + +
+ Content +
+
+
+ +
+
+`; diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-tooltip.test.js.snap b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-tooltip.test.js.snap new file mode 100644 index 000000000000..bafd3fea4948 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-tooltip.test.js.snap @@ -0,0 +1,241 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SiteCellTooltip should render correctly 1`] = ` +
+
Alerts"" + data-tooltipped="" + style="display: inline;" + > +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ Polygon logo +
+
+
+
+ Binance Smart Chain logo +
+
+
+
+ zkSync Era Mainnet logo +
+
+
+
+ Ethereum Mainnet logo +
+
+
+
+

+ +1 +

+
+
+
+
+`; diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js new file mode 100644 index 000000000000..85e50b0b0fed --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js @@ -0,0 +1,131 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + AlignItems, + BackgroundColor, + BlockSize, + Display, + FlexDirection, + IconColor, + TextAlign, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { + AvatarIcon, + AvatarIconSize, + Box, + ButtonIcon, + ButtonIconSize, + ButtonLink, + IconName, + Text, +} from '../../../../component-library'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; + +export const SiteCellConnectionListItem = ({ + title, + iconName, + connectedMessage, + unconnectedMessage, + isConnectFlow, + onClick, + content, +}) => { + const t = useI18nContext(); + + return ( + + + + + {title} + + + + {isConnectFlow ? unconnectedMessage : connectedMessage} + + {content} + + + {isConnectFlow ? ( + onClick()}>{t('edit')} + ) : ( + onClick()} + size={ButtonIconSize.Sm} + /> + )} + + ); +}; +SiteCellConnectionListItem.propTypes = { + /** + * Title that should be displayed in the connection list item + */ + title: PropTypes.string, + + /** + * The name of the icon that should be passed to the AvatarIcon component + */ + iconName: PropTypes.string, + + /** + * The message that should be displayed when there are connected accounts + */ + connectedMessage: PropTypes.string, + + /** + * The message that should be displayed when there are no connected accounts + */ + unconnectedMessage: PropTypes.string, + + /** + * If the component should show context related to adding a connection or editing one + */ + isConnectFlow: PropTypes.bool, + + /** + * Handler called when the edit button is clicked + */ + onClick: PropTypes.func, + + /** + * Components to display in the connection list item + */ + content: PropTypes.node, +}; diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js new file mode 100644 index 000000000000..613f07f348f3 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { IconName } from '../../../../component-library'; +import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; + +describe('SiteCellConnectionListItem', () => { + let getByTestId, container, getByText; + + const renderComponent = () => { + const rendered = render( + null} + content={
Content
} + />, + ); + getByTestId = rendered.getByTestId; + container = rendered.container; + getByText = rendered.getByText; + }; + + beforeEach(() => { + renderComponent(); + }); + + it('renders correctly with required props', () => { + expect(container).toMatchSnapshot(); + const siteCell = getByTestId('site-cell-connection-list-item'); + expect(siteCell).toBeDefined(); + }); + + it('returns wallet icon correctly', () => { + expect(getByText('Title')).toBeDefined(); + }); +}); diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-tooltip.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-tooltip.js new file mode 100644 index 000000000000..2e4eef35d594 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-tooltip.js @@ -0,0 +1,190 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Tooltip } from 'react-tippy'; +import { useSelector } from 'react-redux'; +import { + AlignItems, + BackgroundColor, + BorderStyle, + Display, + FlexDirection, + TextAlign, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { AvatarType } from '../../../avatar-group/avatar-group.types'; +import { AvatarGroup } from '../../..'; +import { + AvatarAccount, + AvatarAccountSize, + AvatarAccountVariant, + AvatarNetwork, + AvatarNetworkSize, + Box, + Text, +} from '../../../../component-library'; +import { getUseBlockie } from '../../../../../selectors'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../shared/constants/network'; + +export const SiteCellTooltip = ({ accounts, networks }) => { + const t = useI18nContext(); + const AVATAR_GROUP_LIMIT = 4; + const TOOLTIP_LIMIT = 4; + const useBlockie = useSelector(getUseBlockie); + const avatarAccountVariant = useBlockie + ? AvatarAccountVariant.Blockies + : AvatarAccountVariant.Jazzicon; + + const avatarAccountsData = accounts?.map((account) => ({ + avatarValue: account.address, + })); + + const avatarNetworksData = networks?.map((network) => ({ + avatarValue: CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[network.chainId], + symbol: network.name, + })); + + return ( + + + {accounts?.slice(0, TOOLTIP_LIMIT).map((acc) => { + return ( + + + + {acc.label || acc.metadata.name} + + + ); + })} + {networks?.slice(0, TOOLTIP_LIMIT).map((network) => { + return ( + + + + {network.name} + + + ); + })} + {accounts?.length > TOOLTIP_LIMIT || + networks?.length > TOOLTIP_LIMIT ? ( + + + {accounts?.length > 0 + ? t('moreAccounts', [accounts?.length - TOOLTIP_LIMIT]) + : t('moreNetworks', [networks.length - TOOLTIP_LIMIT])} + + + ) : null} + + + } + arrow + offset={0} + delay={50} + duration={0} + size="small" + title={t('alertDisableTooltip')} + trigger="mouseenter focus" + theme="dark" + tag="div" + > + {accounts?.length > 0 && ( + + )} + {networks?.length > 0 && ( + + )} + + ); +}; +SiteCellTooltip.propTypes = { + /** + * An array of account objects to be displayed in the tooltip. + * Each object should contain `address`, `label`, and `metadata.name`. + */ + accounts: PropTypes.arrayOf( + PropTypes.shape({ + address: PropTypes.string, // The unique address of the account. + label: PropTypes.string, // Optional label for the account. + metadata: PropTypes.shape({ + name: PropTypes.string, // Account's name from metadata. + }), + }), + ), + + /** + * An array of network objects to display in the tooltip. + */ + networks: PropTypes.arrayOf( + PropTypes.shape({ + chainId: PropTypes.string, // The unique chain ID of the network. + name: PropTypes.string, // The network's name. + }), + ), +}; diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-tooltip.test.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-tooltip.test.js new file mode 100644 index 000000000000..568e077ad0ed --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-tooltip.test.js @@ -0,0 +1,221 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../../../test/jest'; +import configureStore from '../../../../../store/store'; +import mockState from '../../../../../../test/data/mock-state.json'; +import { SiteCellTooltip } from './site-cell-tooltip'; + +describe('SiteCellTooltip', () => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const props = { + accounts: [ + { + id: 'e4a2f136-282d-4f06-8149-2e74e704a3fc', + address: '0x4dd158e8b382ba1649bda883a909037e1298552c', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 4', + nameLastUpdatedAt: 1727088231912, + importTime: 1727088231225, + lastSelected: 1727088231278, + keyring: { + type: 'HD Key Tree', + }, + }, + balance: '0x00', + pinned: false, + hidden: false, + active: false, + keyring: { + type: 'HD Key Tree', + }, + label: null, + }, + { + id: '96bb1385-2807-479a-a00e-af63e74119cd', + address: '0x86771cd233a04c004ceebc3c1ad402fe8a37ff32', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 5', + nameLastUpdatedAt: 1727099031302, + importTime: 1727099031101, + lastSelected: 1727099031109, + keyring: { + type: 'HD Key Tree', + }, + }, + balance: '0x00', + pinned: false, + hidden: false, + active: false, + keyring: { + type: 'HD Key Tree', + }, + label: null, + }, + { + id: '390013ea-34d9-4c58-a2d5-d98cd797aab8', + address: '0xf0b4efe81d9f277d05a9afeacbf076d86d9c041b', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 6', + importTime: 1727180391924, + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 1727180391971, + nameLastUpdatedAt: 1727180392652, + }, + balance: '0x00', + pinned: false, + hidden: false, + active: false, + keyring: { + type: 'HD Key Tree', + }, + label: null, + }, + ], + networks: [ + { + blockExplorerUrls: ['https://etherscan.io'], + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, + { + blockExplorerUrls: ['https://era.zksync.network/'], + chainId: '0x144', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'zkSync Era Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + name: 'ZKsync Era', + networkClientId: '9ceaf9eb-0aa2-4bd4-bf98-b390b91714d5', + type: 'custom', + url: 'https://mainnet.era.zksync.io', + }, + ], + }, + { + blockExplorerUrls: ['https://bscscan.com'], + chainId: '0x38', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Binance Smart Chain', + nativeCurrency: 'BNB', + rpcEndpoints: [ + { + name: 'BNB Smart Chain', + networkClientId: 'f1b61a9b-2238-4344-af5e-36d20f76de10', + type: 'custom', + url: 'https://bsc-dataseed.binance.org/', + }, + ], + }, + { + blockExplorerUrls: ['https://polygonscan.com/'], + chainId: '0x89', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Polygon', + nativeCurrency: 'POL', + rpcEndpoints: [ + { + name: 'Polygon Mainnet', + networkClientId: 'cf19f0de-8a83-468c-ad97-49b855a2ca9e', + type: 'custom', + url: 'https://polygon-mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, + { + blockExplorerUrls: ['https://lineascan.build'], + chainId: '0xe708', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Linea Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'linea-mainnet', + type: 'infura', + url: 'https://linea-mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, + ], + }; + + it('should render correctly', () => { + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Avatar Account correctly', () => { + const { container } = renderWithProvider( + , + store, + ); + + expect( + container.getElementsByClassName('mm-avatar-account__jazzicon'), + ).toBeDefined(); + }); + + it('should render Avatar Networks correctly', () => { + const { container } = renderWithProvider( + , + store, + ); + + expect( + container.getElementsByClassName('multichain-avatar-group'), + ).toBeDefined(); + }); +}); diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.stories.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.stories.tsx new file mode 100644 index 000000000000..7ca949ff9c02 --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.stories.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { SiteCell } from './site-cell'; + +export default { + title: 'Components/Multichain/SiteCell', + component: SiteCell, + argTypes: { + accounts: { control: 'array' }, + nonTestNetworks: { control: 'array' }, + testNetworks: { control: 'array' }, + }, + args: { + accounts: [ + { + id: '689821df-0e8f-4093-bbbb-b95cf0fa79cb', + address: '0x860092756917d3e069926ba130099375eeeb9440', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 1', + importTime: 1726046726882, + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 1726046726882, + }, + balance: '0x00', + }, + ], + selectedAccountAddresses: ['0x860092756917d3e069926ba130099375eeeb9440'], + selectedChainIds: ['0x1', '0xe708', '0x144', '0x89', '0x38'], + activeTabOrigin: 'https://app.uniswap.org', + nonTestNetworks: [ + { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + }, + ], + testNetworks: [ + { + chainId: '0xaa36a7', + rpcEndpoints: [ + { + networkClientId: 'sepolia', + url: 'https://sepolia.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://sepolia.etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Sepolia', + nativeCurrency: 'SepoliaETH', + }, + { + chainId: '0xe705', + rpcEndpoints: [ + { + networkClientId: 'linea-sepolia', + url: 'https://linea-sepolia.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://sepolia.lineascan.build'], + defaultBlockExplorerUrlIndex: 0, + name: 'Linea Sepolia', + nativeCurrency: 'LineaETH', + }, + ], + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; 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 new file mode 100644 index 000000000000..2ed1fce8fddd --- /dev/null +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { Hex } from '@metamask/utils'; +import { BorderColor } from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { + AvatarAccount, + AvatarAccountSize, + IconName, +} from '../../../../component-library'; +import { EditAccountsModal, EditNetworksModal } from '../../..'; +import { MergedInternalAccount } from '../../../../../selectors/selectors.types'; +import { SiteCellTooltip } from './site-cell-tooltip'; +import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; + +// Define types for networks, accounts, and other props +type Network = { + name: string; + chainId: string; +}; + +type SiteCellProps = { + nonTestNetworks: Network[]; + testNetworks: Network[]; + accounts: MergedInternalAccount[]; + onSelectAccountAddresses: (addresses: string[]) => void; + onSelectChainIds: (chainIds: Hex[]) => void; + selectedAccountAddresses: string[]; + selectedChainIds: string[]; + activeTabOrigin: string; + isConnectFlow?: boolean; +}; + +export const SiteCell: React.FC = ({ + nonTestNetworks, + testNetworks, + accounts, + onSelectAccountAddresses, + onSelectChainIds, + selectedAccountAddresses, + selectedChainIds, + activeTabOrigin, + isConnectFlow, +}) => { + const t = useI18nContext(); + + const allNetworks = [...nonTestNetworks, ...testNetworks]; + + const [showEditAccountsModal, setShowEditAccountsModal] = useState(false); + const [showEditNetworksModal, setShowEditNetworksModal] = useState(false); + + const selectedAccounts = accounts.filter(({ address }) => + selectedAccountAddresses.includes(address), + ); + const selectedNetworks = allNetworks.filter(({ chainId }) => + selectedChainIds.includes(chainId), + ); + + // Determine the messages for connected and not connected states + const accountMessageConnectedState = + selectedAccounts.length === 1 + ? t('connectedWithAccount', [ + selectedAccounts[0].label || selectedAccounts[0].metadata.name, + ]) + : t('connectedWith'); + const accountMessageNotConnectedState = + selectedAccounts.length === 1 + ? t('requestingForAccount', [ + selectedAccounts[0].label || selectedAccounts[0].metadata.name, + ]) + : t('requestingFor'); + + return ( + <> + setShowEditAccountsModal(true)} + content={ + // Why this difference? + selectedAccounts.length === 1 ? ( + + ) : ( + + ) + } + /> + setShowEditNetworksModal(true)} + content={} + /> + + {showEditAccountsModal && ( + setShowEditAccountsModal(false)} + onSubmit={onSelectAccountAddresses} + /> + )} + + {showEditNetworksModal && ( + setShowEditNetworksModal(false)} + onSubmit={onSelectChainIds} + /> + )} + + ); +}; diff --git a/ui/components/multichain/permissions-header/permissions-header.tsx b/ui/components/multichain/permissions-header/permissions-header.tsx new file mode 100644 index 000000000000..9ee7bec7a52c --- /dev/null +++ b/ui/components/multichain/permissions-header/permissions-header.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { + AlignItems, + BackgroundColor, + Display, + IconColor, + JustifyContent, + TextAlign, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { + AvatarFavicon, + AvatarFaviconSize, + Box, + ButtonIcon, + ButtonIconSize, + Icon, + IconName, + IconSize, + Text, +} from '../../component-library'; +import { Header } from '../pages/page'; +import { getURLHost } from '../../../helpers/utils/util'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const PermissionsHeader = ({ + securedOrigin, + connectedSubjectsMetadata, +}: { + securedOrigin: string; + connectedSubjectsMetadata?: { name: string; iconUrl: string }; +}) => { + const t = useI18nContext(); + const history = useHistory(); + + return ( +
(history as any).goBack()} + size={ButtonIconSize.Sm} + /> + } + > + + {connectedSubjectsMetadata?.iconUrl ? ( + + ) : ( + + )} + + {getURLHost(securedOrigin)} + + +
+ ); +}; diff --git a/ui/components/ui/account-list/account-list.js b/ui/components/ui/account-list/account-list.js index 18fc35b2c6ce..13afac6c08f2 100644 --- a/ui/components/ui/account-list/account-list.js +++ b/ui/components/ui/account-list/account-list.js @@ -56,15 +56,8 @@ const AccountList = ({ }; const Header = () => { - let checked = false; - let isIndeterminate = false; - if (allAreSelected()) { - checked = true; - } else if (selectedAccounts.size === 0) { - checked = false; - } else { - isIndeterminate = true; - } + const checked = allAreSelected(); + const isIndeterminate = !checked && selectedAccounts.size !== 0; return (
+
+
+
+
+

+

+ 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;" + > +
+
+
+
+ G +
+
+
+
+ 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 new file mode 100644 index 000000000000..9440d5031334 --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; +import { ConnectPage, ConnectPageRequest } from './connect-page'; + +const render = ( + props: { + request: ConnectPageRequest; + permissionsRequestId: string; + rejectPermissionsRequest: (id: string) => void; + approveConnection: (request: ConnectPageRequest) => void; + activeTabOrigin: string; + } = { + request: { + id: '1', + origin: 'https://test.dapp', + }, + permissionsRequestId: '1', + rejectPermissionsRequest: jest.fn(), + approveConnection: jest.fn(), + activeTabOrigin: 'https://test.dapp', + }, + state = {}, +) => { + const store = configureStore({ + ...mockState, + metamask: { + ...mockState.metamask, + ...state, + permissionHistory: { + 'https://test.dapp': { + eth_accounts: { + accounts: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1709225290848, + }, + }, + }, + }, + }, + activeTab: { + origin: 'https://test.dapp', + }, + }); + return renderWithProvider(, store); +}; +describe('ConnectPage', () => { + it('should render correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should render title correctly', () => { + const { getByText } = render(); + expect(getByText('Connect with MetaMask')).toBeDefined(); + }); + + it('should render account connectionListItem', () => { + const { getByText } = render(); + expect( + getByText('See your accounts and suggest transactions'), + ).toBeDefined(); + }); + + it('should render network connectionListItem', () => { + const { getByText } = render(); + expect(getByText('Use your enabled networks')).toBeDefined(); + }); + + it('should render confirm and cancel button', () => { + const { getByText } = render(); + const confirmButton = getByText('Confirm'); + const cancelButton = getByText('Cancel'); + expect(confirmButton).toBeDefined(); + expect(cancelButton).toBeDefined(); + }); +}); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx new file mode 100644 index 000000000000..f332ba6cc07e --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -0,0 +1,147 @@ +import React, { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + getInternalAccounts, + getNetworkConfigurationsByChainId, + getSelectedInternalAccount, + getUpdatedAndSortedAccounts, +} from '../../../selectors'; +import { + Box, + Button, + ButtonSize, + ButtonVariant, + Text, +} from '../../../components/component-library'; +import { + Content, + Footer, + Header, + Page, +} from '../../../components/multichain/pages/page'; +import { SiteCell } from '../../../components/multichain/pages/review-permissions-page'; +import { + BlockSize, + Display, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { MergedInternalAccount } from '../../../selectors/selectors.types'; +import { mergeAccounts } from '../../../components/multichain/account-list-menu/account-list-menu'; +import { TEST_CHAINS } from '../../../../shared/constants/network'; + +export type ConnectPageRequest = { + id: string; + origin: string; +}; + +type ConnectPageProps = { + request: ConnectPageRequest; + permissionsRequestId: string; + rejectPermissionsRequest: (id: string) => void; + approveConnection: (request: ConnectPageRequest) => void; + activeTabOrigin: string; +}; + +export const ConnectPage: React.FC = ({ + request, + permissionsRequestId, + rejectPermissionsRequest, + approveConnection, + activeTabOrigin, +}) => { + const t = useI18nContext(); + + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const [nonTestNetworks, testNetworks] = useMemo( + () => + Object.entries(networkConfigurations).reduce( + ([nonTestNetworksList, testNetworksList], [chainId, network]) => { + const isTest = (TEST_CHAINS as string[]).includes(chainId); + (isTest ? testNetworksList : nonTestNetworksList).push(network); + return [nonTestNetworksList, testNetworksList]; + }, + [[] as NetworkConfiguration[], [] as NetworkConfiguration[]], + ), + [networkConfigurations], + ); + const defaultSelectedChainIds = nonTestNetworks.map(({ chainId }) => chainId); + const [selectedChainIds, setSelectedChainIds] = useState( + defaultSelectedChainIds, + ); + + const accounts = useSelector(getUpdatedAndSortedAccounts); + const internalAccounts = useSelector(getInternalAccounts); + const mergedAccounts: MergedInternalAccount[] = useMemo(() => { + return mergeAccounts(accounts, internalAccounts).filter( + (account: InternalAccount) => isEvmAccountType(account.type), + ); + }, [accounts, internalAccounts]); + + const currentAccount = useSelector(getSelectedInternalAccount); + const defaultAccountsAddresses = [currentAccount?.address]; + const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( + defaultAccountsAddresses, + ); + + const onConfirm = () => { + const _request = { + ...request, + approvedAccounts: selectedAccountAddresses, + approvedChainIds: selectedChainIds, + }; + approveConnection(_request); + }; + + return ( + +
+ {t('connectWithMetaMask')} + {t('connectionDescription')}: +
+ + + +
+ + + + +
+
+ ); +}; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index e5adf45a43fe..403c431330b1 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -25,6 +25,7 @@ import SnapsConnect from './snaps/snaps-connect'; import SnapInstall from './snaps/snap-install'; import SnapUpdate from './snaps/snap-update'; import SnapResult from './snaps/snap-result'; +import { ConnectPage } from './connect-page/connect-page'; const APPROVE_TIMEOUT = MILLISECOND * 1200; @@ -147,6 +148,9 @@ export default class PermissionConnect extends Component { history.replace(DEFAULT_ROUTE); return; } + if (process.env.CHAIN_PERMISSIONS) { + history.replace(confirmPermissionPath); + } // if this is an incremental permission request for permitted chains, skip the account selection if ( permissionsRequest?.diff?.permissionDiffMap?.[ @@ -155,7 +159,6 @@ export default class PermissionConnect extends Component { ) { history.replace(confirmPermissionPath); } - if (history.location.pathname === connectPath && !isRequestingAccounts) { switch (requestType) { case 'wallet_installSnap': @@ -292,9 +295,14 @@ export default class PermissionConnect extends Component { ); } + approveConnection = (...args) => { + const { approvePermissionsRequest } = this.props; + approvePermissionsRequest(...args); + this.redirect(true); + }; + render() { const { - approvePermissionsRequest, accounts, showNewAccountModal, newAccountNumber, @@ -314,6 +322,7 @@ export default class PermissionConnect extends Component { approvePendingApproval, rejectPendingApproval, setSnapsInstallPrivacyWarningShownStatus, + approvePermissionsRequest, } = this.props; const { selectedAccountAddresses, @@ -357,30 +366,42 @@ export default class PermissionConnect extends Component { ( - { - approvePermissionsRequest(...args); - this.redirect(true); - }} - rejectPermissionsRequest={(requestId) => - this.cancelPermissionsRequest(requestId) - } - selectedAccounts={accounts.filter((account) => - selectedAccountAddresses.has(account.address), - )} - targetSubjectMetadata={targetSubjectMetadata} - history={this.props.history} - connectPath={connectPath} - snapsInstallPrivacyWarningShown={ - snapsInstallPrivacyWarningShown - } - setSnapsInstallPrivacyWarningShownStatus={ - setSnapsInstallPrivacyWarningShownStatus - } - /> - )} + render={() => + process.env.CHAIN_PERMISSIONS && !permissionsRequest?.diff ? ( + + this.cancelPermissionsRequest(requestId) + } + activeTabOrigin={this.state.origin} + request={permissionsRequest} + permissionsRequestId={permissionsRequestId} + approveConnection={this.approveConnection} + /> + ) : ( + { + approvePermissionsRequest(...args); + this.redirect(true); + }} + rejectPermissionsRequest={(requestId) => + this.cancelPermissionsRequest(requestId) + } + selectedAccounts={accounts.filter((account) => + selectedAccountAddresses.has(account.address), + )} + targetSubjectMetadata={targetSubjectMetadata} + history={this.props.history} + connectPath={connectPath} + snapsInstallPrivacyWarningShown={ + snapsInstallPrivacyWarningShown + } + setSnapsInstallPrivacyWarningShownStatus={ + setSnapsInstallPrivacyWarningShownStatus + } + /> + ) + } /> ( { - approvePermissionsRequest(...args); - this.redirect(true); - }} + approveConnection={this.approveConnection} rejectConnection={(requestId) => this.cancelPermissionsRequest(requestId) } diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 5c91d49c5266..a02ecfa32ef9 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -11,6 +11,7 @@ import Home from '../home'; import { PermissionsPage, Connections, + ReviewPermissions, } from '../../components/multichain/pages'; import Settings from '../settings'; import Authenticated from '../../helpers/higher-order-components/authenticated'; @@ -77,6 +78,7 @@ import { TOKEN_DETAILS, CONNECTIONS, PERMISSIONS, + REVIEW_PERMISSIONS, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) INSTITUTIONAL_FEATURES_DONE_ROUTE, CUSTODY_ACCOUNT_DONE_ROUTE, @@ -189,6 +191,8 @@ export default class Routes extends Component { accountDetailsAddress: PropTypes.string, isImportNftsModalOpen: PropTypes.bool.isRequired, hideImportNftsModal: PropTypes.func.isRequired, + isPermittedNetworkToastOpen: PropTypes.bool.isRequired, + hidePermittedNetworkToast: PropTypes.func.isRequired, isIpfsModalOpen: PropTypes.bool.isRequired, isBasicConfigurationModalOpen: PropTypes.bool.isRequired, hideIpfsModal: PropTypes.func.isRequired, @@ -199,6 +203,7 @@ export default class Routes extends Component { addPermittedAccount: PropTypes.func.isRequired, switchedNetworkDetails: PropTypes.object, useNftDetection: PropTypes.bool, + currentNetwork: PropTypes.object, showNftEnablementToast: PropTypes.bool, setHideNftEnablementToast: PropTypes.func.isRequired, clearSwitchedNetworkDetails: PropTypes.func.isRequired, @@ -439,6 +444,11 @@ export default class Routes extends Component { component={Connections} /> + ); @@ -635,14 +645,16 @@ export default class Routes extends Component { useNftDetection, showNftEnablementToast, setHideNftEnablementToast, + isPermittedNetworkToastOpen, + currentNetwork, } = this.props; const showAutoNetworkSwitchToast = this.getShowAutoNetworkSwitchTest(); const isPrivacyToastRecent = this.getIsPrivacyToastRecent(); const isPrivacyToastNotShown = !newPrivacyPolicyToastShownDate; const isEvmAccount = isEvmAccountType(account?.type); - const autoHideToastDelay = 5 * SECOND; + const safeEncodedHost = encodeURIComponent(activeTabOrigin); const onAutoHideToast = () => { setHideNftEnablementToast(false); @@ -735,7 +747,7 @@ export default class Routes extends Component { } @@ -761,6 +773,32 @@ export default class Routes extends Component { onAutoHideToast={onAutoHideToast} /> ) : null} + + {process.env.CHAIN_PERMISSIONS && isPermittedNetworkToastOpen ? ( + + } + text={this.context.t('permittedChainToastUpdate', [ + getURLHost(activeTabOrigin), + currentNetwork?.nickname, + ])} + actionText={this.context.t('editPermissions')} + onActionClick={() => { + this.props.hidePermittedNetworkToast(); + this.props.history.push( + `${REVIEW_PERMISSIONS}/${safeEncodedHost}`, + ); + }} + onClose={() => this.props.hidePermittedNetworkToast()} + /> + ) : null} ); } @@ -929,6 +967,7 @@ export default class Routes extends Component { {isImportNftsModalOpen ? ( hideImportNftsModal()} /> ) : null} + {isIpfsModalOpen ? ( hideIpfsModal()} /> ) : null} diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index 856aa8b53ade..419daf561778 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -28,6 +28,7 @@ import { getUseRequestQueue, getUseNftDetection, getNftDetectionEnablementToast, + getCurrentNetwork, } from '../../selectors'; import { getSmartTransactionsOptInStatus } from '../../../shared/modules/selectors'; import { @@ -52,6 +53,7 @@ import { hideKeyringRemovalResultModal, ///: END:ONLY_INCLUDE_IF setEditedNetwork, + hidePermittedNetworkToast, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -77,6 +79,7 @@ function mapStateToProps(state) { const account = getSelectedAccount(state); const activeTabOrigin = activeTab?.origin; const connectedAccounts = getPermittedAccountsForCurrentTab(state); + const currentNetwork = getCurrentNetwork(state); const showConnectAccountToast = Boolean( allowShowAccountSetting && account && @@ -129,10 +132,12 @@ function mapStateToProps(state) { accountDetailsAddress: state.appState.accountDetailsAddress, isImportNftsModalOpen: state.appState.importNftsModal.open, isIpfsModalOpen: state.appState.showIpfsModalOpen, + isPermittedNetworkToastOpen: state.appState.showPermittedNetworkToastOpen, switchedNetworkDetails, useNftDetection, showNftEnablementToast, networkToAutomaticallySwitchTo, + currentNetwork, totalUnapprovedConfirmationCount: getNumberOfAllUnapprovedTransactionsAndMessages(state), neverShowSwitchedNetworkMessage: getNeverShowSwitchedNetworkMessage(state), @@ -160,6 +165,7 @@ function mapDispatchToProps(dispatch) { toggleNetworkMenu: () => dispatch(toggleNetworkMenu()), hideImportNftsModal: () => dispatch(hideImportNftsModal()), hideIpfsModal: () => dispatch(hideIpfsModal()), + hidePermittedNetworkToast: () => dispatch(hidePermittedNetworkToast()), hideImportTokensModal: () => dispatch(hideImportTokensModal()), hideDeprecatedNetworkModal: () => dispatch(hideDeprecatedNetworkModal()), addPermittedAccount: (activeTabOrigin, address) => diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index 65f2acf37c4b..fb32d41c9b17 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -2,6 +2,8 @@ import { ApprovalType } from '@metamask/controller-utils'; import { WALLET_SNAP_PERMISSION_KEY } from '@metamask/snaps-rpc-methods'; import { isEvmAccountType } from '@metamask/keyring-api'; import { CaveatTypes } from '../../shared/constants/permissions'; +// eslint-disable-next-line import/no-restricted-paths +import { PermissionNames } from '../../app/scripts/controllers/permissions'; import { getApprovalRequestsByType } from './approvals'; import { createDeepEqualSelector } from './util'; import { @@ -60,6 +62,12 @@ export function getPermittedAccounts(state, origin) { ); } +export function getPermittedChains(state, origin) { + return getChainsFromPermission( + getChainsPermissionFromSubject(subjectSelector(state, origin)), + ); +} + /** * Selects the permitted accounts from the eth_accounts permission for the * origin of the current tab. @@ -75,6 +83,14 @@ export function getPermittedAccountsForSelectedTab(state, activeTab) { return getPermittedAccounts(state, activeTab); } +export function getPermittedChainsForCurrentTab(state) { + return getPermittedAccounts(state, getOriginOfCurrentTab(state)); +} + +export function getPermittedChainsForSelectedTab(state, activeTab) { + return getPermittedChains(state, activeTab); +} + /** * Returns a map of permitted accounts by origin for all origins. * @@ -92,6 +108,17 @@ export function getPermittedAccountsByOrigin(state) { }, {}); } +export function getPermittedChainsByOrigin(state) { + const subjects = getPermissionSubjects(state); + return Object.keys(subjects).reduce((acc, subjectKey) => { + const chains = getChainsFromSubject(subjects[subjectKey]); + if (chains.length > 0) { + acc[subjectKey] = chains; + } + return acc; + }, {}); +} + export function getSubjectMetadata(state) { return state.metamask.subjectMetadata; } @@ -256,6 +283,14 @@ function getAccountsPermissionFromSubject(subject = {}) { return subject.permissions?.eth_accounts || {}; } +function getChainsFromSubject(subject) { + return getChainsFromPermission(getChainsPermissionFromSubject(subject)); +} + +function getChainsPermissionFromSubject(subject = {}) { + return subject.permissions?.[PermissionNames.permittedChains] || {}; +} + function getAccountsFromPermission(accountsPermission) { const accountsCaveat = getAccountsCaveatFromPermission(accountsPermission); return accountsCaveat && Array.isArray(accountsCaveat.value) @@ -263,6 +298,22 @@ function getAccountsFromPermission(accountsPermission) { : []; } +function getChainsFromPermission(chainsPermission) { + const chainsCaveat = getChainsCaveatFromPermission(chainsPermission); + return chainsCaveat && Array.isArray(chainsCaveat.value) + ? chainsCaveat.value + : []; +} + +function getChainsCaveatFromPermission(chainsPermission = {}) { + return ( + Array.isArray(chainsPermission.caveats) && + chainsPermission.caveats.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + ) + ); +} + function getAccountsCaveatFromPermission(accountsPermission = {}) { return ( Array.isArray(accountsPermission.caveats) && diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index bdc1547b7246..31c262df62c4 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1307,6 +1307,10 @@ export function getShowWhatsNewPopup(state) { return state.appState.showWhatsNewPopup; } +export function getShowPermittedNetworkToastOpen(state) { + return state.appState.showPermittedNetworkToastOpen; +} + /** * Returns a memoized selector that gets the internal accounts from the Redux store. * diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index a54a5a220be8..074568cfbf1d 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -14,6 +14,10 @@ export const NETWORK_DROPDOWN_CLOSE = 'UI_NETWORK_DROPDOWN_CLOSE'; export const IMPORT_NFTS_MODAL_OPEN = 'UI_IMPORT_NFTS_MODAL_OPEN'; export const IMPORT_NFTS_MODAL_CLOSE = 'UI_IMPORT_NFTS_MODAL_CLOSE'; export const SHOW_IPFS_MODAL_OPEN = 'UI_IPFS_MODAL_OPEN'; +export const SHOW_PERMITTED_NETWORK_TOAST_OPEN = + 'UI_PERMITTED_NETWORK_TOAST_OPEN'; +export const SHOW_PERMITTED_NETWORK_TOAST_CLOSE = + 'UI_PERMITTED_NETWORK_TOAST_CLOSE'; export const SHOW_IPFS_MODAL_CLOSE = 'UI_IPFS_MODAL_CLOSE'; export const IMPORT_TOKENS_POPOVER_OPEN = 'UI_IMPORT_TOKENS_POPOVER_OPEN'; export const IMPORT_TOKENS_POPOVER_CLOSE = 'UI_IMPORT_TOKENS_POPOVER_CLOSE'; @@ -78,6 +82,10 @@ export const SHOW_NFT_DETECTION_ENABLEMENT_TOAST = export const TOGGLE_ACCOUNT_MENU = 'TOGGLE_ACCOUNT_MENU'; export const TOGGLE_NETWORK_MENU = 'TOGGLE_NETWORK_MENU'; +export const SET_SELECTED_ACCOUNTS_FOR_DAPP_CONNECTIONS = + 'SET_SELECTED_ACCOUNTS_FOR_DAPP_CONNECTIONS'; +export const SET_SELECTED_NETWORKS_FOR_DAPP_CONNECTIONS = + 'SET_SELECTED_NETWORKS_FOR_DAPP_CONNECTIONS'; // deprecated network modal export const DEPRECATED_NETWORK_POPOVER_OPEN = diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 68c887c82a82..a136287f039c 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -17,6 +17,10 @@ import { MetaMetricsNetworkEventSource } from '../../shared/constants/metametric import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../../test/stub/networks'; import { CHAIN_IDS } from '../../shared/constants/network'; +import { + CaveatTypes, + EndowmentTypes, +} from '../../shared/constants/permissions'; import * as actions from './actions'; import * as actionConstants from './actionConstants'; import { setBackgroundConnection } from './background-connection'; @@ -77,6 +81,10 @@ describe('Actions', () => { background.abortTransactionSigning = sinon.stub(); background.toggleExternalServices = sinon.stub(); background.getStatePatches = sinon.stub().callsFake((cb) => cb(null, [])); + background.removePermittedChain = sinon.stub(); + background.requestAccountsAndChainPermissionsWithId = sinon.stub(); + background.grantPermissions = sinon.stub(); + background.grantPermissionsIncremental = sinon.stub(); }); describe('#tryUnlockMetamask', () => { @@ -2530,4 +2538,124 @@ describe('Actions', () => { ); }); }); + + describe('removePermittedChain', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls removePermittedChain in the background', async () => { + const store = mockStore(); + + background.removePermittedChain.callsFake((_, __, cb) => cb()); + setBackgroundConnection(background); + + await store.dispatch(actions.removePermittedChain('test.com', '0x1')); + + expect( + background.removePermittedChain.calledWith( + 'test.com', + '0x1', + sinon.match.func, + ), + ).toBe(true); + expect(store.getActions()).toStrictEqual([]); + }); + }); + + describe('requestAccountsAndChainPermissionsWithId', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls requestAccountsAndChainPermissionsWithId in the background', async () => { + const store = mockStore(); + + background.requestAccountsAndChainPermissionsWithId.callsFake((_, cb) => + cb(), + ); + setBackgroundConnection(background); + + await store.dispatch( + actions.requestAccountsAndChainPermissionsWithId('test.com'), + ); + + expect( + background.requestAccountsAndChainPermissionsWithId.calledWith( + 'test.com', + sinon.match.func, + ), + ).toBe(true); + expect(store.getActions()).toStrictEqual([]); + }); + }); + + describe('grantPermittedChain', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls grantPermissionsIncremental in the background', async () => { + const store = mockStore(); + + background.grantPermissionsIncremental.callsFake((_, cb) => cb()); + setBackgroundConnection(background); + + await actions.grantPermittedChain('test.com', '0x1'); + expect( + background.grantPermissionsIncremental.calledWith( + { + subject: { origin: 'test.com' }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + sinon.match.func, + ), + ).toBe(true); + expect(store.getActions()).toStrictEqual([]); + }); + }); + + describe('grantPermittedChains', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls grantPermissions in the background', async () => { + const store = mockStore(); + + background.grantPermissions.callsFake((_, cb) => cb()); + setBackgroundConnection(background); + + await actions.grantPermittedChains('test.com', ['0x1', '0x2']); + expect( + background.grantPermissions.calledWith( + { + subject: { origin: 'test.com' }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1', '0x2'], + }, + ], + }, + }, + }, + sinon.match.func, + ), + ).toBe(true); + + expect(store.getActions()).toStrictEqual([]); + }); + }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index db23a2e5e7a2..c4bed2665a6b 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -119,6 +119,10 @@ import { getMethodDataAsync } from '../../shared/lib/four-byte'; import { DecodedTransactionDataResponse } from '../../shared/types/transaction-decode'; import { LastInteractedConfirmationInfo } from '../pages/confirmations/types/confirm'; import { EndTraceRequest } from '../../shared/lib/trace'; +import { + CaveatTypes, + EndowmentTypes, +} from '../../shared/constants/permissions'; import * as actionConstants from './actionConstants'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { updateCustodyState } from './institutional/institution-actions'; @@ -1748,8 +1752,8 @@ export function setSelectedAccount( export function addPermittedAccount( origin: string, - address: [], -): ThunkAction { + address: string, +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { await new Promise((resolve, reject) => { callBackgroundMethod( @@ -1767,14 +1771,14 @@ export function addPermittedAccount( await forceUpdateMetamaskState(dispatch); }; } -export function addMorePermittedAccounts( +export function addPermittedAccounts( origin: string, address: string[], -): ThunkAction { +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { await new Promise((resolve, reject) => { callBackgroundMethod( - 'addMorePermittedAccounts', + 'addPermittedAccounts', [origin, address], (error) => { if (error) { @@ -1792,7 +1796,7 @@ export function addMorePermittedAccounts( export function removePermittedAccount( origin: string, address: string, -): ThunkAction { +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { await new Promise((resolve, reject) => { callBackgroundMethod( @@ -1811,6 +1815,67 @@ export function removePermittedAccount( }; } +export function addPermittedChain( + origin: string, + chainId: string, +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + await new Promise((resolve, reject) => { + callBackgroundMethod('addPermittedChain', [origin, chainId], (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + await forceUpdateMetamaskState(dispatch); + }; +} +export function addPermittedChains( + origin: string, + chainIds: string[], +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + await new Promise((resolve, reject) => { + callBackgroundMethod( + 'addPermittedChains', + [origin, chainIds], + (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }, + ); + }); + await forceUpdateMetamaskState(dispatch); + }; +} + +export function removePermittedChain( + origin: string, + chainId: string, +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + await new Promise((resolve, reject) => { + callBackgroundMethod( + 'removePermittedChain', + [origin, chainId], + (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }, + ); + }); + await forceUpdateMetamaskState(dispatch); + }; +} + export function showAccountsPage() { return { type: actionConstants.SHOW_ACCOUNTS_PAGE, @@ -2552,6 +2617,18 @@ export function hideImportNftsModal(): Action { }; } +export function hidePermittedNetworkToast(): Action { + return { + type: actionConstants.SHOW_PERMITTED_NETWORK_TOAST_CLOSE, + }; +} + +export function showPermittedNetworkToast(): Action { + return { + type: actionConstants.SHOW_PERMITTED_NETWORK_TOAST_OPEN, + }; +} + // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any export function setConfirmationExchangeRates(value: Record) { @@ -3143,7 +3220,7 @@ export function toggleNetworkMenu(payload?: { }; } -export function setAccountDetailsAddress(address: string) { +export function setAccountDetailsAddress(address: string[]) { return { type: actionConstants.SET_ACCOUNT_DETAILS_ADDRESS, payload: address, @@ -3800,6 +3877,19 @@ export function requestAccountsPermissionWithId( }; } +export function requestAccountsAndChainPermissionsWithId( + origin: string, +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + const id = await submitRequestToBackground( + 'requestAccountsAndChainPermissionsWithId', + [origin], + ); + await forceUpdateMetamaskState(dispatch); + return id; + }; +} + /** * Approves the permissions request. * @@ -5557,6 +5647,48 @@ export async function getNextAvailableAccountName( ); } +export async function grantPermittedChain( + selectedTabOrigin: string, + chainId?: string, +): Promise { + return await submitRequestToBackground('grantPermissionsIncremental', [ + { + subject: { origin: selectedTabOrigin }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: [chainId], + }, + ], + }, + }, + }, + ]); +} + +export async function grantPermittedChains( + selectedTabOrigin: string, + chainIds: string[], +): Promise { + return await submitRequestToBackground('grantPermissions', [ + { + subject: { origin: selectedTabOrigin }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: chainIds, + }, + ], + }, + }, + }, + ]); +} + export async function decodeTransactionData({ transactionData, contractAddress, From 83e499c0a121a79bab9889fcb9f1c2ff95e17050 Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Thu, 26 Sep 2024 08:45:42 -0700 Subject: [PATCH 011/226] fix: "Warning: Invalid argument supplied to oneOfType" (#27267) Fixes this Warning introduced in #26426: ``` ramps-card.js:150 Warning: Invalid argument supplied to oneOfType. Expected an array of check functions, but received undefined at index 1. ``` --- ui/components/multichain/ramps-card/ramps-card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index 6d988f8a8bad..ac72f4c5112f 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -147,5 +147,5 @@ export const RampsCard = ({ variant, handleOnClick }) => { RampsCard.propTypes = { variant: PropTypes.oneOf(Object.values(RAMPS_CARD_VARIANT_TYPES)), - handleOnClick: PropTypes.oneOfType([PropTypes.func, PropTypes.undefined]), + handleOnClick: PropTypes.func, }; From 0e2d3e5b7e867aa388328a9bd12d228f37749d74 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 26 Sep 2024 18:02:36 +0100 Subject: [PATCH 012/226] =?UTF-8?q?feat:=20Display=20setApprovalForAll=20a?= =?UTF-8?q?nd=20revoke=20setApprovalForAll=20to=20users=E2=80=A6=20(#27401?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … that opt in ## **Description** This makes both redesigned screens available for users that opt into redesigned transaction screens in the experimental settings page. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27401?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. --- ui/pages/confirmations/utils/confirm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 0b052fa2b359..8c2846b6b69a 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -21,11 +21,11 @@ export const REDESIGN_USER_TRANSACTION_TYPES = [ TransactionType.deployContract, TransactionType.tokenMethodApprove, TransactionType.tokenMethodIncreaseAllowance, + TransactionType.tokenMethodSetApprovalForAll, ]; export const REDESIGN_DEV_TRANSACTION_TYPES = [ ...REDESIGN_USER_TRANSACTION_TYPES, - TransactionType.tokenMethodSetApprovalForAll, ]; const SIGNATURE_APPROVAL_TYPES = [ From 4fbb0ea7028e3c0b7e8d2541a2801f4872d3554c Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 26 Sep 2024 18:03:19 +0100 Subject: [PATCH 013/226] fix: Change speed key color (#27416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** As part of #26986, the wrapper component for "Speed" [was changed from ConfirmInfoRow to ConfirmInfoAlertRow](https://github.com/MetaMask/metamask-extension/pull/26986/files#diff-dca84a26976b31828773277690e34b17f2120e4b8f821b2900f28b4f3c452c98R85). This change means that the `ConfirmInfoRowVariant.Default` applied to this component changes the color to blue. To revert that aesthetic change, this PR removes said variant. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27416?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** Screenshot 2024-09-26 at 10 57 44 ### **After** Screenshot 2024-09-26 at 10 56 03 ## **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. --- .../confirm/info/__snapshots__/info.test.tsx.snap | 6 +++--- .../info/approve/__snapshots__/approve.test.tsx.snap | 2 +- .../__snapshots__/base-transaction-info.test.tsx.snap | 6 +++--- .../__snapshots__/set-approval-for-all-info.test.tsx.snap | 2 +- .../__snapshots__/gas-fees-details.test.tsx.snap | 2 +- .../info/shared/gas-fees-details/gas-fees-details.tsx | 2 -- .../__snapshots__/gas-fees-section.test.tsx.snap | 2 +- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap index 60bb488888b3..c13f6bc4a695 100644 --- a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap @@ -257,7 +257,7 @@ exports[`Info renders info section for approve request 1`] = ` style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
renders component for approve request 1`] = ` style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
renders component for contract interaction requ style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
renders component for contract interaction requ style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
renders component for contract interaction requ style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
renders component for approve request 1`] = ` style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
renders component for gas fees section 1`] = ` style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap index 65932520e283..fcdefe6be7c1 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap @@ -78,7 +78,7 @@ exports[` renders component for gas fees section 1`] = ` style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" >
Date: Thu, 26 Sep 2024 19:14:01 +0200 Subject: [PATCH 014/226] fix: flaky test `Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx should queue send tx after switch network confirmation and transaction should target the correct network after switch is confirmed` (#27352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** There is a race condition with window management that makes this test fail quite often. The test performs these actions and there are different behaviors happening, depending on the build: 1. Action: trigger a network switch -> this opens a new dialog window (we have 4) 2. Action: trigger a send -> we are still with the network switch window open 3. Action: we click switch network -> this closes the dialog (now we have 3 windows for a brief moment - if this is fast, like in webpack, it fails with the error `Error: waitUntilXWindowHandles timed out polling window handles. Expected: 3, Actual: 4`) 4. A new dialog is open automatically -> now we have 4 windows again, but the dialog handle ids are different 5. Sometimes this dialog is closed and then a new one is open 6. We switch to the dialog, but often the context is invalidated as we are switching to either 1 or 4 or 5 Extra Notes: This PR fixes the flakiness of waiting for 3 windows by adding a delay. This won't fix the root cause of the 2 new identified issues, but after re-runs has proven to stabilize the test - In Webpack, sometimes the dialog for the transaction never appears, and the wallet get's in a frozen state. The issue is on the wallet level, see https://github.com/MetaMask/metamask-extension/issues/27414 - see issue https://github.com/MetaMask/metamask-extension/issues/27360 Since there is only a brief moment for having 3 windows, waiting for 3 windows and then proceeding also makes the test flaky, so it's not effective. ![Screenshot from 2024-09-26 11-26-34](https://github.com/user-attachments/assets/d4f7684f-6fc6-481f-9d47-a65656c8989a) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27352?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27387 ## **Manual testing steps** 1. Check ci 2. Run test locally `yarn test:e2e:single test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js --browser=chrome` ## **Screenshots/Recordings** Problem in FF and Webpack, see how popup is open and closed, and then a new popup is open with the transaction. https://github.com/user-attachments/assets/c17ff7bd-6b1d-4517-b33f-95f7ac6d3120 Problem in Webpack, where tx does not open after the change networks https://github.com/user-attachments/assets/2b626159-ec37-4b3c-9c91-80ab4011f751 Problem Webpack, see how popup is open and closed, and then a new popup is open with the transaction. https://github.com/user-attachments/assets/8165b3e7-2ed4-4704-91dc-82776edb0bd8 ## **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. --- .../dapp1-switch-dapp2-send.spec.js | 92 +++++++++---------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js index ee7200d8a59b..567ddf0f619d 100644 --- a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js @@ -1,14 +1,12 @@ const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, + DAPP_ONE_URL, + DAPP_URL, + defaultGanacheOptions, openDapp, unlockWallet, - DAPP_URL, - DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, - defaultGanacheOptions, - switchToNotificationWindow, + withFixtures, } = require('../../helpers'); describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { @@ -51,9 +49,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Next', @@ -61,7 +57,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', css: '[data-testid="page-container-footer-next"]', @@ -80,9 +76,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { css: 'p', }); - // Wait for the first dapp's connect confirmation to disappear - await driver.waitUntilXWindowHandles(2); - // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. // Open Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); @@ -91,9 +84,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Next', @@ -101,7 +92,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', css: '[data-testid="page-container-footer-next"]', @@ -121,23 +112,28 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findElement({ + text: 'Allow this site to switch the network?', + tag: 'h3', + }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); - await driver.findClickableElements({ - text: 'Switch network', - tag: 'button', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + // Wait for switch confirmation to close then tx confirmation to show. - await driver.waitUntilXWindowHandles(3); - await driver.delay(regularDelayMs); + // There is an extra window appearing and disappearing + // so we leave this delay until the issue is fixed (#27360) + await driver.delay(5000); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the send confirmation. await driver.findElement({ @@ -145,7 +141,10 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { text: 'Localhost 8546', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); // Switch back to the extension await driver.switchToWindowWithTitle( @@ -206,9 +205,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Next', @@ -216,7 +213,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', css: '[data-testid="page-container-footer-next"]', @@ -235,9 +232,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { css: 'p', }); - // Wait for the first dapp's connect confirmation to disappear - await driver.waitUntilXWindowHandles(2); - // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. // Open Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); @@ -246,9 +240,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Next', @@ -256,7 +248,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', css: '[data-testid="page-container-footer-next"]', @@ -276,23 +268,26 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findElement({ + text: 'Allow this site to switch the network?', + tag: 'h3', + }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); - await driver.findClickableElements({ - text: 'Cancel', - tag: 'button', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Cancel', tag: 'button' }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); // Wait for switch confirmation to close then tx confirmation to show. - await driver.waitUntilXWindowHandles(3); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver, 4); + // There is an extra window appearing and disappearing + // so we leave this delay until the issue is fixed (#27360) + await driver.delay(5000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the send confirmation. await driver.findElement({ @@ -300,7 +295,10 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { text: 'Localhost 8546', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); // Switch back to the extension await driver.switchToWindowWithTitle( From 251b480589162e18fa09a91af41bc2b8e888fcac Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:40:07 +0200 Subject: [PATCH 015/226] fix: flaky test `Responsive UI Send Transaction from responsive window` (#27417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Before clicking Confirm, we wait until the transaction gas and amount are loaded to avoid any possible rerender. We also login with balance validation, so we can get rid of the initial delay. https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/102392/workflows/75210923-91d6-44cc-b224-72f3f530dbb2/jobs/3815538/tests [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27417?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27418 ## **Manual testing steps** 1. Check ci 2. Run test locally `yarn test:e2e:single test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js --browser=chrome --leave-running=true` ## **Screenshots/Recordings** ![Screenshot from 2024-09-26 12-18-20](https://github.com/user-attachments/assets/604b995f-7db0-4d52-b129-351d82d0831e) ## **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. --- .../metamask-responsive-ui.spec.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js b/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js index 6afdb9062ac3..446d579630bf 100644 --- a/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js @@ -2,10 +2,10 @@ const { strict: assert } = require('assert'); const { TEST_SEED_PHRASE_TWO, defaultGanacheOptions, - withFixtures, locateAccountBalanceDOM, + logInWithBalanceValidation, openActionMenuAndStartSendFlow, - unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -123,10 +123,8 @@ describe('MetaMask Responsive UI', function () { ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); - - await driver.delay(1000); + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); // Send ETH from inside MetaMask // starts to send a transaction @@ -140,9 +138,13 @@ describe('MetaMask Responsive UI', function () { const inputValue = await inputAmount.getProperty('value'); assert.equal(inputValue, '1'); - - // confirming transcation await driver.clickElement({ text: 'Continue', tag: 'button' }); + + // wait for transaction value to be rendered and confirm + await driver.waitForSelector({ + css: '.currency-display-component__text', + text: '1.000042', + }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); // finds the transaction in the transactions list From 7a5e4d6b88897eb914780fdd15157fc0d9b6f0c5 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Fri, 27 Sep 2024 00:01:06 +0200 Subject: [PATCH 016/226] fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi`aded (#27420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In the process for creating a new account, we get to see this error: ``` TimeoutError: Waiting for element to be located By(css selector, [data-testid="account-list-menu-details"]) Wait timed out after 10023ms (Ran on CircleCI Node 17 of 20, Job test-e2e-chrome) at /home/circleci/project/node_modules/selenium-webdriver/lib/webdriver.js:929:17 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) ``` This happens after clicking the account menu, and after clicking the Add account. It seems that the click doesn't hit exactly the button due to the account list loading, making the button move. When the account list is loaded, the click is performed outside the button boundary, and this makes the popup close and you cannot find the next element: "account-list-menu-details" element. I haven't been able to reproduce this but I've seen this behaviour in the manual process [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27420?quickstart=1) ## **Related issues** Fixes: those 3 are fixed in this PR, as they were caused by the same issue - https://github.com/MetaMask/metamask-extension/issues/27419 - https://github.com/MetaMask/metamask-extension/issues/27337 - https://github.com/MetaMask/metamask-extension/issues/27336 ## **Manual testing steps** 1. Check ci 2. Run tests locally ## **Screenshots/Recordings** ## **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/account/add-account.spec.js | 5 +++ test/e2e/tests/account/import-flow.spec.js | 41 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js index a7136837b01d..c1a6136cc47d 100644 --- a/test/e2e/tests/account/add-account.spec.js +++ b/test/e2e/tests/account/add-account.spec.js @@ -74,6 +74,11 @@ describe('Add account', function () { // Create 2nd account await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); diff --git a/test/e2e/tests/account/import-flow.spec.js b/test/e2e/tests/account/import-flow.spec.js index 045600cff4f4..d2c84bfdc2b3 100644 --- a/test/e2e/tests/account/import-flow.spec.js +++ b/test/e2e/tests/account/import-flow.spec.js @@ -58,6 +58,11 @@ describe('Import flow @no-mmi', function () { // Show account information await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="account-list-item-menu-button"]', ); @@ -99,6 +104,11 @@ describe('Import flow @no-mmi', function () { // choose Create account from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -182,6 +192,11 @@ describe('Import flow @no-mmi', function () { // Show account information await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="account-list-item-menu-button"]', ); @@ -226,6 +241,11 @@ describe('Import flow @no-mmi', function () { await unlockWallet(driver); await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -249,6 +269,11 @@ describe('Import flow @no-mmi', function () { text: 'Imported', }); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 4', + tag: 'span', + }); // Imports Account 5 with private key await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', @@ -307,6 +332,11 @@ describe('Import flow @no-mmi', function () { await logInWithBalanceValidation(driver, ganacheServer); // Imports an account with JSON file await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -372,6 +402,11 @@ describe('Import flow @no-mmi', function () { // choose Import Account from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -406,6 +441,12 @@ describe('Import flow @no-mmi', function () { // choose Connect hardware wallet from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); From 122867f102943a9008da9da2eb3c2e862ee82663 Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:16:29 -0700 Subject: [PATCH 017/226] chore: set bridge selected tokens and amount (#26212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Changes include * redux actions to set the selected src/dest tokens and token amount * redux selectors to get the selected src/dest tokens and token amount [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26212?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/METABRIDGE-866 ## **Manual testing steps** N/A. This doesn't change any user functionality, just setting up getters/setters ## **Screenshots/Recordings** N/A, redux state changes will take effect after UI for inputs is implemented ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/ducks/bridge/actions.ts | 7 +-- ui/ducks/bridge/bridge.test.ts | 48 +++++++++++++++- ui/ducks/bridge/bridge.ts | 22 +++++++- ui/ducks/bridge/selectors.test.ts | 94 +++++++++++++++++++++++++++++++ ui/ducks/bridge/selectors.ts | 26 ++++++++- 5 files changed, 187 insertions(+), 10 deletions(-) diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index a0c852512867..24cd9728625f 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -4,13 +4,12 @@ import { BridgeBackgroundAction } from '../../../app/scripts/controllers/bridge/ import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; -import { swapsSlice } from '../swaps/swaps'; import { bridgeSlice } from './bridge'; -// eslint-disable-next-line no-empty-pattern -const {} = swapsSlice.actions; +const { setToChain, setFromToken, setToToken, setFromTokenInputValue } = + bridgeSlice.actions; -export const { setToChain } = bridgeSlice.actions; +export { setToChain, setFromToken, setToToken, setFromTokenInputValue }; const callBridgeControllerMethod = ( bridgeAction: BridgeBackgroundAction, diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index 6b6acf115396..0bfdb47b35eb 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -7,7 +7,13 @@ import { setBackgroundConnection } from '../../store/background-connection'; // eslint-disable-next-line import/no-restricted-paths import { BridgeBackgroundAction } from '../../../app/scripts/controllers/bridge/types'; import bridgeReducer from './bridge'; -import { setBridgeFeatureFlags, setToChain } from './actions'; +import { + setBridgeFeatureFlags, + setFromToken, + setFromTokenInputValue, + setToChain, + setToToken, +} from './actions'; const middleware = [thunk]; @@ -15,6 +21,10 @@ describe('Ducks - Bridge', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const store = configureMockStore(middleware)(createBridgeMockStore()); + beforeEach(() => { + store.clearActions(); + }); + describe('setToChain', () => { it('calls the "bridge/setToChain" action', () => { const state = store.getState().bridge; @@ -27,6 +37,42 @@ describe('Ducks - Bridge', () => { }); }); + describe('setFromToken', () => { + it('calls the "bridge/setFromToken" action', () => { + const state = store.getState().bridge; + const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; + store.dispatch(setFromToken(actionPayload)); + const actions = store.getActions(); + expect(actions[0].type).toBe('bridge/setFromToken'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.fromToken).toBe(actionPayload); + }); + }); + + describe('setToToken', () => { + it('calls the "bridge/setToToken" action', () => { + const state = store.getState().bridge; + const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; + store.dispatch(setToToken(actionPayload)); + const actions = store.getActions(); + expect(actions[0].type).toBe('bridge/setToToken'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toToken).toBe(actionPayload); + }); + }); + + describe('setFromTokenInputValue', () => { + it('calls the "bridge/setFromTokenInputValue" action', () => { + const state = store.getState().bridge; + const actionPayload = '10'; + store.dispatch(setFromTokenInputValue(actionPayload)); + const actions = store.getActions(); + expect(actions[0].type).toBe('bridge/setFromTokenInputValue'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.fromTokenInputValue).toBe(actionPayload); + }); + }); + describe('setBridgeFeatureFlags', () => { it('should call setBridgeFeatureFlags in the background', async () => { const mockSetBridgeFeatureFlags = jest.fn(); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 2f7c2c3482cd..a35534381000 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -1,15 +1,22 @@ import { createSlice } from '@reduxjs/toolkit'; import { swapsSlice } from '../swaps/swaps'; -import { RPCDefinition } from '../../../shared/constants/network'; +import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { SwapsEthToken } from '../../selectors'; +import { MultichainProviderConfig } from '../../../shared/constants/multichain/networks'; -// Only states that are not in swaps slice export type BridgeState = { - toChain: RPCDefinition | null; + toChain: MultichainProviderConfig | null; + fromToken: SwapsTokenObject | SwapsEthToken | null; + toToken: SwapsTokenObject | SwapsEthToken | null; + fromTokenInputValue: string | null; }; const initialState: BridgeState = { toChain: null, + fromToken: null, + toToken: null, + fromTokenInputValue: null, }; const bridgeSlice = createSlice({ @@ -20,6 +27,15 @@ const bridgeSlice = createSlice({ setToChain: (state, action) => { state.toChain = action.payload; }, + setFromToken: (state, action) => { + state.fromToken = action.payload; + }, + setToToken: (state, action) => { + state.toToken = action.payload; + }, + setFromTokenInputValue: (state, action) => { + state.fromTokenInputValue = action.payload; + }, }, }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 50a5ad4beb33..9a7c818fb20a 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -5,11 +5,15 @@ import { getProviderConfig } from '../metamask/metamask'; import { mockNetworkState } from '../../../test/stub/networks'; import { getAllBridgeableNetworks, + getFromAmount, getFromChain, getFromChains, + getFromToken, getIsBridgeTx, + getToAmount, getToChain, getToChains, + getToToken, } from './selectors'; describe('Bridge selectors', () => { @@ -272,4 +276,94 @@ describe('Bridge selectors', () => { expect(result).toBe(true); }); }); + + describe('getFromToken', () => { + it('returns fromToken', () => { + const state = createBridgeMockStore( + {}, + + { fromToken: { address: '0x123', symbol: 'TEST' } }, + ); + const result = getFromToken(state as never); + + expect(result).toStrictEqual({ address: '0x123', symbol: 'TEST' }); + }); + + it('returns defaultToken if fromToken has no address', () => { + const state = createBridgeMockStore( + {}, + { fromToken: { symbol: 'NATIVE' } }, + ); + const result = getFromToken(state as never); + + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + balance: '0', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + string: '0', + symbol: 'ETH', + }); + }); + + it('returns defaultToken if fromToken is undefined', () => { + const state = createBridgeMockStore({}, { fromToken: undefined }); + const result = getFromToken(state as never); + + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + balance: '0', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + string: '0', + symbol: 'ETH', + }); + }); + }); + + describe('getToToken', () => { + it('returns toToken', () => { + const state = createBridgeMockStore( + {}, + { toToken: { address: '0x123', symbol: 'TEST' } }, + ); + const result = getToToken(state as never); + + expect(result).toStrictEqual({ address: '0x123', symbol: 'TEST' }); + }); + + it('returns undefined if toToken is undefined', () => { + const state = createBridgeMockStore({}, { toToken: null }); + const result = getToToken(state as never); + + expect(result).toStrictEqual(null); + }); + }); + + describe('getFromAmount', () => { + it('returns fromTokenInputValue', () => { + const state = createBridgeMockStore({}, { fromTokenInputValue: '123' }); + const result = getFromAmount(state as never); + + expect(result).toStrictEqual('123'); + }); + + it('returns empty string', () => { + const state = createBridgeMockStore({}, { fromTokenInputValue: '' }); + const result = getFromAmount(state as never); + + expect(result).toStrictEqual(''); + }); + }); + + describe('getToAmount', () => { + it('returns hardcoded 0', () => { + const state = createBridgeMockStore(); + const result = getToAmount(state as never); + + expect(result).toStrictEqual('0'); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 5f482ddecb91..b688b2096e51 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -1,8 +1,10 @@ import { NetworkState } from '@metamask/network-controller'; import { uniqBy } from 'lodash'; import { - getIsBridgeEnabled, getNetworkConfigurationsByChainId, + getIsBridgeEnabled, + getSwapsDefaultToken, + SwapsEthToken, } from '../../selectors'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { @@ -14,9 +16,9 @@ import { import { FEATURED_RPCS } from '../../../shared/constants/network'; import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; +import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { BridgeState } from './bridge'; -// TODO add swaps state type BridgeAppState = { metamask: NetworkState & { bridgeState: BridgeControllerState } & { useExternalServices: boolean; @@ -61,6 +63,26 @@ export const getToChains = createDeepEqualSelector( ), ); +export const getFromToken = ( + state: BridgeAppState, +): SwapsTokenObject | SwapsEthToken => { + return state.bridge.fromToken?.address + ? state.bridge.fromToken + : getSwapsDefaultToken(state); +}; + +export const getToToken = ( + state: BridgeAppState, +): SwapsTokenObject | SwapsEthToken | null => { + return state.bridge.toToken; +}; + +export const getFromAmount = (state: BridgeAppState): string | null => + state.bridge.fromTokenInputValue; +export const getToAmount = (_state: BridgeAppState) => { + return '0'; +}; + export const getIsBridgeTx = createDeepEqualSelector( getFromChain, getToChain, From 1fec98f9ada75bd29932b5ecffef127f30973da9 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Fri, 27 Sep 2024 18:04:49 +0200 Subject: [PATCH 018/226] fix(snaps): Set proper text color for secondary button (#27335) --- .../app/snaps/snap-ui-footer-button/index.scss | 14 ++++++++++++++ .../snaps/snap-ui-renderer/components/footer.ts | 9 +++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-footer-button/index.scss b/ui/components/app/snaps/snap-ui-footer-button/index.scss index db46c4928592..7207ac496c76 100644 --- a/ui/components/app/snaps/snap-ui-footer-button/index.scss +++ b/ui/components/app/snaps/snap-ui-footer-button/index.scss @@ -45,6 +45,20 @@ } } + &:not(&--disabled) { + &:hover { + cursor: pointer; + } + } + + &.mm-button-secondary { + &:hover:not(&--disabled) { + & > span { + color: var(--color-primary-inverse); + } + } + } + &--disabled { cursor: default !important; } diff --git a/ui/components/app/snaps/snap-ui-renderer/components/footer.ts b/ui/components/app/snaps/snap-ui-renderer/components/footer.ts index 03aaace0bed8..d08284bcf572 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/footer.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/footer.ts @@ -45,6 +45,7 @@ const getDefaultButtons = ( key: 'default-button', props: { onCancel, + variant: ButtonVariant.Secondary, isSnapAction: false, }, children: t('cancel'), @@ -62,8 +63,9 @@ export const footer: UIComponentFactory = ({ }) => { const defaultButtons = getDefaultButtons(element, t, onCancel); + const providedChildren = getJsxChildren(element); const footerChildren: UIComponent[] = ( - getJsxChildren(element) as ButtonElement[] + providedChildren as ButtonElement[] ).map((children, index) => { const buttonMapped = buttonFn({ ...params, @@ -74,7 +76,10 @@ export const footer: UIComponentFactory = ({ key: `snap-footer-button-${buttonMapped.props?.name ?? index}`, props: { ...buttonMapped.props, - variant: index === 0 ? ButtonVariant.Secondary : ButtonVariant.Primary, + variant: + providedChildren.length === 2 && index === 0 + ? ButtonVariant.Secondary + : ButtonVariant.Primary, isSnapAction: true, }, children: buttonMapped.children, From 8d667a3a96f8a1293fae35b2d71730fad0ebbe43 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Fri, 27 Sep 2024 18:52:55 +0100 Subject: [PATCH 019/226] fix: removed closeMenu for ConnectedAccountsMenu (#27460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR is to remove the unused closeMenu prop from ConnectedAccountsMenu ## **Related issues** Fixes: #27454 ## **Manual testing steps** 1. Go to the connections page 2. click on three dot menu for connected accounts 3. everything should be working as it is ## **Screenshots/Recordings** ### **Before** NA ### **After** NA ## **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. --- .../multichain/account-list-item/account-list-item.js | 1 - .../connected-accounts-menu/connected-accounts-menu.test.tsx | 1 - .../connected-accounts-menu/connected-accounts-menu.tsx | 3 --- 3 files changed, 5 deletions(-) diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index 5b3d972da393..517639b1c86e 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -420,7 +420,6 @@ const AccountListItem = ({ anchorElement={accountListItemMenuElement} account={account} onClose={() => setAccountOptionsMenuOpen(false)} - closeMenu={closeMenu} disableAccountSwitcher={isSingleAccount && selected} isOpen={accountOptionsMenuOpen} onActionClick={onActionClick} diff --git a/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.test.tsx b/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.test.tsx index 4c81b134b28c..91ffeb5e21a7 100644 --- a/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.test.tsx +++ b/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.test.tsx @@ -19,7 +19,6 @@ const DEFAULT_PROPS = { }, anchorElement: null, disableAccountSwitcher: false, - closeMenu: jest.fn(), onActionClick: jest.fn(), activeTabOrigin: 'metamask.github.io', }; diff --git a/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.tsx b/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.tsx index fccfc7245821..47be2557d1f8 100644 --- a/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.tsx +++ b/ui/components/multichain/connected-accounts-menu/connected-accounts-menu.tsx @@ -34,7 +34,6 @@ export const ConnectedAccountsMenu = ({ anchorElement, disableAccountSwitcher = false, onClose, - closeMenu, onActionClick, activeTabOrigin, }: { @@ -43,7 +42,6 @@ export const ConnectedAccountsMenu = ({ anchorElement: HTMLElement | null; disableAccountSwitcher: boolean; onClose: () => void; - closeMenu: () => void; onActionClick: (message: string) => void; activeTabOrigin: string; }) => { @@ -123,7 +121,6 @@ export const ConnectedAccountsMenu = ({ onClick={() => { dispatch(setSelectedAccount(account.address)); onClose(); - closeMenu(); }} > From d730f99a8e1636a3a2dbef8650cbc435e064393c Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 27 Sep 2024 19:40:04 +0100 Subject: [PATCH 020/226] =?UTF-8?q?fix:=20Handle=20null=20return=20value?= =?UTF-8?q?=20from=20getMethodData=20to=20prevent=20destructu=E2=80=A6=20(?= =?UTF-8?q?#27457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ring error ## **Description** The original code assumes that getMethodData will always return an object with a name property. However, in certain instances, getMethodData can return null. When this happens, destructuring the name property from null causes a runtime error. To address this issue, the code has been updated to use optional chaining. This ensures that if getMethodData returns null, the destructuring will not occur, and contractMethodName will be set to undefined instead of causing an error. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27457?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27436 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/transaction/metrics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/transaction/metrics.ts b/app/scripts/lib/transaction/metrics.ts index 66c1e7fcd669..e0be105b1f10 100644 --- a/app/scripts/lib/transaction/metrics.ts +++ b/app/scripts/lib/transaction/metrics.ts @@ -816,10 +816,10 @@ async function buildEventFragmentProperties({ let contractMethodName; if (transactionMeta.txParams.data) { - const { name } = await transactionMetricsRequest.getMethodData( + const methodData = await transactionMetricsRequest.getMethodData( transactionMeta.txParams.data, ); - contractMethodName = name; + contractMethodName = methodData?.name; } // TODO: Replace `any` with type From 1bd0b9e45c5e8bcece84edf01377adfa8ed58129 Mon Sep 17 00:00:00 2001 From: Priya Date: Fri, 27 Sep 2024 22:34:25 +0200 Subject: [PATCH 021/226] test: Fix flaky permit test (#27450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes: `test/e2e/tests/confirmations/signatures/permit.spec.ts` Issue was that e2e were passing in CI, but failing locally. The changes here provide a few helpers to prevent this flakey behavior. The issue was first flagged here: https://github.com/MetaMask/metamask-extension/pull/27184#discussion_r1765883386 Follow up slack thread here: https://consensys.slack.com/archives/C03ETQA9EPK/p1727373903173259 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27450?quickstart=1) ## **Related issues** Fixes: `test/e2e/tests/confirmations/signatures/permit.spec.ts` when running chrome e2e locally ## **Manual testing steps** 1. Running `permit.spec.ts` should pass e2e chrome locally and in CI ## **Screenshots/Recordings** 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** - [ ] 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: Nick Gambino <35090461+gambinish@users.noreply.github.com> --- .../e2e/tests/confirmations/signatures/permit.spec.ts | 11 ++++++----- .../confirmations/signatures/signature-helpers.ts | 7 +++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index f631d4dbf12d..2a87db442da5 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -46,11 +46,6 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await clickHeaderInfoBtn(driver); await assertHeaderInfoBalance(driver); - await assertAccountDetailsMetrics( - driver, - mockedEndpoints as MockedEndpoint[], - 'eth_signTypedData_v4', - ); await copyAddressAndPasteWalletAddress(driver); await assertPastedAddress(driver); @@ -60,6 +55,12 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await scrollAndConfirmAndAssertConfirm(driver); await driver.delay(1000); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v4', + ); + await assertSignatureConfirmedMetrics({ driver, mockedEndpoints: mockedEndpoints as MockedEndpoint[], diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index 029499f230ec..d69a2f6a69ac 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; import { MockedEndpoint } from 'mockttp'; +import { Key } from 'selenium-webdriver/lib/input'; import { WINDOW_TITLES, getEventPayloads, @@ -209,9 +210,11 @@ function assertEventPropertiesMatch( export async function clickHeaderInfoBtn(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement( - 'button[data-testid="header-info__account-details-button"]', + + const accountDetailsButton = await driver.findElement( + '[data-testid="header-info__account-details-button"]', ); + await accountDetailsButton.sendKeys(Key.RETURN); } export async function assertHeaderInfoBalance(driver: Driver) { From d1b778cb2e1807cbd2c5019fcc1fa900806fe808 Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Fri, 27 Sep 2024 17:34:31 -0700 Subject: [PATCH 022/226] ci: Sentry reporting only on develop branch, with Git message overrides (#27412) Vaibhav asked for new Sentry reporting rates from CircleCI: - 10 times as frequent from the `develop` branch - Never from other branches I also put in an override, such that if your Git message includes `[flags.sentry.tracesSampleRate: x.xx]` (a decimal number from 0.00 to 1.00), it will set `tracesSampleRate` to that fraction. Moved what used to be at `_flags.doNotForceSentryForThisTest` to `_flags.sentry.doNotForceSentryForThisTest` --- app/scripts/lib/manifestFlags.ts | 5 ++++- app/scripts/lib/setupSentry.js | 17 ++++++++++++--- test/e2e/set-manifest-flags.ts | 28 +++++++++++++++++++++++++ test/e2e/tests/metrics/errors.spec.js | 28 ++++++++++++------------- test/e2e/tests/metrics/sessions.spec.ts | 4 ++-- test/e2e/tests/metrics/traces.spec.ts | 8 +++---- 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/app/scripts/lib/manifestFlags.ts b/app/scripts/lib/manifestFlags.ts index fce667714ebe..a013373ac9f2 100644 --- a/app/scripts/lib/manifestFlags.ts +++ b/app/scripts/lib/manifestFlags.ts @@ -9,7 +9,10 @@ export type ManifestFlags = { nodeIndex?: number; prNumber?: number; }; - doNotForceSentryForThisTest?: boolean; + sentry?: { + tracesSampleRate?: number; + doNotForceSentryForThisTest?: boolean; + }; }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- you can't extend a type, we want this to be an interface diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 30d9bd34671b..c424354bc984 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -115,8 +115,19 @@ function getTracesSampleRate(sentryTarget) { const flags = getManifestFlags(); + // Grab the tracesSampleRate that may have come in from a git message + // 0 is a valid value, so must explicitly check for undefined + if (flags.sentry?.tracesSampleRate !== undefined) { + return flags.sentry.tracesSampleRate; + } + if (flags.circleci) { - return 0.003; + // Report very frequently on develop branch, and never on other branches + // (Unless you do a [flags.sentry.tracesSampleRate: x.xx] override) + if (flags.circleci.branch === 'develop') { + return 0.03; + } + return 0; } if (METAMASK_DEBUG) { @@ -227,7 +238,7 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - getManifestFlags().doNotForceSentryForThisTest || + getManifestFlags().sentry?.doNotForceSentryForThisTest || (process.env.IN_TEST && !SENTRY_DSN_DEV) ) { return SENTRY_DSN_FAKE; @@ -261,7 +272,7 @@ async function getMetaMetricsEnabled() { if ( METAMASK_BUILD_TYPE === 'mmi' || - (flags.circleci && !flags.doNotForceSentryForThisTest) + (flags.circleci && !flags.sentry?.doNotForceSentryForThisTest) ) { return true; } diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index 6e1c16efa82d..e8d02a12e2cd 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -1,3 +1,4 @@ +import { execSync } from 'child_process'; import fs from 'fs'; import { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; @@ -7,6 +8,25 @@ function parseIntOrUndefined(value: string | undefined): number | undefined { return value ? parseInt(value, 10) : undefined; } +// Grab the tracesSampleRate from the git message if it's set +function getTracesSampleRateFromGitMessage(): number | undefined { + const gitMessage = execSync( + `git show --format='%B' --no-patch "HEAD"`, + ).toString(); + + // Search gitMessage for `[flags.sentry.tracesSampleRate: 0.000 to 1.000]` + const tracesSampleRateMatch = gitMessage.match( + /\[flags\.sentry\.tracesSampleRate: (0*(\.\d+)?|1(\.0*)?)\]/u, + ); + + if (tracesSampleRateMatch) { + // Return 1st capturing group from regex + return parseFloat(tracesSampleRateMatch[1]); + } + + return undefined; +} + // Alter the manifest with CircleCI environment variables and custom flags export function setManifestFlags(flags: ManifestFlags = {}) { if (process.env.CIRCLECI) { @@ -20,6 +40,14 @@ export function setManifestFlags(flags: ManifestFlags = {}) { process.env.CIRCLE_PULL_REQUEST?.split('/').pop(), // The CIRCLE_PR_NUMBER variable is only available on forked Pull Requests ), }; + + const tracesSampleRate = getTracesSampleRateFromGitMessage(); + + // 0 is a valid value, so must explicitly check for undefined + if (tracesSampleRate !== undefined) { + // Add tracesSampleRate to flags.sentry (which may or may not already exist) + flags.sentry = { ...flags.sentry, tracesSampleRate }; + } } const manifest = JSON.parse( diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index ee22bdd93815..fdeb4437d428 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -247,7 +247,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -278,7 +278,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -319,7 +319,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -365,7 +365,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -426,7 +426,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryInvariantMigrationError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -475,7 +475,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -521,7 +521,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -585,7 +585,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -621,7 +621,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -656,7 +656,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -702,7 +702,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -766,7 +766,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -810,7 +810,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -898,7 +898,7 @@ describe('Sentry errors', function () { ganacheOptions, title: this.test.fullTitle(), manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver }) => { diff --git a/test/e2e/tests/metrics/sessions.spec.ts b/test/e2e/tests/metrics/sessions.spec.ts index b5666a9078b8..f1bdee4538fb 100644 --- a/test/e2e/tests/metrics/sessions.spec.ts +++ b/test/e2e/tests/metrics/sessions.spec.ts @@ -38,7 +38,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -60,7 +60,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { diff --git a/test/e2e/tests/metrics/traces.spec.ts b/test/e2e/tests/metrics/traces.spec.ts index 62c4d7da9219..194f36ff73b0 100644 --- a/test/e2e/tests/metrics/traces.spec.ts +++ b/test/e2e/tests/metrics/traces.spec.ts @@ -51,7 +51,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -73,7 +73,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -95,7 +95,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { @@ -117,7 +117,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { doNotForceSentryForThisTest: true }, }, }, async ({ driver, mockedEndpoint }) => { From cd2aefb23c738a905bba6374b526c516c2f35a7c Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Mon, 30 Sep 2024 12:21:24 +0200 Subject: [PATCH 023/226] chore: Add `useLedgerConnection` unit tests (#27358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds `useLedgerConnnection` hook unit tests. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27358?quickstart=1) ## **Related issues** Fixes: - ## **Manual testing steps** No QA needed. ## **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. --- .../hooks/useLedgerConnection.test.ts | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 ui/pages/confirmations/hooks/useLedgerConnection.test.ts diff --git a/ui/pages/confirmations/hooks/useLedgerConnection.test.ts b/ui/pages/confirmations/hooks/useLedgerConnection.test.ts new file mode 100644 index 000000000000..42868115a369 --- /dev/null +++ b/ui/pages/confirmations/hooks/useLedgerConnection.test.ts @@ -0,0 +1,319 @@ +import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { KeyringObject } from '@metamask/keyring-controller'; +import type { Hex } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import { KeyringType } from '../../../../shared/constants/keyring'; +import { renderHookWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers'; +import { getMockConfirmStateForTransaction } from '../../../../test/data/confirmations/helper'; +import { genUnapprovedApproveConfirmation } from '../../../../test/data/confirmations/contract-interaction'; +import { flushPromises } from '../../../../test/lib/timer-helpers'; +import { + LedgerTransportTypes, + WebHIDConnectedStatuses, + LEDGER_USB_VENDOR_ID, + HardwareTransportStates, +} from '../../../../shared/constants/hardware-wallets'; +import * as appActions from '../../../ducks/app/app'; +import { attemptLedgerTransportCreation } from '../../../store/actions'; +import useLedgerConnection from './useLedgerConnection'; + +jest.mock('../../../store/actions', () => ({ + ...jest.requireActual('../../../store/actions'), + attemptLedgerTransportCreation: jest.fn(), +})); + +type RootState = { + metamask: Record; + appState: Record; +} & Record; + +const MOCK_LEDGER_ACCOUNT = '0x1234567890abcdef1234567890abcdef12345678'; + +const updateLedgerHardwareAccounts = (keyrings: KeyringObject[]) => { + const ledgerHardwareIndex = keyrings.findIndex( + (keyring) => keyring.type === KeyringType.ledger, + ); + + if (ledgerHardwareIndex === -1) { + // If 'Ledger Hardware' does not exist, create a new entry + keyrings.push({ + type: KeyringType.ledger, + accounts: [MOCK_LEDGER_ACCOUNT], + }); + } else { + // If 'Ledger Hardware' exists, update its accounts + keyrings[ledgerHardwareIndex].accounts = [MOCK_LEDGER_ACCOUNT]; + } + + return keyrings; +}; + +const generateUnapprovedConfirmationOnLedgerState = (address: Hex) => { + const transactionMeta = genUnapprovedApproveConfirmation({ + address, + chainId: '0x5', + }) as TransactionMeta; + + const clonedState = cloneDeep( + getMockConfirmStateForTransaction(transactionMeta), + ) as RootState; + + clonedState.metamask.keyrings = updateLedgerHardwareAccounts( + clonedState.metamask.keyrings as KeyringObject[], + ); + + clonedState.metamask.ledgerTransportType = LedgerTransportTypes.webhid; + + return clonedState; +}; + +describe('useLedgerConnection', () => { + const mockAttemptLedgerTransportCreation = jest.mocked( + attemptLedgerTransportCreation, + ); + + let state: RootState; + let originalNavigatorHid: HID; + + beforeEach(() => { + originalNavigatorHid = window.navigator.hid; + jest.resetAllMocks(); + Object.defineProperty(window.navigator, 'hid', { + value: { + getDevices: jest + .fn() + .mockImplementation(() => + Promise.resolve([{ vendorId: Number(LEDGER_USB_VENDOR_ID) }]), + ), + }, + configurable: true, + }); + + state = generateUnapprovedConfirmationOnLedgerState(MOCK_LEDGER_ACCOUNT); + }); + + afterAll(() => { + Object.defineProperty(window.navigator, 'hid', { + value: originalNavigatorHid, + configurable: true, + }); + }); + + describe('checks hid devices initially', () => { + it('set LedgerWebHidConnectedStatus to connected if it finds Ledger hid', async () => { + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.notConnected; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).toHaveBeenCalledWith( + WebHIDConnectedStatuses.connected, + ); + }); + + it('set LedgerWebHidConnectedStatus to notConnected if it does not find Ledger hid', async () => { + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.unknown; + + (window.navigator.hid.getDevices as jest.Mock).mockImplementationOnce( + () => Promise.resolve([]), + ); + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).toHaveBeenCalledWith( + WebHIDConnectedStatuses.notConnected, + ); + }); + }); + + describe('determines transport status', () => { + it('set LedgerTransportStatus to verified if transport creation is successful', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockResolvedValue(true); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.verified, + ); + }); + + it('set LedgerTransportStatus to unknownFailure if transport creation fails', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockResolvedValue(false); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.unknownFailure, + ); + }); + + it('set LedgerTransportStatus to deviceOpenFailure if device open fails', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockRejectedValue( + new Error('Failed to open the device'), + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.deviceOpenFailure, + ); + }); + + it('set LedgerTransportStatus to verified if device is already open', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockRejectedValue( + new Error('the device is already open'), + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.verified, + ); + }); + + it('set LedgerTransportStatus to unknownFailure if an unknown error occurs', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockRejectedValue( + new Error('Unknown error'), + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.unknownFailure, + ); + }); + }); + + it('reset LedgerTransportStatus to none on unmount', () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + const { unmount } = renderHookWithConfirmContextProvider( + useLedgerConnection, + state, + ); + + unmount(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.none, + ); + }); + + describe('does nothing', () => { + it('when address is not a ledger address', async () => { + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + // Set state to have empty keyrings, simulating a non-Ledger address + state.metamask.keyrings = []; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).not.toHaveBeenCalled(); + expect(spyOnSetLedgerTransportStatus).not.toHaveBeenCalled(); + }); + + it('when from address is not defined in currentConfirmation', async () => { + const tempState = generateUnapprovedConfirmationOnLedgerState( + undefined as unknown as Hex, + ); + + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + renderHookWithConfirmContextProvider(useLedgerConnection, tempState); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).not.toHaveBeenCalled(); + expect(spyOnSetLedgerTransportStatus).not.toHaveBeenCalled(); + }); + }); +}); From 2eebe1b6763f72860047c443822a83c2b3de634e Mon Sep 17 00:00:00 2001 From: Zbyszek Tenerowicz Date: Mon, 30 Sep 2024 12:37:01 +0200 Subject: [PATCH 024/226] ci: Expand github bot policy update comment to be more actionable (#27242) --- .github/workflows/update-lavamoat-policies.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-lavamoat-policies.yml b/.github/workflows/update-lavamoat-policies.yml index 1baef7fb4460..c8f9c190e533 100644 --- a/.github/workflows/update-lavamoat-policies.yml +++ b/.github/workflows/update-lavamoat-policies.yml @@ -201,7 +201,7 @@ jobs: run: | if [[ $HAS_CHANGES == 'true' ]] then - gh pr comment "${PR_NUMBER}" --body 'Policies updated' + echo -e 'Policies updated. \n👀 Please review the diff for suspicious new powers. \n\n🧠 Learn how: https://lavamoat.github.io/guides/policy-diff/#what-to-look-for-when-reviewing-a-policy-diff' | gh pr comment "${PR_NUMBER}" --body-file - else gh pr comment "${PR_NUMBER}" --body 'No policy changes' fi From b665a1cc3828d61254987c5d916029e1bdbef815 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 30 Sep 2024 10:46:07 -0230 Subject: [PATCH 025/226] feat: Double Sentry performance trace sample rate (#27468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The Sentry trace sample rate for production has been doubled, to take advantage of increased Sentry transaction quotas for our account. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27468?quickstart=1) ## **Related issues** Closes #27467 ## **Manual testing steps** It's not easy to test this because it's probabilistic. ## **Screenshots/Recordings** 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/setupSentry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index c424354bc984..14e3bc0934d8 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -134,7 +134,7 @@ function getTracesSampleRate(sentryTarget) { return 1.0; } - return 0.01; + return 0.02; } /** From 98a9df7163c152437302fc4469070846b8c6b41b Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:03:00 +0200 Subject: [PATCH 026/226] fix: flaky test `Navigation Signature - Different signature types initiates multiple signatures and rejects all` (#27481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** There is a delay for waiting the signatures to be queued, which sometimes is not enough. In this fix, we remove the delay and we add conditions to wait for (each new signature is added in the navigation), so the behaviour is deterministic. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27481?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27480 ## **Manual testing steps** 1. Check ci 2. Run test locally `yarn test:e2e:single test/e2e/tests/confirmations/navigation.spec.ts --browser=chrome --leave-running=true` ## **Screenshots/Recordings** See how the last signature is not properly queued, so in the last screen we don't see the navigation. We should wait on each new signature to be added in the navigation queue, before adding a new one https://github.com/user-attachments/assets/4637fb21-aeb5-4b4f-b2c2-b03b349211a1 ## **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/tests/confirmations/navigation.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts index 8d195656dc44..d6befdff0a26 100644 --- a/test/e2e/tests/confirmations/navigation.spec.ts +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -1,12 +1,12 @@ import { strict as assert } from 'assert'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { Suite } from 'mocha'; +import { By } from 'selenium-webdriver'; import { DAPP_HOST_ADDRESS, - WINDOW_TITLES, openDapp, - regularDelayMs, unlockWallet, + WINDOW_TITLES, } from '../../helpers'; import { Driver } from '../../webdriver/driver'; import { withRedesignConfirmationFixtures } from './helpers'; @@ -98,7 +98,6 @@ describe('Navigation Signature - Different signature types', function (this: Sui await unlockWallet(driver); await openDapp(driver); await queueSignatures(driver); - await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="confirm-nav__reject-all"]'); @@ -166,11 +165,13 @@ async function queueSignatures(driver: Driver) { await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Reject all' }); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4'); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function queueSignaturesAndTransactions(driver: Driver) { From b44e890011afdb1b6daf3f8b07994fb21a83ac79 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 30 Sep 2024 09:47:58 -0700 Subject: [PATCH 027/226] fix: Fix snaps permission connection for `CHAIN_PERMISSIONS` feature flag (#27459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Recent permission flow changes introduced behind the `CHAIN_PERMISSIONS` feature flag have broken permission connection for Snaps. This PR fixes it by removing an incorrect forced route direct in the permission connection component. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27459?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/pull/26635 ## **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. --- .../permission-page-container.component.js | 6 +- .../permissions-connect.component.js | 91 +++++++++---------- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/ui/components/app/permission-page-container/permission-page-container.component.js b/ui/components/app/permission-page-container/permission-page-container.component.js index da7719f6d4dc..f5f69da8f947 100644 --- a/ui/components/app/permission-page-container/permission-page-container.component.js +++ b/ui/components/app/permission-page-container/permission-page-container.component.js @@ -147,7 +147,7 @@ export default class PermissionPageContainer extends Component { ); const permittedChainsPermission = - _request.permissions[PermissionNames.permittedChains]; + _request.permissions?.[PermissionNames.permittedChains]; const approvedChainIds = permittedChainsPermission?.caveats.find( (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, )?.value; @@ -155,8 +155,8 @@ export default class PermissionPageContainer extends Component { const request = { ..._request, permissions: { ..._request.permissions }, - ...(_request.permissions.eth_accounts && { approvedAccounts }), - ...(_request.permissions[PermissionNames.permittedChains] && { + ...(_request.permissions?.eth_accounts && { approvedAccounts }), + ...(_request.permissions?.[PermissionNames.permittedChains] && { approvedChainIds, }), }; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 403c431330b1..09befa7218cd 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -148,9 +148,6 @@ export default class PermissionConnect extends Component { history.replace(DEFAULT_ROUTE); return; } - if (process.env.CHAIN_PERMISSIONS) { - history.replace(confirmPermissionPath); - } // if this is an incremental permission request for permitted chains, skip the account selection if ( permissionsRequest?.diff?.permissionDiffMap?.[ @@ -341,33 +338,8 @@ export default class PermissionConnect extends Component { ( - this.selectAccounts(addresses)} - selectNewAccountViaModal={(handleAccountClick) => { - showNewAccountModal({ - onCreateNewAccount: (address) => - handleAccountClick(address), - newAccountNumber, - }); - }} - addressLastConnectedMap={addressLastConnectedMap} - cancelPermissionsRequest={(requestId) => - this.cancelPermissionsRequest(requestId) - } - permissionsRequestId={permissionsRequestId} - selectedAccountAddresses={selectedAccountAddresses} - targetSubjectMetadata={targetSubjectMetadata} - /> - )} - /> - - process.env.CHAIN_PERMISSIONS && !permissionsRequest?.diff ? ( + process.env.CHAIN_PERMISSIONS ? ( this.cancelPermissionsRequest(requestId) @@ -378,31 +350,58 @@ export default class PermissionConnect extends Component { approveConnection={this.approveConnection} /> ) : ( - { - approvePermissionsRequest(...args); - this.redirect(true); + + this.selectAccounts(addresses) + } + selectNewAccountViaModal={(handleAccountClick) => { + showNewAccountModal({ + onCreateNewAccount: (address) => + handleAccountClick(address), + newAccountNumber, + }); }} - rejectPermissionsRequest={(requestId) => + addressLastConnectedMap={addressLastConnectedMap} + cancelPermissionsRequest={(requestId) => this.cancelPermissionsRequest(requestId) } - selectedAccounts={accounts.filter((account) => - selectedAccountAddresses.has(account.address), - )} + permissionsRequestId={permissionsRequestId} + selectedAccountAddresses={selectedAccountAddresses} targetSubjectMetadata={targetSubjectMetadata} - history={this.props.history} - connectPath={connectPath} - snapsInstallPrivacyWarningShown={ - snapsInstallPrivacyWarningShown - } - setSnapsInstallPrivacyWarningShownStatus={ - setSnapsInstallPrivacyWarningShownStatus - } /> ) } /> + ( + { + approvePermissionsRequest(...args); + this.redirect(true); + }} + rejectPermissionsRequest={(requestId) => + this.cancelPermissionsRequest(requestId) + } + selectedAccounts={accounts.filter((account) => + selectedAccountAddresses.has(account.address), + )} + targetSubjectMetadata={targetSubjectMetadata} + history={this.props.history} + connectPath={connectPath} + snapsInstallPrivacyWarningShown={ + snapsInstallPrivacyWarningShown + } + setSnapsInstallPrivacyWarningShownStatus={ + setSnapsInstallPrivacyWarningShownStatus + } + /> + )} + /> Date: Mon, 30 Sep 2024 22:39:09 +0200 Subject: [PATCH 028/226] feat: convert account tracker to typescript (#27231) --- app/scripts/controllers/mmi-controller.ts | 2 +- app/scripts/lib/account-tracker.test.js | 729 ---------------- app/scripts/lib/account-tracker.test.ts | 805 ++++++++++++++++++ ...{account-tracker.js => account-tracker.ts} | 326 ++++--- app/scripts/metamask-controller.js | 7 +- types/single-call-balance-checker-abi.d.ts | 6 + 6 files changed, 1028 insertions(+), 847 deletions(-) delete mode 100644 app/scripts/lib/account-tracker.test.js create mode 100644 app/scripts/lib/account-tracker.test.ts rename app/scripts/lib/{account-tracker.js => account-tracker.ts} (60%) create mode 100644 types/single-call-balance-checker-abi.d.ts diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index cbb08308ec59..0c43684d7f58 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -458,7 +458,7 @@ export default class MMIController extends EventEmitter { const allAccounts = await this.keyringController.getAccounts(); const accountsToTrack = [ - ...new Set( + ...new Set( oldAccounts.concat(allAccounts.map((a: string) => a.toLowerCase())), ), ]; diff --git a/app/scripts/lib/account-tracker.test.js b/app/scripts/lib/account-tracker.test.js deleted file mode 100644 index 4bd73a472811..000000000000 --- a/app/scripts/lib/account-tracker.test.js +++ /dev/null @@ -1,729 +0,0 @@ -import EventEmitter from 'events'; -import { ControllerMessenger } from '@metamask/base-controller'; - -import { flushPromises } from '../../../test/lib/timer-helpers'; -import { createTestProviderTools } from '../../../test/stub/provider'; -import AccountTracker from './account-tracker'; - -const noop = () => true; -const currentNetworkId = '5'; -const currentChainId = '0x5'; -const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; -const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; - -const SELECTED_ADDRESS = '0x123'; - -const INITIAL_BALANCE_1 = '0x1'; -const INITIAL_BALANCE_2 = '0x2'; -const UPDATE_BALANCE = '0xabc'; -const UPDATE_BALANCE_HOOK = '0xabcd'; - -const GAS_LIMIT = '0x111111'; -const GAS_LIMIT_HOOK = '0x222222'; - -// The below three values were generated by running MetaMask in the browser -// The response to eth_call, which is called via `ethContract.balances` -// in `_updateAccountsViaBalanceChecker` of account-tracker.js, needs to be properly -// formatted or else ethers will throw an error. -const ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN = - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800600000000000000000000000000000000000000000000000000000000000186a0'; -const EXPECTED_CONTRACT_BALANCE_1 = '0x038d7ea4c68006'; -const EXPECTED_CONTRACT_BALANCE_2 = '0x0186a0'; - -const mockAccounts = { - [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: INITIAL_BALANCE_1 }, - [VALID_ADDRESS_TWO]: { - address: VALID_ADDRESS_TWO, - balance: INITIAL_BALANCE_2, - }, -}; - -function buildMockBlockTracker({ shouldStubListeners = true } = {}) { - const blockTrackerStub = new EventEmitter(); - blockTrackerStub.getCurrentBlock = noop; - blockTrackerStub.getLatestBlock = noop; - if (shouldStubListeners) { - jest.spyOn(blockTrackerStub, 'addListener').mockImplementation(); - jest.spyOn(blockTrackerStub, 'removeListener').mockImplementation(); - } - return blockTrackerStub; -} - -function buildAccountTracker({ - completedOnboarding = false, - useMultiAccountBalanceChecker = false, - ...accountTrackerOptions -} = {}) { - const { provider } = createTestProviderTools({ - scaffold: { - eth_getBalance: UPDATE_BALANCE, - eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, - eth_getBlockByNumber: { gasLimit: GAS_LIMIT }, - }, - networkId: currentNetworkId, - chainId: currentNetworkId, - }); - const blockTrackerStub = buildMockBlockTracker(); - - const providerFromHook = createTestProviderTools({ - scaffold: { - eth_getBalance: UPDATE_BALANCE_HOOK, - eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, - eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, - }, - networkId: '0x1', - chainId: '0x1', - }).provider; - - const blockTrackerFromHookStub = buildMockBlockTracker(); - - const getNetworkClientByIdStub = jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub, - provider: providerFromHook, - }); - - const controllerMessenger = new ControllerMessenger(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => ({ - id: 'accountId', - address: SELECTED_ADDRESS, - }), - ); - - const accountTracker = new AccountTracker({ - provider, - blockTracker: blockTrackerStub, - getNetworkClientById: getNetworkClientByIdStub, - getNetworkIdentifier: jest.fn(), - preferencesController: { - store: { - getState: () => ({ - useMultiAccountBalanceChecker, - }), - subscribe: noop, - }, - }, - onboardingController: { - state: { - completedOnboarding, - }, - }, - controllerMessenger, - onAccountRemoved: noop, - getCurrentChainId: () => currentChainId, - ...accountTrackerOptions, - }); - - return { accountTracker, blockTrackerFromHookStub, blockTrackerStub }; -} - -describe('Account Tracker', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('start', () => { - it('restarts the subscription to the block tracker and update accounts', async () => { - const { accountTracker, blockTrackerStub } = buildAccountTracker(); - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.start(); - - expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( - 1, - 'latest', - expect.any(Function), - ); - expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( - 1, - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(1); // called first time with no args - - accountTracker.start(); - - expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( - 2, - 'latest', - expect.any(Function), - ); - expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( - 2, - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(2); // called second time with no args - - accountTracker.stop(); - }); - }); - - describe('stop', () => { - it('ends the subscription to the block tracker', async () => { - const { accountTracker, blockTrackerStub } = buildAccountTracker(); - - accountTracker.stop(); - - expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( - 1, - 'latest', - expect.any(Function), - ); - }); - }); - - describe('startPollingByNetworkClientId', () => { - it('should subscribe to the block tracker and update accounts if not already using the networkClientId', async () => { - const { accountTracker, blockTrackerFromHookStub } = - buildAccountTracker(); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledTimes(1); - expect(updateAccountsSpy).toHaveBeenCalledTimes(1); - - accountTracker.stopAllPolling(); - }); - - it('should subscribe to the block tracker and update accounts for each networkClientId', async () => { - const blockTrackerFromHookStub1 = buildMockBlockTracker(); - const blockTrackerFromHookStub2 = buildMockBlockTracker(); - const blockTrackerFromHookStub3 = buildMockBlockTracker(); - const getNetworkClientByIdStub = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'mainnet': - return { - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub1, - }; - case 'goerli': - return { - configuration: { - chainId: '0x5', - }, - blockTracker: blockTrackerFromHookStub2, - }; - case 'networkClientId1': - return { - configuration: { - chainId: '0xa', - }, - blockTracker: blockTrackerFromHookStub3, - }; - default: - throw new Error('unexpected networkClientId'); - } - }); - const { accountTracker } = buildAccountTracker({ - getNetworkClientById: getNetworkClientByIdStub, - }); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - expect(blockTrackerFromHookStub1.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - - accountTracker.startPollingByNetworkClientId('goerli'); - - expect(blockTrackerFromHookStub2.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('goerli'); - - accountTracker.startPollingByNetworkClientId('networkClientId1'); - - expect(blockTrackerFromHookStub3.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('networkClientId1'); - - accountTracker.stopAllPolling(); - }); - }); - - describe('stopPollingByPollingToken', () => { - it('should unsubscribe from the block tracker when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const { accountTracker, blockTrackerFromHookStub } = - buildAccountTracker(); - - jest.spyOn(accountTracker, 'updateAccounts').mockResolvedValue(); - - const pollingToken = - accountTracker.startPollingByNetworkClientId('mainnet'); - - accountTracker.stopPollingByPollingToken(pollingToken); - - expect(blockTrackerFromHookStub.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - }); - - it('should not unsubscribe from the block tracker if called with one of multiple active polling tokens for a given networkClient', async () => { - const { accountTracker, blockTrackerFromHookStub } = - buildAccountTracker(); - - jest.spyOn(accountTracker, 'updateAccounts').mockResolvedValue(); - - const pollingToken1 = - accountTracker.startPollingByNetworkClientId('mainnet'); - accountTracker.startPollingByNetworkClientId('mainnet'); - - accountTracker.stopPollingByPollingToken(pollingToken1); - - expect(blockTrackerFromHookStub.removeListener).not.toHaveBeenCalled(); - - accountTracker.stopAllPolling(); - }); - - it('should error if no pollingToken is passed', () => { - const { accountTracker } = buildAccountTracker(); - - expect(() => { - accountTracker.stopPollingByPollingToken(undefined); - }).toThrow('pollingToken required'); - }); - - it('should error if no matching pollingToken is found', () => { - const { accountTracker } = buildAccountTracker(); - - expect(() => { - accountTracker.stopPollingByPollingToken('potato'); - }).toThrow('pollingToken not found'); - }); - }); - - describe('stopAll', () => { - it('should end all subscriptions', async () => { - const blockTrackerFromHookStub1 = buildMockBlockTracker(); - const blockTrackerFromHookStub2 = buildMockBlockTracker(); - const getNetworkClientByIdStub = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'mainnet': - return { - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub1, - }; - case 'goerli': - return { - configuration: { - chainId: '0x5', - }, - blockTracker: blockTrackerFromHookStub2, - }; - default: - throw new Error('unexpected networkClientId'); - } - }); - const { accountTracker, blockTrackerStub } = buildAccountTracker({ - getNetworkClientById: getNetworkClientByIdStub, - }); - - jest.spyOn(accountTracker, 'updateAccounts').mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - accountTracker.startPollingByNetworkClientId('goerli'); - - accountTracker.stopAllPolling(); - - expect(blockTrackerStub.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(blockTrackerFromHookStub1.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(blockTrackerFromHookStub2.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - }); - }); - - describe('blockTracker "latest" events', () => { - it('updates currentBlockGasLimit, currentBlockGasLimitByChainId, and accounts when polling is initiated via `start`', async () => { - const blockTrackerStub = buildMockBlockTracker({ - shouldStubListeners: false, - }); - const { accountTracker } = buildAccountTracker({ - blockTracker: blockTrackerStub, - }); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.start(); - blockTrackerStub.emit('latest', 'blockNumber'); - - await flushPromises(); - - expect(updateAccountsSpy).toHaveBeenCalledWith(null); - - const newState = accountTracker.store.getState(); - - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: {}, - currentBlockGasLimit: GAS_LIMIT, - currentBlockGasLimitByChainId: { - [currentChainId]: GAS_LIMIT, - }, - }); - - accountTracker.stop(); - }); - - it('updates only the currentBlockGasLimitByChainId and accounts when polling is initiated via `startPollingByNetworkClientId`', async () => { - const blockTrackerFromHookStub = buildMockBlockTracker({ - shouldStubListeners: false, - }); - const providerFromHook = createTestProviderTools({ - scaffold: { - eth_getBalance: UPDATE_BALANCE_HOOK, - eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, - eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, - }, - networkId: '0x1', - chainId: '0x1', - }).provider; - const getNetworkClientByIdStub = jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub, - provider: providerFromHook, - }); - const { accountTracker } = buildAccountTracker({ - getNetworkClientById: getNetworkClientByIdStub, - }); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - blockTrackerFromHookStub.emit('latest', 'blockNumber'); - - await flushPromises(); - - expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - - const newState = accountTracker.store.getState(); - - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: {}, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: { - '0x1': GAS_LIMIT_HOOK, - }, - }); - - accountTracker.stopAllPolling(); - }); - }); - - describe('updateAccountsAllActiveNetworks', () => { - it('updates accounts for the globally selected network and all currently polling networks', async () => { - const { accountTracker } = buildAccountTracker(); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - await accountTracker.startPollingByNetworkClientId('networkClientId1'); - await accountTracker.startPollingByNetworkClientId('networkClientId2'); - await accountTracker.startPollingByNetworkClientId('networkClientId3'); - - expect(updateAccountsSpy).toHaveBeenCalledTimes(3); - - await accountTracker.updateAccountsAllActiveNetworks(); - - expect(updateAccountsSpy).toHaveBeenCalledTimes(7); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(4); // called with no args - expect(updateAccountsSpy).toHaveBeenNthCalledWith(5, 'networkClientId1'); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(6, 'networkClientId2'); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(7, 'networkClientId3'); - }); - }); - - describe('updateAccounts', () => { - it('does not update accounts if completedOnBoarding is false', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: false, - }); - - await accountTracker.updateAccounts(); - - const state = accountTracker.store.getState(); - expect(state).toStrictEqual({ - accounts: {}, - currentBlockGasLimit: '', - accountsByChainId: {}, - currentBlockGasLimitByChainId: {}, - }); - }); - - describe('chain does not have single call balance address', () => { - const getCurrentChainIdStub = () => '0x999'; // chain without single call balance address - const mockAccountsWithSelectedAddress = { - ...mockAccounts, - [SELECTED_ADDRESS]: { - address: SELECTED_ADDRESS, - balance: '0x0', - }, - }; - const mockInitialState = { - accounts: mockAccountsWithSelectedAddress, - accountsByChainId: { - '0x999': mockAccountsWithSelectedAddress, - }, - }; - - describe('when useMultiAccountBalanceChecker is true', () => { - it('updates all accounts directly', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: true, - useMultiAccountBalanceChecker: true, - getCurrentChainId: getCurrentChainIdStub, - }); - accountTracker.store.updateState(mockInitialState); - - await accountTracker.updateAccounts(); - - const accounts = { - [VALID_ADDRESS]: { - address: VALID_ADDRESS, - balance: UPDATE_BALANCE, - }, - [VALID_ADDRESS_TWO]: { - address: VALID_ADDRESS_TWO, - balance: UPDATE_BALANCE, - }, - [SELECTED_ADDRESS]: { - address: SELECTED_ADDRESS, - balance: UPDATE_BALANCE, - }, - }; - - const newState = accountTracker.store.getState(); - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - '0x999': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - - describe('when useMultiAccountBalanceChecker is false', () => { - it('updates only the selectedAddress directly, setting other balances to null', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: true, - useMultiAccountBalanceChecker: false, - getCurrentChainId: getCurrentChainIdStub, - }); - accountTracker.store.updateState(mockInitialState); - - await accountTracker.updateAccounts(); - - const accounts = { - [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: null }, - [VALID_ADDRESS_TWO]: { address: VALID_ADDRESS_TWO, balance: null }, - [SELECTED_ADDRESS]: { - address: SELECTED_ADDRESS, - balance: UPDATE_BALANCE, - }, - }; - - const newState = accountTracker.store.getState(); - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - '0x999': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - }); - - describe('chain does have single call balance address and network is not localhost', () => { - const getNetworkIdentifierStub = jest - .fn() - .mockReturnValue('http://not-localhost:8545'); - const controllerMessenger = new ControllerMessenger(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => ({ - id: 'accountId', - address: VALID_ADDRESS, - }), - ); - const getCurrentChainIdStub = () => '0x1'; // chain with single call balance address - const mockInitialState = { - accounts: { ...mockAccounts }, - accountsByChainId: { - '0x1': { ...mockAccounts }, - }, - }; - - describe('when useMultiAccountBalanceChecker is true', () => { - it('updates all accounts via balance checker', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: true, - useMultiAccountBalanceChecker: true, - controllerMessenger, - getNetworkIdentifier: getNetworkIdentifierStub, - getCurrentChainId: getCurrentChainIdStub, - }); - - accountTracker.store.updateState(mockInitialState); - - await accountTracker.updateAccounts('mainnet'); - - const accounts = { - [VALID_ADDRESS]: { - address: VALID_ADDRESS, - balance: EXPECTED_CONTRACT_BALANCE_1, - }, - [VALID_ADDRESS_TWO]: { - address: VALID_ADDRESS_TWO, - balance: EXPECTED_CONTRACT_BALANCE_2, - }, - }; - - const newState = accountTracker.store.getState(); - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - '0x1': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - }); - }); - - describe('onAccountRemoved', () => { - it('should remove an account from state', () => { - let accountRemovedListener; - const { accountTracker } = buildAccountTracker({ - onAccountRemoved: (callback) => { - accountRemovedListener = callback; - }, - }); - accountTracker.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, - }, - }, - }); - - accountRemovedListener(VALID_ADDRESS); - - const newState = accountTracker.store.getState(); - - const accounts = { - [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], - }; - - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - [currentChainId]: accounts, - '0x1': accounts, - '0x2': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - - describe('clearAccounts', () => { - it('should reset state', () => { - const { accountTracker } = buildAccountTracker(); - - accountTracker.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, - }, - }, - }); - - accountTracker.clearAccounts(); - - const newState = accountTracker.store.getState(); - - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: { - [currentChainId]: {}, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); -}); diff --git a/app/scripts/lib/account-tracker.test.ts b/app/scripts/lib/account-tracker.test.ts new file mode 100644 index 000000000000..7cc0dcba14c7 --- /dev/null +++ b/app/scripts/lib/account-tracker.test.ts @@ -0,0 +1,805 @@ +import EventEmitter from 'events'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { InternalAccount } from '@metamask/keyring-api'; +import { Hex } from '@metamask/utils'; +import { BlockTracker, Provider } from '@metamask/network-controller'; + +import { flushPromises } from '../../../test/lib/timer-helpers'; +import PreferencesController from '../controllers/preferences-controller'; +import OnboardingController from '../controllers/onboarding'; +import { createTestProviderTools } from '../../../test/stub/provider'; +import AccountTracker, { + AccountTrackerOptions, + AllowedActions, + AllowedEvents, + getDefaultAccountTrackerState, +} from './account-tracker'; + +const noop = () => true; +const currentNetworkId = '5'; +const currentChainId = '0x5'; +const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; +const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; + +const SELECTED_ADDRESS = '0x123'; + +const INITIAL_BALANCE_1 = '0x1'; +const INITIAL_BALANCE_2 = '0x2'; +const UPDATE_BALANCE = '0xabc'; +const UPDATE_BALANCE_HOOK = '0xabcd'; + +const GAS_LIMIT = '0x111111'; +const GAS_LIMIT_HOOK = '0x222222'; + +// The below three values were generated by running MetaMask in the browser +// The response to eth_call, which is called via `ethContract.balances` +// in `_updateAccountsViaBalanceChecker` of account-tracker.js, needs to be properly +// formatted or else ethers will throw an error. +const ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN = + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800600000000000000000000000000000000000000000000000000000000000186a0'; +const EXPECTED_CONTRACT_BALANCE_1 = '0x038d7ea4c68006'; +const EXPECTED_CONTRACT_BALANCE_2 = '0x0186a0'; + +const mockAccounts = { + [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: INITIAL_BALANCE_1 }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: INITIAL_BALANCE_2, + }, +}; + +class MockBlockTracker extends EventEmitter { + getCurrentBlock = noop; + + getLatestBlock = noop; +} + +function buildMockBlockTracker({ shouldStubListeners = true } = {}) { + const blockTrackerStub = new MockBlockTracker(); + if (shouldStubListeners) { + jest.spyOn(blockTrackerStub, 'addListener').mockImplementation(); + jest.spyOn(blockTrackerStub, 'removeListener').mockImplementation(); + } + return blockTrackerStub; +} + +type WithControllerOptions = { + completedOnboarding?: boolean; + useMultiAccountBalanceChecker?: boolean; + getNetworkClientById?: jest.Mock; + getSelectedAccount?: jest.Mock; +} & Partial; + +type WithControllerCallback = ({ + controller, + blockTrackerFromHookStub, + blockTrackerStub, + triggerOnAccountRemoved, +}: { + controller: AccountTracker; + blockTrackerFromHookStub: MockBlockTracker; + blockTrackerStub: MockBlockTracker; + triggerOnAccountRemoved: (address: string) => void; +}) => ReturnValue; + +type WithControllerArgs = + | [WithControllerCallback] + | [WithControllerOptions, WithControllerCallback]; + +function withController( + ...args: WithControllerArgs +): ReturnValue { + const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; + const { + completedOnboarding = false, + useMultiAccountBalanceChecker = false, + getNetworkClientById, + getSelectedAccount, + ...accountTrackerOptions + } = rest; + const { provider } = createTestProviderTools({ + scaffold: { + eth_getBalance: UPDATE_BALANCE, + eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, + eth_getBlockByNumber: { gasLimit: GAS_LIMIT }, + }, + networkId: currentNetworkId, + chainId: currentNetworkId, + }); + const blockTrackerStub = buildMockBlockTracker(); + + const controllerMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + const getSelectedAccountStub = () => + ({ + id: 'accountId', + address: SELECTED_ADDRESS, + } as InternalAccount); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + getSelectedAccount || getSelectedAccountStub, + ); + + const { provider: providerFromHook } = createTestProviderTools({ + scaffold: { + eth_getBalance: UPDATE_BALANCE_HOOK, + eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, + eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, + }, + networkId: '0x1', + chainId: '0x1', + }); + + const blockTrackerFromHookStub = buildMockBlockTracker(); + + const getNetworkClientByIdStub = jest.fn().mockReturnValue({ + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById || getNetworkClientByIdStub, + ); + + const controller = new AccountTracker({ + initState: getDefaultAccountTrackerState(), + provider: provider as Provider, + blockTracker: blockTrackerStub as unknown as BlockTracker, + getNetworkIdentifier: jest.fn(), + preferencesController: { + store: { + getState: () => ({ + useMultiAccountBalanceChecker, + }), + }, + } as PreferencesController, + onboardingController: { + state: { + completedOnboarding, + }, + } as OnboardingController, + controllerMessenger, + getCurrentChainId: () => currentChainId, + ...accountTrackerOptions, + }); + + return fn({ + controller, + blockTrackerFromHookStub, + blockTrackerStub, + triggerOnAccountRemoved: (address: string) => { + controllerMessenger.publish('KeyringController:accountRemoved', address); + }, + }); +} + +describe('Account Tracker', () => { + describe('start', () => { + it('restarts the subscription to the block tracker and update accounts', async () => { + withController(({ controller, blockTrackerStub }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.start(); + + expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( + 1, + 'latest', + expect.any(Function), + ); + expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( + 1, + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith(1); // called first time with no args + + controller.start(); + + expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( + 2, + 'latest', + expect.any(Function), + ); + expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( + 2, + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith(2); // called second time with no args + + controller.stop(); + }); + }); + }); + + describe('stop', () => { + it('ends the subscription to the block tracker', async () => { + withController(({ controller, blockTrackerStub }) => { + controller.stop(); + + expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( + 1, + 'latest', + expect.any(Function), + ); + }); + }); + }); + + describe('startPollingByNetworkClientId', () => { + it('should subscribe to the block tracker and update accounts if not already using the networkClientId', async () => { + withController(({ controller, blockTrackerFromHookStub }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledTimes(1); + expect(updateAccountsSpy).toHaveBeenCalledTimes(1); + + controller.stopAllPolling(); + }); + }); + + it('should subscribe to the block tracker and update accounts for each networkClientId', async () => { + const blockTrackerFromHookStub1 = buildMockBlockTracker(); + const blockTrackerFromHookStub2 = buildMockBlockTracker(); + const blockTrackerFromHookStub3 = buildMockBlockTracker(); + withController( + { + getNetworkClientById: jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub1, + }; + case 'goerli': + return { + configuration: { + chainId: '0x5', + }, + blockTracker: blockTrackerFromHookStub2, + }; + case 'networkClientId1': + return { + configuration: { + chainId: '0xa', + }, + blockTracker: blockTrackerFromHookStub3, + }; + default: + throw new Error('unexpected networkClientId'); + } + }), + }, + ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(blockTrackerFromHookStub1.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); + + controller.startPollingByNetworkClientId('goerli'); + + expect(blockTrackerFromHookStub2.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('goerli'); + + controller.startPollingByNetworkClientId('networkClientId1'); + + expect(blockTrackerFromHookStub3.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('networkClientId1'); + + controller.stopAllPolling(); + }, + ); + }); + }); + + describe('stopPollingByPollingToken', () => { + it('should unsubscribe from the block tracker when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { + withController(({ controller, blockTrackerFromHookStub }) => { + jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); + + const pollingToken = + controller.startPollingByNetworkClientId('mainnet'); + + controller.stopPollingByPollingToken(pollingToken); + + expect(blockTrackerFromHookStub.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + }); + }); + + it('should not unsubscribe from the block tracker if called with one of multiple active polling tokens for a given networkClient', async () => { + withController(({ controller, blockTrackerFromHookStub }) => { + jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); + + const pollingToken1 = + controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('mainnet'); + + controller.stopPollingByPollingToken(pollingToken1); + + expect(blockTrackerFromHookStub.removeListener).not.toHaveBeenCalled(); + + controller.stopAllPolling(); + }); + }); + + it('should error if no pollingToken is passed', () => { + withController(({ controller }) => { + expect(() => { + controller.stopPollingByPollingToken(undefined); + }).toThrow('pollingToken required'); + }); + }); + + it('should error if no matching pollingToken is found', () => { + withController(({ controller }) => { + expect(() => { + controller.stopPollingByPollingToken('potato'); + }).toThrow('pollingToken not found'); + }); + }); + }); + + describe('stopAll', () => { + it('should end all subscriptions', async () => { + const blockTrackerFromHookStub1 = buildMockBlockTracker(); + const blockTrackerFromHookStub2 = buildMockBlockTracker(); + const getNetworkClientByIdStub = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub1, + }; + case 'goerli': + return { + configuration: { + chainId: '0x5', + }, + blockTracker: blockTrackerFromHookStub2, + }; + default: + throw new Error('unexpected networkClientId'); + } + }); + withController( + { + getNetworkClientById: getNetworkClientByIdStub, + }, + ({ controller, blockTrackerStub }) => { + jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + controller.startPollingByNetworkClientId('goerli'); + + controller.stopAllPolling(); + + expect(blockTrackerStub.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(blockTrackerFromHookStub1.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(blockTrackerFromHookStub2.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + }, + ); + }); + }); + + describe('blockTracker "latest" events', () => { + it('updates currentBlockGasLimit, currentBlockGasLimitByChainId, and accounts when polling is initiated via `start`', async () => { + const blockTrackerStub = buildMockBlockTracker({ + shouldStubListeners: false, + }); + withController( + { + blockTracker: blockTrackerStub as unknown as BlockTracker, + }, + async ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.start(); + blockTrackerStub.emit('latest', 'blockNumber'); + + await flushPromises(); + + expect(updateAccountsSpy).toHaveBeenCalledWith(undefined); + + const newState = controller.store.getState(); + + expect(newState).toStrictEqual({ + accounts: {}, + accountsByChainId: {}, + currentBlockGasLimit: GAS_LIMIT, + currentBlockGasLimitByChainId: { + [currentChainId]: GAS_LIMIT, + }, + }); + + controller.stop(); + }, + ); + }); + + it('updates only the currentBlockGasLimitByChainId and accounts when polling is initiated via `startPollingByNetworkClientId`', async () => { + const blockTrackerFromHookStub = buildMockBlockTracker({ + shouldStubListeners: false, + }); + const providerFromHook = createTestProviderTools({ + scaffold: { + eth_getBalance: UPDATE_BALANCE_HOOK, + eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, + eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, + }, + networkId: '0x1', + chainId: '0x1', + }).provider; + const getNetworkClientByIdStub = jest.fn().mockReturnValue({ + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }); + withController( + { + getNetworkClientById: getNetworkClientByIdStub, + }, + async ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + blockTrackerFromHookStub.emit('latest', 'blockNumber'); + + await flushPromises(); + + expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); + + const newState = controller.store.getState(); + + expect(newState).toStrictEqual({ + accounts: {}, + accountsByChainId: {}, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: { + '0x1': GAS_LIMIT_HOOK, + }, + }); + + controller.stopAllPolling(); + }, + ); + }); + }); + + describe('updateAccountsAllActiveNetworks', () => { + it('updates accounts for the globally selected network and all currently polling networks', async () => { + withController(async ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + await controller.startPollingByNetworkClientId('networkClientId1'); + await controller.startPollingByNetworkClientId('networkClientId2'); + await controller.startPollingByNetworkClientId('networkClientId3'); + + expect(updateAccountsSpy).toHaveBeenCalledTimes(3); + + await controller.updateAccountsAllActiveNetworks(); + + expect(updateAccountsSpy).toHaveBeenCalledTimes(7); + expect(updateAccountsSpy).toHaveBeenNthCalledWith(4); // called with no args + expect(updateAccountsSpy).toHaveBeenNthCalledWith( + 5, + 'networkClientId1', + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith( + 6, + 'networkClientId2', + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith( + 7, + 'networkClientId3', + ); + }); + }); + }); + + describe('updateAccounts', () => { + it('does not update accounts if completedOnBoarding is false', async () => { + withController( + { + completedOnboarding: false, + }, + async ({ controller }) => { + await controller.updateAccounts(); + + const state = controller.store.getState(); + expect(state).toStrictEqual({ + accounts: {}, + currentBlockGasLimit: '', + accountsByChainId: {}, + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + + describe('chain does not have single call balance address', () => { + const getCurrentChainIdStub: () => Hex = () => '0x999'; // chain without single call balance address + const mockAccountsWithSelectedAddress = { + ...mockAccounts, + [SELECTED_ADDRESS]: { + address: SELECTED_ADDRESS, + balance: '0x0', + }, + }; + const mockInitialState = { + accounts: mockAccountsWithSelectedAddress, + accountsByChainId: { + '0x999': mockAccountsWithSelectedAddress, + }, + }; + + describe('when useMultiAccountBalanceChecker is true', () => { + it('updates all accounts directly', async () => { + withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: true, + getCurrentChainId: getCurrentChainIdStub, + }, + async ({ controller }) => { + controller.store.updateState(mockInitialState); + + await controller.updateAccounts(); + + const accounts = { + [VALID_ADDRESS]: { + address: VALID_ADDRESS, + balance: UPDATE_BALANCE, + }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: UPDATE_BALANCE, + }, + [SELECTED_ADDRESS]: { + address: SELECTED_ADDRESS, + balance: UPDATE_BALANCE, + }, + }; + + const newState = controller.store.getState(); + expect(newState).toStrictEqual({ + accounts, + accountsByChainId: { + '0x999': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + + describe('when useMultiAccountBalanceChecker is false', () => { + it('updates only the selectedAddress directly, setting other balances to null', async () => { + withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: false, + getCurrentChainId: getCurrentChainIdStub, + }, + async ({ controller }) => { + controller.store.updateState(mockInitialState); + + await controller.updateAccounts(); + + const accounts = { + [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: null }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: null, + }, + [SELECTED_ADDRESS]: { + address: SELECTED_ADDRESS, + balance: UPDATE_BALANCE, + }, + }; + + const newState = controller.store.getState(); + expect(newState).toStrictEqual({ + accounts, + accountsByChainId: { + '0x999': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + }); + + describe('chain does have single call balance address and network is not localhost', () => { + describe('when useMultiAccountBalanceChecker is true', () => { + it('updates all accounts via balance checker', async () => { + withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: true, + getNetworkIdentifier: jest + .fn() + .mockReturnValue('http://not-localhost:8545'), + getCurrentChainId: () => '0x1', // chain with single call balance address + getSelectedAccount: jest.fn().mockReturnValue({ + id: 'accountId', + address: VALID_ADDRESS, + } as InternalAccount), + }, + async ({ controller }) => { + controller.store.updateState({ + accounts: { ...mockAccounts }, + accountsByChainId: { + '0x1': { ...mockAccounts }, + }, + }); + + await controller.updateAccounts('mainnet'); + + const accounts = { + [VALID_ADDRESS]: { + address: VALID_ADDRESS, + balance: EXPECTED_CONTRACT_BALANCE_1, + }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: EXPECTED_CONTRACT_BALANCE_2, + }, + }; + + const newState = controller.store.getState(); + expect(newState).toStrictEqual({ + accounts, + accountsByChainId: { + '0x1': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + }); + }); + + describe('onAccountRemoved', () => { + it('should remove an account from state', () => { + withController(({ controller, triggerOnAccountRemoved }) => { + controller.store.updateState({ + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, + }, + }); + + triggerOnAccountRemoved(VALID_ADDRESS); + + const newState = controller.store.getState(); + + const accounts = { + [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], + }; + + expect(newState).toStrictEqual({ + accounts, + accountsByChainId: { + [currentChainId]: accounts, + '0x1': accounts, + '0x2': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }); + }); + }); + + describe('clearAccounts', () => { + it('should reset state', () => { + withController(({ controller }) => { + controller.store.updateState({ + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, + }, + }); + + controller.clearAccounts(); + + const newState = controller.store.getState(); + + expect(newState).toStrictEqual({ + accounts: {}, + accountsByChainId: { + [currentChainId]: {}, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }); + }); + }); +}); diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.ts similarity index 60% rename from app/scripts/lib/account-tracker.js rename to app/scripts/lib/account-tracker.ts index 6dbd13f1c2df..8ca119ccf83f 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.ts @@ -17,52 +17,127 @@ import { Web3Provider } from '@ethersproject/providers'; import { Contract } from '@ethersproject/contracts'; import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; import { cloneDeep } from 'lodash'; +import { + BlockTracker, + NetworkClientConfiguration, + NetworkClientId, + NetworkControllerGetNetworkClientByIdAction, + Provider, +} from '@metamask/network-controller'; +import { hasProperty, Hex } from '@metamask/utils'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { + AccountsControllerGetSelectedAccountAction, + AccountsControllerSelectedEvmAccountChangeEvent, +} from '@metamask/accounts-controller'; +import { KeyringControllerAccountRemovedEvent } from '@metamask/keyring-controller'; +import { InternalAccount } from '@metamask/keyring-api'; + +import OnboardingController, { + OnboardingControllerStateChangeEvent, +} from '../controllers/onboarding'; +import PreferencesController from '../controllers/preferences-controller'; import { LOCALHOST_RPC_URL } from '../../../shared/constants/network'; - import { SINGLE_CALL_BALANCES_ADDRESSES } from '../constants/contracts'; import { previousValueComparator } from './util'; +type Account = { + address: string; + balance: string | null; +}; + +export type AccountTrackerState = { + accounts: Record>; + currentBlockGasLimit: string; + accountsByChainId: Record; + currentBlockGasLimitByChainId: Record; +}; + +export const getDefaultAccountTrackerState = (): AccountTrackerState => ({ + accounts: {}, + currentBlockGasLimit: '', + accountsByChainId: {}, + currentBlockGasLimitByChainId: {}, +}); + +export type AllowedActions = + | AccountsControllerGetSelectedAccountAction + | NetworkControllerGetNetworkClientByIdAction; + +export type AllowedEvents = + | AccountsControllerSelectedEvmAccountChangeEvent + | KeyringControllerAccountRemovedEvent + | OnboardingControllerStateChangeEvent; + +export type AccountTrackerOptions = { + initState: Partial; + provider: Provider; + blockTracker: BlockTracker; + getCurrentChainId: () => Hex; + getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; + preferencesController: PreferencesController; + onboardingController: OnboardingController; + controllerMessenger: ControllerMessenger; +}; + /** * This module is responsible for tracking any number of accounts and caching their current balances & transaction * counts. * * It also tracks transaction hashes, and checks their inclusion status on each new block. * - * @typedef {object} AccountTracker - * @property {object} store The stored object containing all accounts to track, as well as the current block's gas limit. - * @property {object} store.accounts The accounts currently stored in this AccountTracker - * @property {object} store.accountsByChainId The accounts currently stored in this AccountTracker keyed by chain id - * @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block - * @property {string} store.currentBlockGasLimitByChainId A hex string indicating the gas limit of the current block keyed by chain id + * AccountTracker + * + * @property store The stored object containing all accounts to track, as well as the current block's gas limit. + * @property store.accounts The accounts currently stored in this AccountTracker + * @property store.accountsByChainId The accounts currently stored in this AccountTracker keyed by chain id + * @property store.currentBlockGasLimit A hex string indicating the gas limit of the current block + * @property store.currentBlockGasLimitByChainId A hex string indicating the gas limit of the current block keyed by chain id */ export default class AccountTracker { /** - * @param {object} opts - Options for initializing the controller - * @param {object} opts.provider - An EIP-1193 provider instance that uses the current global network - * @param {object} opts.blockTracker - A block tracker, which emits events for each new block - * @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network - * @param {Function} opts.getNetworkClientById - Gets the network client with the given id from the NetworkController. - * @param {Function} opts.getNetworkIdentifier - A function that returns the current network or passed nework configuration - * @param {Function} opts.onAccountRemoved - Allows subscribing to keyring controller accountRemoved event + * Observable store containing controller data. */ - #pollingTokenSets = new Map(); + store: ObservableStore; - #listeners = {}; + resetState: () => void; - #provider = null; + #pollingTokenSets = new Map>(); - #blockTracker = null; + #listeners: Record Promise> = + {}; - #currentBlockNumberByChainId = {}; + #provider: Provider; - constructor(opts = {}) { - const initState = { - accounts: {}, - currentBlockGasLimit: '', - accountsByChainId: {}, - currentBlockGasLimitByChainId: {}, - }; - this.store = new ObservableStore({ ...initState, ...opts.initState }); + #blockTracker: BlockTracker; + + #currentBlockNumberByChainId: Record = {}; + + #getCurrentChainId: AccountTrackerOptions['getCurrentChainId']; + + #getNetworkIdentifier: AccountTrackerOptions['getNetworkIdentifier']; + + #preferencesController: AccountTrackerOptions['preferencesController']; + + #onboardingController: AccountTrackerOptions['onboardingController']; + + #controllerMessenger: AccountTrackerOptions['controllerMessenger']; + + #selectedAccount: InternalAccount; + + /** + * @param opts - Options for initializing the controller + * @param opts.provider - An EIP-1193 provider instance that uses the current global network + * @param opts.blockTracker - A block tracker, which emits events for each new block + * @param opts.getCurrentChainId - A function that returns the `chainId` for the current global network + * @param opts.getNetworkIdentifier - A function that returns the current network or passed nework configuration + */ + constructor(opts: AccountTrackerOptions) { + const initState = getDefaultAccountTrackerState(); + this.store = new ObservableStore({ + ...initState, + ...opts.initState, + }); this.resetState = () => { this.store.updateState(initState); @@ -71,17 +146,19 @@ export default class AccountTracker { this.#provider = opts.provider; this.#blockTracker = opts.blockTracker; - this.getCurrentChainId = opts.getCurrentChainId; - this.getNetworkClientById = opts.getNetworkClientById; - this.getNetworkIdentifier = opts.getNetworkIdentifier; - this.preferencesController = opts.preferencesController; - this.onboardingController = opts.onboardingController; - this.controllerMessenger = opts.controllerMessenger; + this.#getCurrentChainId = opts.getCurrentChainId; + this.#getNetworkIdentifier = opts.getNetworkIdentifier; + this.#preferencesController = opts.preferencesController; + this.#onboardingController = opts.onboardingController; + this.#controllerMessenger = opts.controllerMessenger; // subscribe to account removal - opts.onAccountRemoved((address) => this.removeAccounts([address])); + this.#controllerMessenger.subscribe( + 'KeyringController:accountRemoved', + (address) => this.removeAccounts([address]), + ); - this.controllerMessenger.subscribe( + this.#controllerMessenger.subscribe( 'OnboardingController:stateChange', previousValueComparator((prevState, currState) => { const { completedOnboarding: prevCompletedOnboarding } = prevState; @@ -89,24 +166,25 @@ export default class AccountTracker { if (!prevCompletedOnboarding && currCompletedOnboarding) { this.updateAccountsAllActiveNetworks(); } - }, this.onboardingController.state), + return true; + }, this.#onboardingController.state), ); - this.selectedAccount = this.controllerMessenger.call( + this.#selectedAccount = this.#controllerMessenger.call( 'AccountsController:getSelectedAccount', ); - this.controllerMessenger.subscribe( + this.#controllerMessenger.subscribe( 'AccountsController:selectedEvmAccountChange', (newAccount) => { const { useMultiAccountBalanceChecker } = - this.preferencesController.store.getState(); + this.#preferencesController.store.getState(); if ( - this.selectedAccount.id !== newAccount.id && + this.#selectedAccount.id !== newAccount.id && !useMultiAccountBalanceChecker ) { - this.selectedAccount = newAccount; + this.#selectedAccount = newAccount; this.updateAccountsAllActiveNetworks(); } }, @@ -116,13 +194,14 @@ export default class AccountTracker { /** * Starts polling with global selected network */ - start() { + start(): void { // blockTracker.currentBlock may be null this.#currentBlockNumberByChainId = { - [this.getCurrentChainId()]: this.#blockTracker.getCurrentBlock(), + [this.#getCurrentChainId()]: this.#blockTracker.getCurrentBlock(), }; this.#blockTracker.once('latest', (blockNumber) => { - this.#currentBlockNumberByChainId[this.getCurrentChainId()] = blockNumber; + this.#currentBlockNumberByChainId[this.#getCurrentChainId()] = + blockNumber; }); // remove first to avoid double add @@ -136,7 +215,7 @@ export default class AccountTracker { /** * Stops polling with global selected network */ - stop() { + stop(): void { // remove listener this.#blockTracker.removeListener('latest', this.#updateForBlock); } @@ -148,22 +227,31 @@ export default class AccountTracker { * @param networkClientId - Optional networkClientId to fetch a network client with * @returns network client config */ - #getCorrectNetworkClient(networkClientId) { + #getCorrectNetworkClient(networkClientId?: NetworkClientId): { + chainId: Hex; + provider: Provider; + blockTracker: BlockTracker; + identifier: string; + } { if (networkClientId) { - const networkClient = this.getNetworkClientById(networkClientId); + const { configuration, provider, blockTracker } = + this.#controllerMessenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); return { - chainId: networkClient.configuration.chainId, - provider: networkClient.provider, - blockTracker: networkClient.blockTracker, - identifier: this.getNetworkIdentifier(networkClient.configuration), + chainId: configuration.chainId, + provider, + blockTracker, + identifier: this.#getNetworkIdentifier(configuration), }; } return { - chainId: this.getCurrentChainId(), + chainId: this.#getCurrentChainId(), provider: this.#provider, blockTracker: this.#blockTracker, - identifier: this.getNetworkIdentifier(), + identifier: this.#getNetworkIdentifier(), }; } @@ -173,14 +261,14 @@ export default class AccountTracker { * @param networkClientId - The networkClientId to start polling for * @returns pollingToken */ - startPollingByNetworkClientId(networkClientId) { + startPollingByNetworkClientId(networkClientId: NetworkClientId): string { const pollToken = random(); const pollingTokenSet = this.#pollingTokenSets.get(networkClientId); if (pollingTokenSet) { pollingTokenSet.add(pollToken); } else { - const set = new Set(); + const set = new Set(); set.add(pollToken); this.#pollingTokenSets.set(networkClientId, set); this.#subscribeWithNetworkClientId(networkClientId); @@ -191,7 +279,7 @@ export default class AccountTracker { /** * Stops polling for all networkClientIds */ - stopAllPolling() { + stopAllPolling(): void { this.stop(); this.#pollingTokenSets.forEach((tokenSet, _networkClientId) => { tokenSet.forEach((token) => { @@ -205,7 +293,7 @@ export default class AccountTracker { * * @param pollingToken - The polling token to stop polling for */ - stopPollingByPollingToken(pollingToken) { + stopPollingByPollingToken(pollingToken: string | undefined): void { if (!pollingToken) { throw new Error('pollingToken required'); } @@ -228,9 +316,9 @@ export default class AccountTracker { /** * Subscribes from the block tracker for the given networkClientId if not currently subscribed * - * @param {string} networkClientId - network client ID to fetch a block tracker with + * @param networkClientId - network client ID to fetch a block tracker with */ - #subscribeWithNetworkClientId(networkClientId) { + #subscribeWithNetworkClientId(networkClientId: NetworkClientId): void { if (this.#listeners[networkClientId]) { return; } @@ -249,9 +337,9 @@ export default class AccountTracker { /** * Unsubscribes from the block tracker for the given networkClientId if currently subscribed * - * @param {string} networkClientId - The network client ID to fetch a block tracker with + * @param networkClientId - The network client ID to fetch a block tracker with */ - #unsubscribeWithNetworkClientId(networkClientId) { + #unsubscribeWithNetworkClientId(networkClientId: NetworkClientId): void { if (!this.#listeners[networkClientId]) { return; } @@ -265,16 +353,15 @@ export default class AccountTracker { * Returns the accounts object for the chain ID, or initializes it from the globally selected * if it doesn't already exist. * - * @private - * @param {string} chainId - The chain ID + * @param chainId - The chain ID */ - #getAccountsForChainId(chainId) { + #getAccountsForChainId(chainId: Hex): AccountTrackerState['accounts'] { const { accounts, accountsByChainId } = this.store.getState(); if (accountsByChainId[chainId]) { return cloneDeep(accountsByChainId[chainId]); } - const newAccounts = {}; + const newAccounts: AccountTrackerState['accounts'] = {}; Object.keys(accounts).forEach((address) => { newAccounts[address] = {}; }); @@ -288,21 +375,21 @@ export default class AccountTracker { * Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each * of these accounts are given an updated balance via EthQuery. * - * @param {Array} addresses - The array of hex addresses for accounts with which this AccountTracker's accounts should be + * @param addresses - The array of hex addresses for accounts with which this AccountTracker's accounts should be * in sync */ - syncWithAddresses(addresses) { + syncWithAddresses(addresses: string[]): void { const { accounts } = this.store.getState(); const locals = Object.keys(accounts); - const accountsToAdd = []; + const accountsToAdd: string[] = []; addresses.forEach((upstream) => { if (!locals.includes(upstream)) { accountsToAdd.push(upstream); } }); - const accountsToRemove = []; + const accountsToRemove: string[] = []; locals.forEach((local) => { if (!addresses.includes(local)) { accountsToRemove.push(local); @@ -317,9 +404,9 @@ export default class AccountTracker { * Adds new addresses to track the balances of * given a balance as long this.#currentBlockNumberByChainId is defined for the chainId. * - * @param {Array} addresses - An array of hex addresses of new accounts to track + * @param addresses - An array of hex addresses of new accounts to track */ - addAccounts(addresses) { + addAccounts(addresses: string[]): void { const { accounts: _accounts, accountsByChainId: _accountsByChainId } = this.store.getState(); const accounts = cloneDeep(_accounts); @@ -338,7 +425,7 @@ export default class AccountTracker { this.store.updateState({ accounts, accountsByChainId }); // fetch balances for the accounts if there is block number ready - if (this.#currentBlockNumberByChainId[this.getCurrentChainId()]) { + if (this.#currentBlockNumberByChainId[this.#getCurrentChainId()]) { this.updateAccounts(); } this.#pollingTokenSets.forEach((_tokenSet, networkClientId) => { @@ -352,9 +439,9 @@ export default class AccountTracker { /** * Removes accounts from being tracked * - * @param {Array} addresses - An array of hex addresses to stop tracking. + * @param addresses - An array of hex addresses to stop tracking. */ - removeAccounts(addresses) { + removeAccounts(addresses: string[]): void { const { accounts: _accounts, accountsByChainId: _accountsByChainId } = this.store.getState(); const accounts = cloneDeep(_accounts); @@ -376,11 +463,11 @@ export default class AccountTracker { /** * Removes all addresses and associated balances */ - clearAccounts() { + clearAccounts(): void { this.store.updateState({ accounts: {}, accountsByChainId: { - [this.getCurrentChainId()]: {}, + [this.#getCurrentChainId()]: {}, }, }); } @@ -390,11 +477,11 @@ export default class AccountTracker { * each local account's balance via EthQuery * * @private - * @param {number} blockNumber - the block number to update to. + * @param blockNumber - the block number to update to. * @fires 'block' The updated state, if all account updates are successful */ - #updateForBlock = async (blockNumber) => { - await this.#updateForBlockByNetworkClientId(null, blockNumber); + #updateForBlock = async (blockNumber: string): Promise => { + await this.#updateForBlockByNetworkClientId(undefined, blockNumber); }; /** @@ -402,11 +489,14 @@ export default class AccountTracker { * via EthQuery * * @private - * @param {string} networkClientId - optional network client ID to use instead of the globally selected network. - * @param {number} blockNumber - the block number to update to. + * @param networkClientId - optional network client ID to use instead of the globally selected network. + * @param blockNumber - the block number to update to. * @fires 'block' The updated state, if all account updates are successful */ - async #updateForBlockByNetworkClientId(networkClientId, blockNumber) { + async #updateForBlockByNetworkClientId( + networkClientId: NetworkClientId | undefined, + blockNumber: string, + ): Promise { const { chainId, provider } = this.#getCorrectNetworkClient(networkClientId); this.#currentBlockNumberByChainId[chainId] = blockNumber; @@ -422,7 +512,7 @@ export default class AccountTracker { const currentBlockGasLimit = currentBlock.gasLimit; const { currentBlockGasLimitByChainId } = this.store.getState(); this.store.updateState({ - ...(chainId === this.getCurrentChainId() && { + ...(chainId === this.#getCurrentChainId() && { currentBlockGasLimit, }), currentBlockGasLimitByChainId: { @@ -442,9 +532,8 @@ export default class AccountTracker { * Updates accounts for the globally selected network * and all networks that are currently being polled. * - * @returns {Promise} after all account balances updated */ - async updateAccountsAllActiveNetworks() { + async updateAccountsAllActiveNetworks(): Promise { await this.updateAccounts(); await Promise.all( Array.from(this.#pollingTokenSets).map(([networkClientId]) => { @@ -457,11 +546,10 @@ export default class AccountTracker { * balanceChecker is deployed on main eth (test)nets and requires a single call * for all other networks, calls this.#updateAccount for each account in this.store * - * @param {string} networkClientId - optional network client ID to use instead of the globally selected network. - * @returns {Promise} after all account balances updated + * @param networkClientId - optional network client ID to use instead of the globally selected network. */ - async updateAccounts(networkClientId) { - const { completedOnboarding } = this.onboardingController.state; + async updateAccounts(networkClientId?: NetworkClientId): Promise { + const { completedOnboarding } = this.#onboardingController.state; if (!completedOnboarding) { return; } @@ -469,7 +557,7 @@ export default class AccountTracker { const { chainId, provider, identifier } = this.#getCorrectNetworkClient(networkClientId); const { useMultiAccountBalanceChecker } = - this.preferencesController.store.getState(); + this.#preferencesController.store.getState(); let addresses = []; if (useMultiAccountBalanceChecker) { @@ -477,7 +565,7 @@ export default class AccountTracker { addresses = Object.keys(accounts); } else { - const selectedAddress = this.controllerMessenger.call( + const selectedAddress = this.#controllerMessenger.call( 'AccountsController:getSelectedAccount', ).address; @@ -485,7 +573,10 @@ export default class AccountTracker { } const rpcUrl = 'http://127.0.0.1:8545'; - const singleCallBalancesAddress = SINGLE_CALL_BALANCES_ADDRESSES[chainId]; + const singleCallBalancesAddress = + SINGLE_CALL_BALANCES_ADDRESSES[ + chainId as keyof typeof SINGLE_CALL_BALANCES_ADDRESSES + ]; if ( identifier === LOCALHOST_RPC_URL || identifier === rpcUrl || @@ -510,15 +601,18 @@ export default class AccountTracker { * Updates the current balance of an account. * * @private - * @param {string} address - A hex address of a the account to be updated - * @param {object} provider - The provider instance to fetch the balance with - * @param {string} chainId - The chain ID to update in state - * @returns {Promise} after the account balance is updated + * @param address - A hex address of a the account to be updated + * @param provider - The provider instance to fetch the balance with + * @param chainId - The chain ID to update in state */ - async #updateAccount(address, provider, chainId) { + async #updateAccount( + address: string, + provider: Provider, + chainId: Hex, + ): Promise { const { useMultiAccountBalanceChecker } = - this.preferencesController.store.getState(); + this.#preferencesController.store.getState(); let balance = '0x0'; @@ -526,7 +620,16 @@ export default class AccountTracker { try { balance = await pify(new EthQuery(provider)).getBalance(address); } catch (error) { - if (error.data?.request?.method !== 'eth_getBalance') { + if ( + error && + typeof error === 'object' && + hasProperty(error, 'data') && + error.data && + hasProperty(error.data, 'request') && + error.data.request && + hasProperty(error.data.request, 'method') && + error.data.request.method !== 'eth_getBalance' + ) { throw error; } } @@ -556,7 +659,7 @@ export default class AccountTracker { const { accountsByChainId } = this.store.getState(); this.store.updateState({ - ...(chainId === this.getCurrentChainId() && { + ...(chainId === this.#getCurrentChainId() && { accounts: newAccounts, }), accountsByChainId: { @@ -570,18 +673,17 @@ export default class AccountTracker { * Updates current address balances from balanceChecker deployed contract instance * * @private - * @param {Array} addresses - A hex addresses of a the accounts to be updated - * @param {string} deployedContractAddress - The contract address to fetch balances with - * @param {object} provider - The provider instance to fetch the balance with - * @param {string} chainId - The chain ID to update in state - * @returns {Promise} after the account balance is updated + * @param addresses - A hex addresses of a the accounts to be updated + * @param deployedContractAddress - The contract address to fetch balances with + * @param provider - The provider instance to fetch the balance with + * @param chainId - The chain ID to update in state */ async #updateAccountsViaBalanceChecker( - addresses, - deployedContractAddress, - provider, - chainId, - ) { + addresses: string[], + deployedContractAddress: string, + provider: Provider, + chainId: Hex, + ): Promise { const ethContract = await new Contract( deployedContractAddress, SINGLE_CALL_BALANCES_ABI, @@ -593,7 +695,7 @@ export default class AccountTracker { const balances = await ethContract.balances(addresses, ethBalance); const accounts = this.#getAccountsForChainId(chainId); - const newAccounts = {}; + const newAccounts: AccountTrackerState['accounts'] = {}; Object.keys(accounts).forEach((address) => { if (!addresses.includes(address)) { newAccounts[address] = { address, balance: null }; @@ -606,7 +708,7 @@ export default class AccountTracker { const { accountsByChainId } = this.store.getState(); this.store.updateState({ - ...(chainId === this.getCurrentChainId() && { + ...(chainId === this.#getCurrentChainId() && { accounts: newAccounts, }), accountsByChainId: { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 30112ee61a3c..c03b8802e22f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1672,17 +1672,14 @@ export default class MetamaskController extends EventEmitter { onboardingController: this.onboardingController, controllerMessenger: this.controllerMessenger.getRestricted({ name: 'AccountTracker', + allowedActions: ['AccountsController:getSelectedAccount'], allowedEvents: [ 'AccountsController:selectedEvmAccountChange', 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', ], - allowedActions: ['AccountsController:getSelectedAccount'], }), initState: { accounts: {} }, - onAccountRemoved: this.controllerMessenger.subscribe.bind( - this.controllerMessenger, - 'KeyringController:accountRemoved', - ), }); // start and stop polling for balances based on activeControllerConnections diff --git a/types/single-call-balance-checker-abi.d.ts b/types/single-call-balance-checker-abi.d.ts new file mode 100644 index 000000000000..ae42a6e98775 --- /dev/null +++ b/types/single-call-balance-checker-abi.d.ts @@ -0,0 +1,6 @@ +declare module 'single-call-balance-checker-abi' { + import { ContractInterface } from '@ethersproject/contracts'; + + const SINGLE_CALL_BALANCES_ABI: ContractInterface; + export default SINGLE_CALL_BALANCES_ABI; +} From 80b85f4cd95cc06c5aa7d1094b692a1105b31c33 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 1 Oct 2024 09:45:30 +0100 Subject: [PATCH 029/226] feat: Custom header for wallet initiated confirmations (#27391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The back button brings the user to the stepper for ERC20 token transfer. This PR includes creating a placeholder component for token transfer confirmations. It also includes unit tests. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27391?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3218 ## **Manual testing steps** 1. Enable redesign on developer options. 2. Initiate transfer from an erc20 token in the wallet. 3. The confirmation in the screenshot below should appear. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-09-25 at 11 14 03 ## **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 + .../confirmations/contract-interaction.ts | 40 ----- test/data/confirmations/helper.ts | 24 ++- .../confirmations/set-approval-for-all.ts | 27 +++ test/data/confirmations/token-approve.ts | 27 +++ test/data/confirmations/token-transfer.ts | 32 ++++ .../header/__snapshots__/header.test.tsx.snap | 164 ++++++++++++++++++ .../wallet-initiated-header.test.tsx.snap | 40 +++++ .../components/confirm/header/header.test.tsx | 21 +++ .../components/confirm/header/header.tsx | 18 ++ .../header/wallet-initiated-header.test.tsx | 21 +++ .../header/wallet-initiated-header.tsx | 103 +++++++++++ .../approve/hooks/use-received-token.test.ts | 6 +- .../components/confirm/info/info.tsx | 2 + .../token-transfer.test.tsx.snap | 3 + .../token-transfer/token-transfer.stories.tsx | 26 +++ .../token-transfer/token-transfer.test.tsx | 26 +++ .../info/token-transfer/token-transfer.tsx | 5 + .../components/confirm/nav/nav.tsx | 2 +- .../title/hooks/useCurrentSpendingCap.test.ts | 6 +- ui/pages/confirmations/utils/confirm.ts | 1 + 21 files changed, 542 insertions(+), 55 deletions(-) create mode 100644 test/data/confirmations/set-approval-for-all.ts create mode 100644 test/data/confirmations/token-approve.ts create mode 100644 test/data/confirmations/token-transfer.ts create mode 100644 ui/pages/confirmations/components/confirm/header/__snapshots__/wallet-initiated-header.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/header/wallet-initiated-header.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b42895983048..330573566f70 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4544,6 +4544,9 @@ "revealTheSeedPhrase": { "message": "Reveal seed phrase" }, + "review": { + "message": "Review" + }, "reviewAlert": { "message": "Review alert" }, diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 0556789ccdb2..507a27a48dc3 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -161,43 +161,3 @@ export const genUnapprovedContractInteractionConfirmation = ({ userFeeLevel: 'medium', verifiedOnBlockchain: false, } as SignatureRequestType); - -export const genUnapprovedApproveConfirmation = ({ - address = CONTRACT_INTERACTION_SENDER_ADDRESS, - chainId = CHAIN_ID, -}: { - address?: Hex; - chainId?: string; -} = {}) => ({ - ...genUnapprovedContractInteractionConfirmation({ chainId }), - txParams: { - from: address, - data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', - }, - type: TransactionType.tokenMethodApprove, -}); - -export const genUnapprovedSetApprovalForAllConfirmation = ({ - address = CONTRACT_INTERACTION_SENDER_ADDRESS, - chainId = CHAIN_ID, -}: { - address?: Hex; - chainId?: string; -} = {}) => ({ - ...genUnapprovedContractInteractionConfirmation({ chainId }), - txParams: { - from: address, - data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', - }, - type: TransactionType.tokenMethodSetApprovalForAll, -}); diff --git a/test/data/confirmations/helper.ts b/test/data/confirmations/helper.ts index 9eb8bb234768..6669c043d0ea 100644 --- a/test/data/confirmations/helper.ts +++ b/test/data/confirmations/helper.ts @@ -1,18 +1,17 @@ import { ApprovalType } from '@metamask/controller-utils'; import { merge } from 'lodash'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { Confirmation, SignatureRequestType, } from '../../../ui/pages/confirmations/types/confirm'; import mockState from '../mock-state.json'; -import { CHAIN_IDS } from '../../../shared/constants/network'; -import { - genUnapprovedApproveConfirmation, - genUnapprovedContractInteractionConfirmation, - genUnapprovedSetApprovalForAllConfirmation, -} from './contract-interaction'; +import { genUnapprovedContractInteractionConfirmation } from './contract-interaction'; import { unapprovedPersonalSignMsg } from './personal_sign'; +import { genUnapprovedSetApprovalForAllConfirmation } from './set-approval-for-all'; +import { genUnapprovedApproveConfirmation } from './token-approve'; +import { genUnapprovedTokenTransferConfirmation } from './token-transfer'; import { unapprovedTypedSignMsgV4 } from './typed_sign'; type RootState = { metamask: Record } & Record< @@ -183,3 +182,16 @@ export const getMockSetApprovalForAllConfirmState = () => { genUnapprovedSetApprovalForAllConfirmation({ chainId: '0x5' }), ); }; + +export const getMockTokenTransferConfirmState = ({ + isWalletInitiatedConfirmation = false, +}: { + isWalletInitiatedConfirmation?: boolean; +}) => { + return getMockConfirmStateForTransaction( + genUnapprovedTokenTransferConfirmation({ + chainId: '0x5', + isWalletInitiatedConfirmation, + }), + ); +}; diff --git a/test/data/confirmations/set-approval-for-all.ts b/test/data/confirmations/set-approval-for-all.ts new file mode 100644 index 000000000000..ca997f6212af --- /dev/null +++ b/test/data/confirmations/set-approval-for-all.ts @@ -0,0 +1,27 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { + CHAIN_ID, + CONTRACT_INTERACTION_SENDER_ADDRESS, + genUnapprovedContractInteractionConfirmation, +} from './contract-interaction'; + +export const genUnapprovedSetApprovalForAllConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + chainId = CHAIN_ID, +}: { + address?: Hex; + chainId?: string; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation({ chainId }), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodSetApprovalForAll, +}); diff --git a/test/data/confirmations/token-approve.ts b/test/data/confirmations/token-approve.ts new file mode 100644 index 000000000000..c77d59101a99 --- /dev/null +++ b/test/data/confirmations/token-approve.ts @@ -0,0 +1,27 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { + CHAIN_ID, + CONTRACT_INTERACTION_SENDER_ADDRESS, + genUnapprovedContractInteractionConfirmation, +} from './contract-interaction'; + +export const genUnapprovedApproveConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + chainId = CHAIN_ID, +}: { + address?: Hex; + chainId?: string; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation({ chainId }), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodApprove, +}); diff --git a/test/data/confirmations/token-transfer.ts b/test/data/confirmations/token-transfer.ts new file mode 100644 index 000000000000..22d0cb2d00b4 --- /dev/null +++ b/test/data/confirmations/token-transfer.ts @@ -0,0 +1,32 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { + CHAIN_ID, + CONTRACT_INTERACTION_SENDER_ADDRESS, + genUnapprovedContractInteractionConfirmation, +} from './contract-interaction'; + +export const genUnapprovedTokenTransferConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + chainId = CHAIN_ID, + isWalletInitiatedConfirmation = false, +}: { + address?: Hex; + chainId?: string; + isWalletInitiatedConfirmation?: boolean; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation({ chainId }), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodTransfer, + origin: isWalletInitiatedConfirmation + ? 'metamask' + : 'https://metamask.github.io', +}); 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 46bf53c2a7bc..1af0810d285f 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 @@ -113,6 +113,170 @@ 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 +

+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`Header should match snapshot with token transfer confirmation initiated in the wallet 1`] = ` +
+
+ +

+ Review +

+
+ +
+
+
+`; + exports[`Header should match snapshot with transaction confirmation 1`] = `
should match snapshot 1`] = ` +
+
+ +

+ Review +

+
+ +
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/header/header.test.tsx b/ui/pages/confirmations/components/confirm/header/header.test.tsx index c6b8481c01fc..841c0196ae29 100644 --- a/ui/pages/confirmations/components/confirm/header/header.test.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.test.tsx @@ -4,6 +4,7 @@ import { DefaultRootState } from 'react-redux'; import { getMockContractInteractionConfirmState, + getMockTokenTransferConfirmState, getMockTypedSignConfirmState, } from '../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; @@ -28,6 +29,26 @@ describe('Header', () => { expect(container).toMatchSnapshot(); }); + it('should match snapshot with token transfer confirmation initiated in a dApp', () => { + const { container } = render( + getMockTokenTransferConfirmState({ + isWalletInitiatedConfirmation: false, + }), + ); + + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with token transfer confirmation initiated in the wallet', () => { + const { container } = render( + getMockTokenTransferConfirmState({ + isWalletInitiatedConfirmation: true, + }), + ); + + expect(container).toMatchSnapshot(); + }); + it('contains network name and account name', () => { const { getByText } = render(); expect(getByText('Test Account')).toBeInTheDocument(); diff --git a/ui/pages/confirmations/components/confirm/header/header.tsx b/ui/pages/confirmations/components/confirm/header/header.tsx index 255384c58b82..9c113effe6a5 100644 --- a/ui/pages/confirmations/components/confirm/header/header.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.tsx @@ -1,3 +1,7 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; import React from 'react'; import { AvatarNetwork, @@ -14,15 +18,29 @@ import { TextVariant, } from '../../../../../helpers/constants/design-system'; import { getAvatarNetworkColor } from '../../../../../helpers/utils/accounts'; +import { useConfirmContext } from '../../../context/confirm'; import useConfirmationNetworkInfo from '../../../hooks/useConfirmationNetworkInfo'; import useConfirmationRecipientInfo from '../../../hooks/useConfirmationRecipientInfo'; +import { Confirmation } from '../../../types/confirm'; import HeaderInfo from './header-info'; +import { WalletInitiatedHeader } from './wallet-initiated-header'; const Header = () => { const { networkImageUrl, networkDisplayName } = useConfirmationNetworkInfo(); const { senderAddress: fromAddress, senderName: fromName } = useConfirmationRecipientInfo(); + const { currentConfirmation } = useConfirmContext(); + + if (currentConfirmation?.type === TransactionType.tokenMethodTransfer) { + const isWalletInitiated = + (currentConfirmation as TransactionMeta).origin === 'metamask'; + + if (isWalletInitiated) { + return ; + } + } + return ( { + const store = configureStore(state); + return renderWithConfirmContextProvider(, store); +}; + +describe('', () => { + it.only('should match snapshot', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx new file mode 100644 index 000000000000..c1bca06c74b0 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx @@ -0,0 +1,103 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { AssetType } from '../../../../../../shared/constants/transaction'; +import { + Box, + ButtonIcon, + ButtonIconSize, + IconName, + Text, +} from '../../../../../components/component-library'; +import { clearConfirmTransaction } from '../../../../../ducks/confirm-transaction/confirm-transaction.duck'; +import { editExistingTransaction } from '../../../../../ducks/send'; +import { + AlignItems, + BackgroundColor, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { SEND_ROUTE } from '../../../../../helpers/constants/routes'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { + setConfirmationAdvancedDetailsOpen, + showSendTokenPage, +} from '../../../../../store/actions'; +import { useConfirmContext } from '../../../context/confirm'; +import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; + +export const WalletInitiatedHeader = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const history = useHistory(); + + const { currentConfirmation } = useConfirmContext(); + + const showAdvancedDetails = useSelector( + selectConfirmationAdvancedDetailsOpen, + ); + + const setShowAdvancedDetails = (value: boolean): void => { + dispatch(setConfirmationAdvancedDetailsOpen(value)); + }; + + const handleBackButtonClick = useCallback(async () => { + const { id } = currentConfirmation; + + await dispatch(editExistingTransaction(AssetType.token, id.toString())); + dispatch(clearConfirmTransaction()); + dispatch(showSendTokenPage()); + + history.push(SEND_ROUTE); + }, [currentConfirmation, dispatch, history]); + + return ( + + + + {t('review')} + + + { + setShowAdvancedDetails(!showAdvancedDetails); + }} + /> + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts index 874e817cc20a..a6e92167e558 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts @@ -1,10 +1,8 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { - CONTRACT_INTERACTION_SENDER_ADDRESS, - genUnapprovedApproveConfirmation, -} from '../../../../../../../../test/data/confirmations/contract-interaction'; +import { CONTRACT_INTERACTION_SENDER_ADDRESS } from '../../../../../../../../test/data/confirmations/contract-interaction'; import { getMockConfirmStateForTransaction } from '../../../../../../../../test/data/confirmations/helper'; +import { genUnapprovedApproveConfirmation } from '../../../../../../../../test/data/confirmations/token-approve'; import { renderHookWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; import { useAccountTotalFiatBalance } from '../../../../../../../hooks/useAccountTotalFiatBalance'; import { useReceivedToken } from './use-received-token'; diff --git a/ui/pages/confirmations/components/confirm/info/info.tsx b/ui/pages/confirmations/components/confirm/info/info.tsx index 5a9c4757158e..3e87f4f7908c 100644 --- a/ui/pages/confirmations/components/confirm/info/info.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.tsx @@ -6,6 +6,7 @@ import ApproveInfo from './approve/approve'; import BaseTransactionInfo from './base-transaction-info/base-transaction-info'; import PersonalSignInfo from './personal-sign/personal-sign'; import SetApprovalForAllInfo from './set-approval-for-all-info/set-approval-for-all-info'; +import TokenTransferInfo from './token-transfer/token-transfer'; import TypedSignV1Info from './typed-sign-v1/typed-sign-v1'; import TypedSignInfo from './typed-sign/typed-sign'; @@ -29,6 +30,7 @@ const Info = () => { [TransactionType.tokenMethodIncreaseAllowance]: () => ApproveInfo, [TransactionType.tokenMethodSetApprovalForAll]: () => SetApprovalForAllInfo, + [TransactionType.tokenMethodTransfer]: () => TokenTransferInfo, }), [currentConfirmation], ); 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 new file mode 100644 index 000000000000..c3aa8e4e26ea --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenTransferInfo renders correctly 1`] = `
`; 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 new file mode 100644 index 000000000000..384a8f161e9b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import configureStore from '../../../../../../store/store'; +import { ConfirmContextProvider } from '../../../../context/confirm'; +import TokenTransferInfo from './token-transfer'; + +const store = configureStore(getMockTokenTransferConfirmState({})); + +const Story = { + title: 'Components/App/Confirm/info/TokenTransferInfo', + component: TokenTransferInfo, + decorators: [ + (story: () => any) => ( + + {story()} + + ), + ], +}; + +export default Story; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; 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 new file mode 100644 index 000000000000..186505ee7740 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.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 TokenTransferInfo from './token-transfer'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +describe('TokenTransferInfo', () => { + 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-transfer.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx new file mode 100644 index 000000000000..8da9493ebbc4 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx @@ -0,0 +1,5 @@ +const TokenTransferInfo = () => { + return null; +}; + +export default TokenTransferInfo; diff --git a/ui/pages/confirmations/components/confirm/nav/nav.tsx b/ui/pages/confirmations/components/confirm/nav/nav.tsx index 2fd394f18ae2..6546b882b784 100644 --- a/ui/pages/confirmations/components/confirm/nav/nav.tsx +++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx @@ -32,9 +32,9 @@ import { import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { pendingConfirmationsSortedSelector } from '../../../../../selectors'; import { rejectPendingApproval } from '../../../../../store/actions'; +import { useConfirmContext } from '../../../context/confirm'; import { useQueuedConfirmationsEvent } from '../../../hooks/useQueuedConfirmationEvents'; import { isSignatureApprovalRequest } from '../../../utils'; -import { useConfirmContext } from '../../../context/confirm'; const Nav = () => { const history = useHistory(); diff --git a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts index 40886608870b..9bea069d0935 100644 --- a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts +++ b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts @@ -1,8 +1,6 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { - CONTRACT_INTERACTION_SENDER_ADDRESS, - genUnapprovedApproveConfirmation, -} from '../../../../../../../test/data/confirmations/contract-interaction'; +import { CONTRACT_INTERACTION_SENDER_ADDRESS } from '../../../../../../../test/data/confirmations/contract-interaction'; +import { genUnapprovedApproveConfirmation } from '../../../../../../../test/data/confirmations/token-approve'; import mockState from '../../../../../../../test/data/mock-state.json'; import { renderHookWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import { useCurrentSpendingCap } from './useCurrentSpendingCap'; diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 8c2846b6b69a..41ffd2832169 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -26,6 +26,7 @@ export const REDESIGN_USER_TRANSACTION_TYPES = [ export const REDESIGN_DEV_TRANSACTION_TYPES = [ ...REDESIGN_USER_TRANSACTION_TYPES, + TransactionType.tokenMethodTransfer, ]; const SIGNATURE_APPROVAL_TYPES = [ From 9b50f596b706585d6075cacd90e1aeba9e4acf20 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Tue, 1 Oct 2024 09:57:14 +0100 Subject: [PATCH 030/226] fix: updated ui for connect and review page (#27478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to update the following UI changes in the connect and the new permissions page ## **Description** * Fix the Typography * Update background color on Connect Page * Don't show header on review permissions page ## **Related issues** Fixes: ## **Manual testing steps** 1. Run extension with `CHAIN_PERMISSIONS=1 yarn start` 2. Check the following changes in Connect Page: Background color is fixed, Typography looks aligned with design, Bottom learn more message 3. Fix Permissions Page: Typography is fixed, the list item has border radius and proper padding, three dot menu replaced by Edit button ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-09-30 at 3 53 23 AM](https://github.com/user-attachments/assets/98aeb9f2-df44-46af-b2f2-777002228c64) ![Screenshot 2024-09-30 at 3 53 09 AM](https://github.com/user-attachments/assets/ade555e9-fbb2-424b-bc15-31af8a50e1b4) ### **After** ## Connect Page ![Screenshot 2024-09-30 at 4 01 38 AM](https://github.com/user-attachments/assets/818b2ef1-bf43-4eb3-890f-8b45eb29c50b) ## Permission Page ![Screenshot 2024-09-30 at 3 52 38 AM](https://github.com/user-attachments/assets/dd488013-2cd8-41df-ba5f-681daf48df93) ## **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 | 8 + ...ite-cell-connection-list-item.test.js.snap | 7 +- .../site-cell-connection-list-item.js | 36 +-- .../site-cell-connection-list-item.test.js | 4 + .../site-cell/site-cell.tsx | 85 +++--- .../__snapshots__/connect-page.test.tsx.snap | 276 ++++++++++-------- .../connect-page/connect-page.test.tsx | 2 +- .../connect-page/connect-page.tsx | 64 ++-- ui/pages/permissions-connect/index.scss | 4 + ui/pages/routes/routes.component.js | 11 + 10 files changed, 294 insertions(+), 203 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 330573566f70..b880d92ad468 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1186,9 +1186,17 @@ "message": "Connected with" }, "connectedWithAccount": { + "message": "$1 accounts connected", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { "message": "Connected with $1", "description": "$1 represents account name" }, + "connectedWithNetworks": { + "message": "$1 networks connected", + "description": "$1 represents network length" + }, "connecting": { "message": "Connecting" }, diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap index 5dc31c8e210a..ae198ab79882 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap @@ -3,7 +3,7 @@ exports[`SiteCellConnectionListItem renders correctly with required props 1`] = `

Title

@@ -27,7 +27,7 @@ exports[`SiteCellConnectionListItem renders correctly with required props 1`] = class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" > Unconnected Message @@ -38,6 +38,7 @@ exports[`SiteCellConnectionListItem renders correctly with required props 1`] =
diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js index 85e50b0b0fed..be2aafb7257b 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js @@ -15,10 +15,7 @@ import { AvatarIcon, AvatarIconSize, Box, - ButtonIcon, - ButtonIconSize, ButtonLink, - IconName, Text, } from '../../../../component-library'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; @@ -31,6 +28,8 @@ export const SiteCellConnectionListItem = ({ isConnectFlow, onClick, content, + paddingTopValue, + paddingBottomValue, }) => { const t = useI18nContext(); @@ -42,9 +41,10 @@ export const SiteCellConnectionListItem = ({ alignItems={AlignItems.baseline} width={BlockSize.Full} backgroundColor={BackgroundColor.backgroundDefault} - padding={4} gap={4} className="multichain-connection-list-item" + paddingTop={paddingTopValue} + paddingBottom={paddingBottomValue} > - + {title} {isConnectFlow ? unconnectedMessage : connectedMessage} @@ -79,17 +79,9 @@ export const SiteCellConnectionListItem = ({ {content} - {isConnectFlow ? ( - onClick()}>{t('edit')} - ) : ( - onClick()} - size={ButtonIconSize.Sm} - /> - )} + onClick()} data-testid="edit"> + {t('edit')} + ); }; @@ -109,6 +101,16 @@ SiteCellConnectionListItem.propTypes = { */ connectedMessage: PropTypes.string, + /** + * Padding Top Value to keep the padding between list items same + */ + paddingTopValue: PropTypes.number, + + /** + * Padding Bottom Value to keep the padding between list items same + */ + paddingBottomValue: PropTypes.number, + /** * The message that should be displayed when there are no connected accounts */ diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js index 613f07f348f3..954d1e2d1fc2 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.test.js @@ -36,4 +36,8 @@ describe('SiteCellConnectionListItem', () => { it('returns wallet icon correctly', () => { expect(getByText('Title')).toBeDefined(); }); + + it('returns edit button correctly', () => { + expect(getByTestId('edit')).toBeDefined(); + }); }); 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 2ed1fce8fddd..4bc42604adf3 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 @@ -1,10 +1,15 @@ import React, { useState } from 'react'; import { Hex } from '@metamask/utils'; -import { BorderColor } from '../../../../../helpers/constants/design-system'; +import { + BackgroundColor, + BorderColor, + BorderRadius, +} from '../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { AvatarAccount, AvatarAccountSize, + Box, IconName, } from '../../../../component-library'; import { EditAccountsModal, EditNetworksModal } from '../../..'; @@ -55,13 +60,15 @@ export const SiteCell: React.FC = ({ selectedChainIds.includes(chainId), ); + const selectedChainIdsLength = selectedChainIds.length; + // Determine the messages for connected and not connected states const accountMessageConnectedState = selectedAccounts.length === 1 - ? t('connectedWithAccount', [ + ? t('connectedWithAccountName', [ selectedAccounts[0].label || selectedAccounts[0].metadata.name, ]) - : t('connectedWith'); + : t('connectedWithAccount', [accounts.length]); const accountMessageNotConnectedState = selectedAccounts.length === 1 ? t('requestingForAccount', [ @@ -71,36 +78,48 @@ export const SiteCell: React.FC = ({ return ( <> - setShowEditAccountsModal(true)} - content={ - // Why this difference? - selectedAccounts.length === 1 ? ( - - ) : ( - - ) - } - /> - setShowEditNetworksModal(true)} - content={} - /> - + + setShowEditAccountsModal(true)} + paddingBottomValue={2} + paddingTopValue={0} + content={ + // Why this difference? + selectedAccounts.length === 1 ? ( + + ) : ( + + ) + } + /> + setShowEditNetworksModal(true)} + paddingTopValue={2} + paddingBottomValue={0} + content={} + /> + {showEditAccountsModal && (
- -
-
-

- See your accounts and suggest transactions -

+
+
+

- Requesting for Test Account - + See your accounts and suggest transactions +

-
- -
-
-
- +
-

- Use your enabled networks -

+
+
+

- Requesting for - + Use your enabled networks +

Alerts"" - data-tooltipped="" - style="display: inline;" + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" > + + Requesting for +
Alerts"" + data-tooltipped="" + style="display: inline;" >
- G +
+ G +
-
-
- Custom Mainnet RPC logo +
+ 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 9440d5031334..d7c50c6aa501 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -69,7 +69,7 @@ describe('ConnectPage', () => { it('should render confirm and cancel button', () => { const { getByText } = render(); - const confirmButton = getByText('Confirm'); + const confirmButton = getByText('Connect'); const cancelButton = getByText('Cancel'); expect(confirmButton).toBeDefined(); expect(cancelButton).toBeDefined(); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index f332ba6cc07e..a30047fbd38a 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -24,13 +24,16 @@ import { } from '../../../components/multichain/pages/page'; import { SiteCell } from '../../../components/multichain/pages/review-permissions-page'; import { + BackgroundColor, BlockSize, Display, + FlexDirection, TextVariant, } from '../../../helpers/constants/design-system'; 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'; export type ConnectPageRequest = { id: string; @@ -97,14 +100,15 @@ export const ConnectPage: React.FC = ({ return ( -
+
{t('connectWithMetaMask')} {t('connectionDescription')}:
- + = ({ />
- - - + + + + + +
diff --git a/ui/pages/permissions-connect/index.scss b/ui/pages/permissions-connect/index.scss index 513809505d50..954ec7a1121c 100644 --- a/ui/pages/permissions-connect/index.scss +++ b/ui/pages/permissions-connect/index.scss @@ -44,4 +44,8 @@ justify-self: flex-end; font-weight: bold; } + + .connect-page { + background-color: var(--color-background-alternative); // main-container adds the width but overrides the boxProps. So, we need extra class to apply css + } } diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index a02ecfa32ef9..82361cb6b690 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -564,6 +564,17 @@ export default class Routes extends Component { return true; } + const isReviewPermissionsPgae = Boolean( + matchPath(location.pathname, { + path: REVIEW_PERMISSIONS, + exact: false, + }), + ); + + if (isReviewPermissionsPgae) { + return true; + } + if (windowType === ENVIRONMENT_TYPE_POPUP && this.onConfirmPage()) { return true; } From 8b3556394db3fb5a9fe0398fe747686351b20b22 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 1 Oct 2024 11:05:37 +0200 Subject: [PATCH 031/226] fix: Allow state updates in Snaps interfaces to state values that are falsy (#27488) ## **Description** Fixes a problem where state updates to Snaps interfaces would be ignored if the value was falsy, e.g. empty string. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27488?quickstart=1) ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/e6ea3cee-f8f3-4f0d-9c48-f47ab8456b71 ### **After** https://github.com/user-attachments/assets/6ebf5124-dc16-4f6d-8d6c-bf46ae00c998 --- ui/components/app/snaps/snap-ui-dropdown/snap-ui-dropdown.tsx | 2 +- ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx | 2 +- ui/components/app/snaps/snap-ui-selector/snap-ui-selector.tsx | 2 +- ui/contexts/snaps/snap-interface.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-dropdown/snap-ui-dropdown.tsx b/ui/components/app/snaps/snap-ui-dropdown/snap-ui-dropdown.tsx index c9000c13839b..f2cb85cc4ef0 100644 --- a/ui/components/app/snaps/snap-ui-dropdown/snap-ui-dropdown.tsx +++ b/ui/components/app/snaps/snap-ui-dropdown/snap-ui-dropdown.tsx @@ -34,7 +34,7 @@ export const SnapUIDropdown: FunctionComponent = ({ const [value, setValue] = useState(initialValue ?? ''); useEffect(() => { - if (initialValue) { + if (initialValue !== undefined && initialValue !== null) { setValue(initialValue); } }, [initialValue]); diff --git a/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx b/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx index b6f68c646ec5..fbb340d92889 100644 --- a/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx +++ b/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx @@ -22,7 +22,7 @@ export const SnapUIInput: FunctionComponent< const [value, setValue] = useState(initialValue ?? ''); useEffect(() => { - if (initialValue) { + if (initialValue !== undefined && initialValue !== null) { setValue(initialValue); } }, [initialValue]); diff --git a/ui/components/app/snaps/snap-ui-selector/snap-ui-selector.tsx b/ui/components/app/snaps/snap-ui-selector/snap-ui-selector.tsx index 49a13400bbf8..a0869ff0b46b 100644 --- a/ui/components/app/snaps/snap-ui-selector/snap-ui-selector.tsx +++ b/ui/components/app/snaps/snap-ui-selector/snap-ui-selector.tsx @@ -102,7 +102,7 @@ export const SnapUISelector: React.FunctionComponent = ({ const [isModalOpen, setIsModalOpen] = useState(false); useEffect(() => { - if (initialValue) { + if (initialValue !== undefined && initialValue !== null) { setSelectedOption(initialValue); } }, [initialValue]); diff --git a/ui/contexts/snaps/snap-interface.tsx b/ui/contexts/snaps/snap-interface.tsx index 25249d31420a..7e37485c3ea2 100644 --- a/ui/contexts/snaps/snap-interface.tsx +++ b/ui/contexts/snaps/snap-interface.tsx @@ -230,7 +230,7 @@ export const SnapInterfaceContextProvider: FunctionComponent< ? (initialState[form] as FormState)?.[name] : (initialState as FormState)?.[name]; - if (value) { + if (value !== undefined && value !== null) { return value; } From 7fa528628368d6ad9d15aa48c7090ff37084e190 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Tue, 1 Oct 2024 12:28:53 +0200 Subject: [PATCH 032/226] fix(snaps): Keep focus on input if interface re-renders (#27429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds a reference to the currently focused input in a snap interface and set the focus back to the last focused input if the interface re-renders. This fixes a problem where it will loose input focus and set it to the last input of the interface if an interface was re-rendered. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27429?quickstart=1) ## **Related issues** Fixes: #27424 ## **Manual testing steps** 1. Go to test-snaps 2. Use the send flow example snap 3. Try typing something in the "To address" field 4. The focus should stay on your input. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/4a15acda-f41b-4e32-bc71-dfceaa1920c9 ### **After** https://github.com/user-attachments/assets/80745da0-edbb-4ab1-8a32-d2b465a31aee ## **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/snaps/snap-ui-input/snap-ui-input.tsx | 23 +++++++++++++++++-- ui/contexts/snaps/snap-interface.tsx | 10 ++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx b/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx index fbb340d92889..51c1c9a54a7a 100644 --- a/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx +++ b/ui/components/app/snaps/snap-ui-input/snap-ui-input.tsx @@ -2,6 +2,7 @@ import React, { ChangeEvent, FunctionComponent, useEffect, + useRef, useState, } from 'react'; import { useSnapInterfaceContext } from '../../../../contexts/snaps'; @@ -15,7 +16,10 @@ export type SnapUIInputProps = { export const SnapUIInput: FunctionComponent< SnapUIInputProps & FormTextFieldProps<'div'> > = ({ name, form, ...props }) => { - const { handleInputChange, getValue } = useSnapInterfaceContext(); + const { handleInputChange, getValue, focusedInput, setCurrentFocusedInput } = + useSnapInterfaceContext(); + + const inputRef = useRef(null); const initialValue = getValue(name, form) as string; @@ -27,14 +31,29 @@ export const SnapUIInput: FunctionComponent< } }, [initialValue]); + /* + * Focus input if the last focused input was this input + * This avoids loosing the focus when the UI is re-rendered + */ + useEffect(() => { + if (inputRef.current && name === focusedInput) { + (inputRef.current.children[0] as HTMLInputElement).focus(); + } + }, [inputRef]); + const handleChange = (event: ChangeEvent) => { setValue(event.target.value); handleInputChange(name, event.target.value ?? null, form); }; + const handleFocus = () => setCurrentFocusedInput(name); + const handleBlur = () => setCurrentFocusedInput(null); + return ( void; +export type SetCurrentInputFocus = (name: string | null) => void; + export type SnapInterfaceContextType = { handleEvent: HandleEvent; getValue: GetValue; handleInputChange: HandleInputChange; handleFileChange: HandleFileChange; + setCurrentFocusedInput: SetCurrentInputFocus; + focusedInput: string | null; snapId: string; }; @@ -80,6 +84,7 @@ export const SnapInterfaceContextProvider: FunctionComponent< // UI. It's kept in a ref to avoid useless re-rendering of the entire tree of // components. const internalState = useRef(initialState ?? {}); + const focusedInput = useRef(null); // Since the internal state is kept in a reference, it won't update when the // interface is updated. We have to manually update it. @@ -237,6 +242,9 @@ export const SnapInterfaceContextProvider: FunctionComponent< return undefined; }; + const setCurrentFocusedInput: SetCurrentInputFocus = (name) => + (focusedInput.current = name); + return ( From 778e5ab5378f002966036b501c02d411bea432db Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 1 Oct 2024 12:58:40 +0100 Subject: [PATCH 033/226] fix: genUnapprovedApproveConfirmation import path (#27530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Merging https://github.com/MetaMask/metamask-extension/pull/27358 and then https://github.com/MetaMask/metamask-extension/pull/27391 without rebasing caused an erroneous import path in unit tests. This caused failing test and import linting. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27530?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. --- .../hooks/useLedgerConnection.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/pages/confirmations/hooks/useLedgerConnection.test.ts b/ui/pages/confirmations/hooks/useLedgerConnection.test.ts index 42868115a369..7041b11b1aa4 100644 --- a/ui/pages/confirmations/hooks/useLedgerConnection.test.ts +++ b/ui/pages/confirmations/hooks/useLedgerConnection.test.ts @@ -1,18 +1,18 @@ -import type { TransactionMeta } from '@metamask/transaction-controller'; import type { KeyringObject } from '@metamask/keyring-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; -import { KeyringType } from '../../../../shared/constants/keyring'; -import { renderHookWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers'; -import { getMockConfirmStateForTransaction } from '../../../../test/data/confirmations/helper'; -import { genUnapprovedApproveConfirmation } from '../../../../test/data/confirmations/contract-interaction'; -import { flushPromises } from '../../../../test/lib/timer-helpers'; import { + HardwareTransportStates, + LEDGER_USB_VENDOR_ID, LedgerTransportTypes, WebHIDConnectedStatuses, - LEDGER_USB_VENDOR_ID, - HardwareTransportStates, } from '../../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../../shared/constants/keyring'; +import { getMockConfirmStateForTransaction } from '../../../../test/data/confirmations/helper'; +import { genUnapprovedApproveConfirmation } from '../../../../test/data/confirmations/token-approve'; +import { renderHookWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers'; +import { flushPromises } from '../../../../test/lib/timer-helpers'; import * as appActions from '../../../ducks/app/app'; import { attemptLedgerTransportCreation } from '../../../store/actions'; import useLedgerConnection from './useLedgerConnection'; From 874f704259535818ff62a69746a73c26f2fea495 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Tue, 1 Oct 2024 14:40:27 +0200 Subject: [PATCH 034/226] fix(NOTIFY-1171): account syncing performance and bug fixes (#27529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes some bugs and adds as well some huge performance improvements for account syncing. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27529?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Create a new SRP 2. Add new accounts, rename some 3. Uninstall extension and reinstall 4. Import your previously created SRP 5. All your previously created accounts and respective names should be there! ## **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 --- lavamoat/browserify/beta/policy.json | 2 -- lavamoat/browserify/flask/policy.json | 2 -- lavamoat/browserify/main/policy.json | 2 -- lavamoat/browserify/mmi/policy.json | 2 -- package.json | 2 +- yarn.lock | 10 +++++----- 6 files changed, 6 insertions(+), 14 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 95835b028ee9..70552b0d32a7 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2039,12 +2039,10 @@ "URL": true, "URLSearchParams": true, "addEventListener": true, - "clearInterval": true, "console.error": true, "dispatchEvent": true, "fetch": true, "removeEventListener": true, - "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 95835b028ee9..70552b0d32a7 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2039,12 +2039,10 @@ "URL": true, "URLSearchParams": true, "addEventListener": true, - "clearInterval": true, "console.error": true, "dispatchEvent": true, "fetch": true, "removeEventListener": true, - "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 95835b028ee9..70552b0d32a7 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2039,12 +2039,10 @@ "URL": true, "URLSearchParams": true, "addEventListener": true, - "clearInterval": true, "console.error": true, "dispatchEvent": true, "fetch": true, "removeEventListener": true, - "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 8c60013092de..2116f0a1a7c8 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2131,12 +2131,10 @@ "URL": true, "URLSearchParams": true, "addEventListener": true, - "clearInterval": true, "console.error": true, "dispatchEvent": true, "fetch": true, "removeEventListener": true, - "setInterval": true, "setTimeout": true }, "packages": { diff --git a/package.json b/package.json index 86b0718ff463..e3063816cbcb 100644 --- a/package.json +++ b/package.json @@ -344,7 +344,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", "@metamask/preinstalled-example-snap": "^0.1.0", - "@metamask/profile-sync-controller": "^0.9.3", + "@metamask/profile-sync-controller": "^0.9.4", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 22889932623c..1434d86d4e62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6053,9 +6053,9 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^0.9.3": - version: 0.9.3 - resolution: "@metamask/profile-sync-controller@npm:0.9.3" +"@metamask/profile-sync-controller@npm:^0.9.4": + version: 0.9.4 + resolution: "@metamask/profile-sync-controller@npm:0.9.4" dependencies: "@metamask/base-controller": "npm:^7.0.1" "@metamask/keyring-api": "npm:^8.1.3" @@ -6071,7 +6071,7 @@ __metadata: "@metamask/accounts-controller": ^18.1.1 "@metamask/keyring-controller": ^17.2.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/31efea63cac0b5f01024163fb6911f971aeb6f7e7a7d71fa4a43b8e31e0fc60033e99bcfec19283f9410e7258bcd0ce3bf751bed374e6b3d09ea4a9782731320 + checksum: 10/86079da552eed316f2754bd899047de1d8d9d15d390c9cdee0aef66b95bea708b5c7929a8d8d946210cc0e4c52347fee971a5cf5130149d0ca60abdc85f47774 languageName: node linkType: hard @@ -26138,7 +26138,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" "@metamask/preinstalled-example-snap": "npm:^0.1.0" - "@metamask/profile-sync-controller": "npm:^0.9.3" + "@metamask/profile-sync-controller": "npm:^0.9.4" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" From cf55b09b190fc9fa0a5036b6f110b467d38f1274 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Tue, 1 Oct 2024 16:59:59 +0200 Subject: [PATCH 035/226] fix(snaps): Fix custom UI buttons submitting forms (#27531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes a bug in custom UI where a `Button` with no type would trigger a form submission when inside a form due to the type not being set by default. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27531?quickstart=1) ## **Related issues** Fixes: #27400 ## **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. --- ui/components/app/snaps/snap-ui-button/snap-ui-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/app/snaps/snap-ui-button/snap-ui-button.tsx b/ui/components/app/snaps/snap-ui-button/snap-ui-button.tsx index cedcc375c17e..08fef2f9a6b7 100644 --- a/ui/components/app/snaps/snap-ui-button/snap-ui-button.tsx +++ b/ui/components/app/snaps/snap-ui-button/snap-ui-button.tsx @@ -23,7 +23,7 @@ export const SnapUIButton: FunctionComponent< > = ({ name, children, - type, + type = ButtonType.Button, variant = 'primary', disabled = false, className = '', From facf90562a5314d087fc9a1d2b17bae07f5097c7 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:03:01 +0200 Subject: [PATCH 036/226] feat: Enable gas included swaps (#27427) --- app/_locales/en/messages.json | 16 + app/scripts/controllers/swaps/swaps.test.ts | 2 + app/scripts/controllers/swaps/swaps.types.ts | 1 + shared/lib/swaps-utils.js | 2 + shared/lib/swaps-utils.test.js | 1 + test/jest/mock-store.js | 49 ++- ui/ducks/swaps/swaps.js | 31 +- ui/ducks/swaps/swaps.test.js | 25 +- ui/helpers/constants/zendesk-url.js | 2 + .../prepare-swap-page/prepare-swap-page.js | 11 +- .../swaps/prepare-swap-page/review-quote.js | 338 +++++++++++++----- .../prepare-swap-page/review-quote.test.js | 37 +- .../smart-transaction-status.js | 7 +- ui/pages/swaps/view-quote/view-quote.js | 5 +- ui/store/actions.ts | 10 +- 15 files changed, 424 insertions(+), 113 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b880d92ad468..42a5a108fdf1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2121,6 +2121,9 @@ "message": "This gas fee has been suggested by $1. Overriding this may cause a problem with your transaction. Please reach out to $1 if you have questions.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Gas fee" + }, "gasIsETH": { "message": "Gas is $1 " }, @@ -2435,6 +2438,9 @@ "inYourSettings": { "message": "in your Settings" }, + "included": { + "message": "included" + }, "infuraBlockedNotification": { "message": "MetaMask is unable to connect to the blockchain host. Review possible reasons $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -5668,12 +5674,22 @@ "message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "This quote incorporates gas fees by adjusting the token amount sent or received. You may receive ETH in a separate transaction on your activity list." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Learn more about gas fees" + }, "swapHighSlippage": { "message": "High slippage" }, "swapHighSlippageWarning": { "message": "Slippage amount is very high." }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Includes gas and a $1% MetaMask fee", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Includes a $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." diff --git a/app/scripts/controllers/swaps/swaps.test.ts b/app/scripts/controllers/swaps/swaps.test.ts index 3fa4f1ff9409..4ed1b545f170 100644 --- a/app/scripts/controllers/swaps/swaps.test.ts +++ b/app/scripts/controllers/swaps/swaps.test.ts @@ -26,6 +26,7 @@ const MOCK_FETCH_PARAMS: FetchTradesInfoParams = { fromAddress: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078', exchangeList: 'zeroExV1', balanceError: false, + enableGasIncludedQuotes: false, }; const TEST_AGG_ID_1 = 'TEST_AGG_1'; @@ -1164,6 +1165,7 @@ describe('SwapsController', function () { fromAddress: '', exchangeList: 'zeroExV1', balanceError: false, + enableGasIncludedQuotes: false, metaData: {} as FetchTradesInfoParamsMetadata, }; const swapsFeatureIsLive = false; diff --git a/app/scripts/controllers/swaps/swaps.types.ts b/app/scripts/controllers/swaps/swaps.types.ts index 44e4d4939742..ca059723277a 100644 --- a/app/scripts/controllers/swaps/swaps.types.ts +++ b/app/scripts/controllers/swaps/swaps.types.ts @@ -308,6 +308,7 @@ export type FetchTradesInfoParams = { fromAddress: string; exchangeList: string; balanceError: boolean; + enableGasIncludedQuotes: boolean; }; export type FetchTradesInfoParamsMetadata = { diff --git a/shared/lib/swaps-utils.js b/shared/lib/swaps-utils.js index d80d70902810..c51a3ac1198e 100644 --- a/shared/lib/swaps-utils.js +++ b/shared/lib/swaps-utils.js @@ -265,6 +265,7 @@ export async function fetchTradesInfo( value, fromAddress, exchangeList, + enableGasIncludedQuotes, }, { chainId }, ) { @@ -275,6 +276,7 @@ export async function fetchTradesInfo( slippage, timeout: SECOND * 10, walletAddress: fromAddress, + enableGasIncludedQuotes, }; if (exchangeList) { diff --git a/shared/lib/swaps-utils.test.js b/shared/lib/swaps-utils.test.js index 06080a8f55e7..891c1c5fb961 100644 --- a/shared/lib/swaps-utils.test.js +++ b/shared/lib/swaps-utils.test.js @@ -87,6 +87,7 @@ describe('Swaps Utils', () => { sourceDecimals: TOKENS[0].decimals, sourceTokenInfo: { ...TOKENS[0] }, destinationTokenInfo: { ...TOKENS[1] }, + enableGasIncludedQuotes: false, }, { chainId: CHAIN_IDS.MAINNET }, ); diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 625b6dcf6c83..736b9c4eb325 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -210,7 +210,7 @@ export const createSwapsMockStore = () => { }, ], useCurrencyRateCheck: true, - currentCurrency: 'ETH', + currentCurrency: 'usd', currencyRates: { ETH: { conversionRate: 1, @@ -469,6 +469,23 @@ export const createSwapsMockStore = () => { decimals: 18, }, fee: 1, + isGasIncludedTrade: false, + approvalTxFees: { + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 2310003200, maxPriorityFeePerGas: 513154852 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, + tradeTxFees: { + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 2310003200, maxPriorityFeePerGas: 513154852 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, }, TEST_AGG_2: { trade: { @@ -503,6 +520,36 @@ export const createSwapsMockStore = () => { decimals: 18, }, fee: 1, + isGasIncludedTrade: false, + approvalTxFees: { + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 2310003200, maxPriorityFeePerGas: 513154852 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, + tradeTxFees: { + feeEstimate: 42000000000000, + fees: [ + { + maxFeePerGas: 2310003200, + maxPriorityFeePerGas: 513154852, + tokenFees: [ + { + token: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + symbol: 'DAI', + decimals: 18, + }, + balanceNeededToken: '0x426dc933c2e5a', + }, + ], + }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, }, }, fetchParams: { diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 8abb63aa4a14..e399dc663806 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -435,7 +435,14 @@ export const getPendingSmartTransactions = (state) => { }; export const getSmartTransactionFees = (state) => { - return state.metamask.smartTransactionsState?.fees; + const usedQuote = getUsedQuote(state); + if (!usedQuote?.isGasIncludedTrade) { + return state.metamask.smartTransactionsState?.fees; + } + return { + approvalTxFees: usedQuote.approvalTxFees, + tradeTxFees: usedQuote.tradeTxFees, + }; }; export const getSmartTransactionEstimatedGas = (state) => { @@ -780,6 +787,8 @@ export const fetchQuotesAndSetQuoteState = ( fromAddress: selectedAccount.address, balanceError, sourceDecimals: fromTokenDecimals, + enableGasIncludedQuotes: + currentSmartTransactionsEnabled && smartTransactionsOptInStatus, }, { sourceTokenInfo, @@ -933,6 +942,7 @@ export const signAndSendSwapsSmartTransaction = ({ stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, + is_gas_included_trade: usedQuote.isGasIncludedTrade, ...additionalTrackingParams, }; trackEvent({ @@ -964,13 +974,18 @@ export const signAndSendSwapsSmartTransaction = ({ value: '0x0', }; } - const fees = await dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction, - approveTxParams: updatedApproveTxParams, - fallbackOnNotEnoughFunds: true, - }), - ); + let fees; + if (usedQuote.isGasIncludedTrade) { + fees = getSmartTransactionFees(state); + } else { + fees = await dispatch( + fetchSwapsSmartTransactionFees({ + unsignedTransaction, + approveTxParams: updatedApproveTxParams, + fallbackOnNotEnoughFunds: true, + }), + ); + } if (!fees) { log.error('"fetchSwapsSmartTransactionFees" failed'); dispatch(setSwapsSTXSubmitLoading(false)); diff --git a/ui/ducks/swaps/swaps.test.js b/ui/ducks/swaps/swaps.test.js index 0bba5e5a68be..83c133572c0d 100644 --- a/ui/ducks/swaps/swaps.test.js +++ b/ui/ducks/swaps/swaps.test.js @@ -652,13 +652,36 @@ describe('Ducks - Swaps', () => { }); describe('getSmartTransactionFees', () => { - it('returns unsigned transactions and estimates', () => { + it('returns estimates from the STX controller', () => { const state = createSwapsMockStore(); const smartTransactionFees = swaps.getSmartTransactionFees(state); expect(smartTransactionFees).toMatchObject( state.metamask.smartTransactionsState.fees, ); }); + + it('returns estimates from a selected quote', () => { + const state = createSwapsMockStore(); + state.metamask.swapsState.quotes.TEST_AGG_2.isGasIncludedTrade = true; + const smartTransactionFees = swaps.getSmartTransactionFees(state); + expect(smartTransactionFees).toMatchObject({ + approvalTxFees: + state.metamask.swapsState.quotes.TEST_AGG_2.approvalTxFees, + tradeTxFees: state.metamask.swapsState.quotes.TEST_AGG_2.tradeTxFees, + }); + }); + + it('returns estimates from a top quote if no quote is selected', () => { + const state = createSwapsMockStore(); + state.metamask.swapsState.selectedAggId = null; + state.metamask.swapsState.quotes.TEST_AGG_BEST.isGasIncludedTrade = true; + const smartTransactionFees = swaps.getSmartTransactionFees(state); + expect(smartTransactionFees).toMatchObject({ + approvalTxFees: + state.metamask.swapsState.quotes.TEST_AGG_BEST.approvalTxFees, + tradeTxFees: state.metamask.swapsState.quotes.TEST_AGG_BEST.tradeTxFees, + }); + }); }); describe('getSmartTransactionEstimatedGas', () => { diff --git a/ui/helpers/constants/zendesk-url.js b/ui/helpers/constants/zendesk-url.js index 1e29bd9b1fc7..3986df5b41ef 100644 --- a/ui/helpers/constants/zendesk-url.js +++ b/ui/helpers/constants/zendesk-url.js @@ -8,6 +8,8 @@ const ZENDESK_URLS = { CUSTOMIZE_NONCE: 'https://support.metamask.io/transactions-and-gas/transactions/how-to-customize-a-transaction-nonce/', GAS_FEES: 'https://support.metamask.io/transactions-and-gas/gas-fees/', + SWAPS_GAS_FEES: + 'https://support.metamask.io/token-swaps/user-guide-swaps/#gas-fees', HARDWARE_CONNECTION: 'https://support.metamask.io/privacy-and-security/hardware-wallet-hub/', IMPORT_ACCOUNTS: 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..7ea900c5eb59 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -782,10 +782,17 @@ export default function PrepareSwapPage({ ); } + const isNonDefaultToken = !isSwapsDefaultTokenSymbol( + fromTokenSymbol, + chainId, + ); + const hasPositiveFromTokenBalance = rawFromTokenBalance > 0; + const isTokenEligibleForMaxBalance = + isSmartTransaction || (!isSmartTransaction && isNonDefaultToken); const showMaxBalanceLink = fromTokenSymbol && - !isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && - rawFromTokenBalance > 0; + isTokenEligibleForMaxBalance && + hasPositiveFromTokenBalance; return (
diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 496ae5ee6d9e..31cf9959f231 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -23,7 +23,6 @@ import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { getQuotes, - getSelectedQuote, getApproveTxParams, getFetchParams, setBalanceError, @@ -36,6 +35,7 @@ import { getDestinationTokenInfo, getUsedSwapsGasPrice, getTopQuote, + getUsedQuote, signAndSendTransactions, getBackgroundSwapRouteState, swapsQuoteSelected, @@ -84,6 +84,7 @@ import { decimalToHex, decWEIToDecETH, sumHexes, + hexToDecimal, } from '../../../../shared/modules/conversion.utils'; import { getCustomTxParamsData } from '../../confirmations/confirm-approve/confirm-approve.util'; import { @@ -113,6 +114,7 @@ import { Size, FlexDirection, Severity, + FontStyle, } from '../../../helpers/constants/design-system'; import { BannerAlert, @@ -143,11 +145,41 @@ import { import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; import useRamps from '../../../hooks/ramps/useRamps/useRamps'; +import { getTokenFiatAmount } from '../../../helpers/utils/token-util'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; let intervalId; +const ViewAllQuotesLink = React.memo(function ViewAllQuotesLink({ + trackAllAvailableQuotesOpened, + setSelectQuotePopoverShown, + t, +}) { + const handleClick = useCallback(() => { + trackAllAvailableQuotesOpened(); + setSelectQuotePopoverShown(true); + }, [trackAllAvailableQuotesOpened, setSelectQuotePopoverShown]); + + return ( + + {t('viewAllQuotes')} + + ); +}); + +ViewAllQuotesLink.propTypes = { + trackAllAvailableQuotesOpened: PropTypes.func.isRequired, + setSelectQuotePopoverShown: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, +}; + export default function ReviewQuote({ setReceiveToAmount }) { const history = useHistory(); const dispatch = useDispatch(); @@ -206,9 +238,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { const balanceError = useSelector(getBalanceError); const fetchParams = useSelector(getFetchParams, isEqual); const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const selectedQuote = useSelector(getSelectedQuote, isEqual); const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; + const usedQuote = useSelector(getUsedQuote, isEqual); const tradeValue = usedQuote?.trade?.value ?? '0x0'; const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const chainId = useSelector(getCurrentChainId); @@ -229,6 +260,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); const unsignedTransaction = usedQuote.trade; + const { isGasIncludedTrade } = usedQuote; const isSmartTransaction = currentSmartTransactionsEnabled && smartTransactionsOptInStatus; @@ -880,7 +912,9 @@ export default function ReviewQuote({ setReceiveToAmount }) { ]); useEffect(() => { - if (isSmartTransaction && !insufficientTokens) { + // If it's a smart transaction, has sufficient tokens, and gas is not included in the trade, + // set up gas fee polling. + if (isSmartTransaction && !insufficientTokens && !isGasIncludedTrade) { const unsignedTx = { from: unsignedTransaction.from, to: unsignedTransaction.to, @@ -923,6 +957,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { chainId, swapsNetworkConfig.stxGetTransactionsRefreshTime, insufficientTokens, + isGasIncludedTrade, ]); useEffect(() => { @@ -1045,6 +1080,40 @@ export default function ReviewQuote({ setReceiveToAmount }) { } }; + const gasTokenFiatAmount = useMemo(() => { + if (!isGasIncludedTrade) { + return undefined; + } + const tradeTxTokenFee = + smartTransactionFees?.tradeTxFees?.fees?.[0]?.tokenFees?.[0]; + if (!tradeTxTokenFee) { + return undefined; + } + const { token: { address, decimals, symbol } = {}, balanceNeededToken } = + tradeTxTokenFee; + const checksumAddress = toChecksumHexAddress(address); + const contractExchangeRate = memoizedTokenConversionRates[checksumAddress]; + const gasTokenAmountDec = calcTokenAmount( + hexToDecimal(balanceNeededToken), + decimals, + ).toString(10); + return getTokenFiatAmount( + contractExchangeRate, + conversionRate, + currentCurrency, + gasTokenAmountDec, + symbol, + true, + true, + ); + }, [ + isGasIncludedTrade, + smartTransactionFees, + memoizedTokenConversionRates, + conversionRate, + currentCurrency, + ]); + return (
@@ -1122,9 +1191,9 @@ export default function ReviewQuote({ setReceiveToAmount }) { - {t('quoteRate')} + {t('quoteRate')}* - + {isGasIncludedTrade && ( - - {t('transactionDetailGasHeading')} - - - {t('swapGasFeesExplanation', [ + + {t('gasFee')} + + +

+ {t('swapGasIncludedTooltipExplanation')} +

{ trackEvent({ - event: 'Clicked "Gas Fees: Learn More" Link', + event: + 'Clicked "GasIncluded tooltip: Learn More" Link', category: MetaMetricsEventCategory.Swaps, }); }} > - {t('swapGasFeesExplanationLinkText')} - , - ])} -

- } - /> + {t('swapGasIncludedTooltipExplanationLinkText')} + + + } + /> +
+ + + {gasTokenFiatAmount} + + + {t('included')} + +
+ )} + {!isGasIncludedTrade && ( - - {feeInEth} - - + {t('transactionDetailGasHeading')} + + + {t('swapGasFeesExplanation', [ + { + trackEvent({ + event: 'Clicked "Gas Fees: Learn More" Link', + category: MetaMetricsEventCategory.Swaps, + }); + }} + > + {t('swapGasFeesExplanationLinkText')} + , + ])} +

+ } + /> +
+ - {` ${feeInFiat}`} - + + {feeInEth} + + + {` ${feeInFiat}`} + + - - {(maxFeeInFiat || maxFeeInEth) && ( + )} + {!isGasIncludedTrade && (maxFeeInFiat || maxFeeInEth) && ( @@ -1248,7 +1395,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { {t('swapEnableTokenForSwapping', [tokenApprovalTextComponent])} @@ -1264,32 +1411,55 @@ export default function ReviewQuote({ setReceiveToAmount }) { )} - - - {t('swapIncludesMetaMaskFeeViewAllQuotes', [ - metaMaskFee, - { - trackAllAvailableQuotesOpened(); - setSelectQuotePopoverShown(true); + {isGasIncludedTrade && ( + + + * {t('swapIncludesGasAndMetaMaskFee', [metaMaskFee])} + + + + + + )} + {!isGasIncludedTrade && ( + + + * + {t('swapIncludesMetaMaskFeeViewAllQuotes', [ + metaMaskFee, + - {t('viewAllQuotes')} - , - ])} - - + setSelectQuotePopoverShown={setSelectQuotePopoverShown} + t={t} + />, + ])} + + + )}
{ const props = createProps(); const { getByText } = renderWithProvider(, store); expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByText('Quote rate')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); expect(getByText('Includes a 1% MetaMask fee –')).toBeInTheDocument(); expect(getByText('view all quotes')).toBeInTheDocument(); expect(getByText('Estimated gas fee')).toBeInTheDocument(); @@ -73,7 +74,7 @@ describe('ReviewQuote', () => { const props = createProps(); const { getByText } = renderWithProvider(, store); expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByText('Quote rate')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); expect(getByText('Includes a 1% MetaMask fee –')).toBeInTheDocument(); expect(getByText('view all quotes')).toBeInTheDocument(); expect(getByText('Estimated gas fee')).toBeInTheDocument(); @@ -96,7 +97,7 @@ describe('ReviewQuote', () => { const props = createProps(); const { getByText } = renderWithProvider(, store); expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByText('Quote rate')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); expect(getByText('Includes a 1% MetaMask fee –')).toBeInTheDocument(); expect(getByText('view all quotes')).toBeInTheDocument(); expect(getByText('Estimated gas fee')).toBeInTheDocument(); @@ -106,4 +107,34 @@ describe('ReviewQuote', () => { expect(getByText('Edit limit')).toBeInTheDocument(); expect(getByText('Swap')).toBeInTheDocument(); }); + + it('renders the component with gas included quotes', () => { + const state = createSwapsMockStore(); + state.metamask.swapsState.quotes.TEST_AGG_2.isGasIncludedTrade = true; + state.metamask.marketData[CHAIN_IDS.MAINNET][ + '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI token contract address. + ] = { + price: 2, + contractPercentChange1d: 0.004, + priceChange1d: 0.00004, + }; + state.metamask.currencyRates.ETH = { + conversionDate: 1708532473.416, + conversionRate: 2918.02, + usdConversionRate: 2918.02, + }; + const store = configureMockStore(middleware)(state); + const props = createProps(); + const { getByText } = renderWithProvider(, store); + expect(getByText('New quotes in')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); + expect( + getByText('* Includes gas and a 1% MetaMask fee'), + ).toBeInTheDocument(); + expect(getByText('view all quotes')).toBeInTheDocument(); + expect(getByText('Gas fee')).toBeInTheDocument(); + // $6.82 gas fee is calculated based on params set in the the beginning of the test. + expect(getByText('$6.82')).toBeInTheDocument(); + expect(getByText('Swap')).toBeInTheDocument(); + }); }); 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 157190687f31..530372105b69 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -8,11 +8,10 @@ import { getFetchParams, prepareToLeaveSwaps, getCurrentSmartTransactions, - getSelectedQuote, - getTopQuote, getCurrentSmartTransactionsEnabled, getSwapsNetworkConfig, cancelSwapsSmartTransaction, + getUsedQuote, } from '../../../ducks/swaps/swaps'; import { isHardwareWallet, @@ -74,9 +73,7 @@ export default function SmartTransactionStatusPage() { const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const needsTwoConfirmations = true; - const selectedQuote = useSelector(getSelectedQuote, isEqual); - const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; + const usedQuote = useSelector(getUsedQuote, isEqual); const currentSmartTransactions = useSelector( getCurrentSmartTransactions, isEqual, diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 8dc17ac3c765..be02ba840eb5 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -23,7 +23,6 @@ import { MetaMetricsContext } from '../../../contexts/metametrics'; import FeeCard from '../fee-card'; import { getQuotes, - getSelectedQuote, getApproveTxParams, getFetchParams, setBalanceError, @@ -36,6 +35,7 @@ import { getDestinationTokenInfo, getUsedSwapsGasPrice, getTopQuote, + getUsedQuote, signAndSendTransactions, getBackgroundSwapRouteState, swapsQuoteSelected, @@ -181,9 +181,8 @@ export default function ViewQuote() { const balanceError = useSelector(getBalanceError); const fetchParams = useSelector(getFetchParams, isEqual); const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const selectedQuote = useSelector(getSelectedQuote, isEqual); const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; + const usedQuote = useSelector(getUsedQuote, isEqual); const tradeValue = usedQuote?.trade?.value ?? '0x0'; const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index c4bed2665a6b..dae0052c46f6 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3664,6 +3664,7 @@ export function fetchAndSetQuotes( fromAddress: string; balanceError: string; sourceDecimals: number; + enableGasIncludedQuotes: boolean; }, fetchParamsMetaData: { sourceTokenInfo: Token; @@ -4757,18 +4758,15 @@ export function signAndSendSmartTransaction({ unsignedTransaction, smartTransactionFees.fees, ); - const signedCanceledTransactions = await createSignedTransactions( - unsignedTransaction, - smartTransactionFees.cancelFees, - true, - ); try { const response = await submitRequestToBackground<{ uuid: string }>( 'submitSignedTransactions', [ { signedTransactions, - signedCanceledTransactions, + // The "signedCanceledTransactions" parameter is still expected by the STX controller but is no longer used. + // So we are passing an empty array. The parameter may be deprecated in a future update. + signedCanceledTransactions: [], txParams: unsignedTransaction, }, ], From 64dde2bdbdabc3d8bfab012c002da4c0a59190c8 Mon Sep 17 00:00:00 2001 From: martahj Date: Tue, 1 Oct 2024 13:07:11 -0500 Subject: [PATCH 037/226] feat: remove squiggle animation from swaps smart transactions (#27264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes the background animation from the swaps smart transaction screen [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27264?quickstart=1) ## **Manual testing steps** 1. Enable smart transactions 2. Perform a swap 3. Observe that the animation has been removed ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/c201b72f-b0fc-4599-b73b-4559829bc9cb ### **After** Screenshot 2024-09-18 at 3 27 49 PM Screenshot 2024-09-18 at 3 28 00 PM Screenshot 2024-09-18 at 3 28 26 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 - [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. --- .../swaps/smart-transaction-status/index.scss | 15 +-------------- .../smart-transaction-status.js | 4 ++-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/ui/pages/swaps/smart-transaction-status/index.scss b/ui/pages/swaps/smart-transaction-status/index.scss index 4229acca71c0..d19add085c65 100644 --- a/ui/pages/swaps/smart-transaction-status/index.scss +++ b/ui/pages/swaps/smart-transaction-status/index.scss @@ -36,26 +36,13 @@ width: 100%; } - &__background-animation { - position: relative; - left: -88px; - background-repeat: repeat; - background-position: 0 0; - + &__spacer-box { &--top { - width: 1634px; height: 54px; - background-size: 817px 54px; - background-image: url('/images/transaction-background-top.svg'); - animation: shift 19s linear infinite; } &--bottom { - width: 1600px; height: 62px; - background-size: 800px 62px; - background-image: url('/images/transaction-background-bottom.svg'); - animation: shift 22s linear infinite; } } 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 530372105b69..b103ead2097c 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -365,7 +365,7 @@ export default function SmartTransactionStatusPage() { {icon && ( @@ -440,7 +440,7 @@ export default function SmartTransactionStatusPage() { )} {subDescription && ( Date: Tue, 1 Oct 2024 20:24:49 +0200 Subject: [PATCH 038/226] feat: add merge queue (#26871) 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/26687?quickstart=1) This PR introduces workflow support for merge queues, adding the "merge_group" trigger to the relevant GitHub actions ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3012 ## **Manual testing steps** 1. CI works with merge queues ## **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. --- .github/workflows/run-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a0240346af64..77958f69da2d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,6 +10,7 @@ on: - opened - reopened - synchronize + merge_group: jobs: test-unit: From 2069163fe6b18a075247e93a9cc29ddc7fbe765c Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:22:25 +0200 Subject: [PATCH 039/226] fix: flaky test `4byte setting does not try to get contract method name from 4byte when the setting is off` (#27560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This test is failing as it's trying to look for the `Deposit initiated` element in the test dapp and it doesn't appear in time. ![Screenshot from 2024-10-02 12-21-16](https://github.com/user-attachments/assets/5b01d83c-1684-4e30-a02b-ed812708d90b) We shouldn't care if the test dapp sets the value to Deposit initiated into its div element, as long as the popup is open (which it does). This removes any potential race condition on the test dapp side There are several things to fix/improve around the 2 specs for 4byte, so I took the opportunity to fix those too. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27560?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/21494 ## **Manual testing steps** 1. Check ci 2. Run test locally ## **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. --- .../tests/settings/4byte-directory.spec.js | 52 +++++++------------ 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/test/e2e/tests/settings/4byte-directory.spec.js b/test/e2e/tests/settings/4byte-directory.spec.js index 2874118c3a28..483ff1e0149a 100644 --- a/test/e2e/tests/settings/4byte-directory.spec.js +++ b/test/e2e/tests/settings/4byte-directory.spec.js @@ -1,12 +1,10 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, + logInWithBalanceValidation, openDapp, - unlockWallet, openMenuSafe, - largeDelayMs, - veryLargeDelayMs, + unlockWallet, + withFixtures, WINDOW_TITLES, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); @@ -27,27 +25,23 @@ describe('4byte setting', function () { const contractAddress = await contractRegistry.getContractAddress( smartContract, ); - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // deploy contract await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent - await driver.delay(largeDelayMs); await driver.clickElement('#depositButton'); - await driver.waitForSelector({ - css: 'span', - text: 'Deposit initiated', - }); - - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const actionElement = await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', + await driver.waitForSelector({ + tag: 'span', text: 'Deposit', }); - assert.equal(await actionElement.getText(), 'DEPOSIT'); + await driver.assertElementNotPresent({ + tag: 'span', + text: 'Contract interaction', + }); }, ); }); @@ -83,28 +77,18 @@ describe('4byte setting', function () { await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent - await driver.findClickableElement('#depositButton'); await driver.clickElement('#depositButton'); - await driver.waitForSelector({ - css: 'span', - text: 'Deposit initiated', - }); - - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const contractInteraction = 'Contract interaction'; - const actionElement = await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', - text: contractInteraction, + + await driver.assertElementNotPresent({ + tag: 'span', + text: 'Deposit', + }); + await driver.waitForSelector({ + tag: 'span', + text: 'Contract interaction', }); - // We add a delay here to wait for any potential UI changes - await driver.delay(veryLargeDelayMs); - // css text-transform: uppercase is applied to the text - assert.equal( - await actionElement.getText(), - contractInteraction.toUpperCase(), - ); }, ); }); From eea557d94674fc8c20b04a3966f97f6211990aa9 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Wed, 2 Oct 2024 13:31:21 +0100 Subject: [PATCH 040/226] feat: Add redesign integration tests (#27259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds integration tests for redesigned screens: - erc721 Approve, - contract deployment, - increaseAllowance and - setApprovalForAll. Migrate integration tests to using `tEn` by referring to text snippets through the localization key. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27259?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. --- .../transactions/contract-deployment.test.tsx | 408 ++++++++++++++++++ .../contract-interaction.test.tsx | 61 +-- .../transactions/erc20-approve.test.tsx | 45 +- .../transactions/erc721-approve.test.tsx | 214 ++++++++- .../transactions/increase-allowance.test.tsx | 384 +++++++++++++++++ .../set-approval-for-all.test.tsx | 348 +++++++++++++++ .../transactions/transactionDataHelpers.tsx | 86 +++- 7 files changed, 1475 insertions(+), 71 deletions(-) create mode 100644 test/integration/confirmations/transactions/contract-deployment.test.tsx create mode 100644 test/integration/confirmations/transactions/increase-allowance.test.tsx create mode 100644 test/integration/confirmations/transactions/set-approval-for-all.test.tsx diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx new file mode 100644 index 000000000000..ecef04f30861 --- /dev/null +++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx @@ -0,0 +1,408 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; +import { + act, + fireEvent, + screen, + waitFor, + within, +} from '@testing-library/react'; +import nock from 'nock'; +import { + MetaMetricsEventCategory, + MetaMetricsEventLocation, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedContractDeploymentTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedContractDeployment = ({ + accountAddress, + showConfirmationAdvancedDetails = false, +}: { + accountAddress: string; + showConfirmationAdvancedDetails?: boolean; +}) => { + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails, + }, + nextNonce: '8', + currencyRates: { + SepoliaETH: { + conversionDate: 1721392020.645, + conversionRate: 3404.13, + usdConversionRate: 3404.13, + }, + ETH: { + conversionDate: 1721393858.083, + conversionRate: 3414.67, + usdConversionRate: 3414.67, + }, + }, + currentCurrency: 'usd', + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'local:http://localhost:8086/', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0xd0e30db0': { + name: 'Deposit', + params: [ + { + type: 'uint256', + }, + ], + }, + }, + transactions: [ + getUnapprovedContractDeploymentTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'Deposit', + params: [ + { + name: 'numberOfTokens', + type: 'uint256', + value: 1, + }, + ], + }, + ], + source: 'Sourcify', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); +}; + +describe('Contract Deployment Confirmation', () => { + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks(); + const DEPOSIT_HEX_SIG = '0xd0e30db0'; + mock4byte(DEPOSIT_HEX_SIG); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it('displays the header account modal with correct data', async () => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const accountName = account.metadata.name; + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect(screen.getByTestId('header-account-name')).toHaveTextContent( + accountName, + ); + expect(screen.getByTestId('header-network-display-name')).toHaveTextContent( + 'Sepolia', + ); + + fireEvent.click(screen.getByTestId('header-info__account-details-button')); + + expect( + await screen.findByTestId( + 'confirmation-account-details-modal__account-name', + ), + ).toHaveTextContent(accountName); + expect(screen.getByTestId('address-copy-button-text')).toHaveTextContent( + '0x0DCD5...3E7bc', + ); + expect( + screen.getByTestId('confirmation-account-details-modal__account-balance'), + ).toHaveTextContent('1.582717SepoliaETH'); + + let confirmAccountDetailsModalMetricsEvent; + + await waitFor(() => { + confirmAccountDetailsModalMetricsEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => + call[0] === 'trackMetaMetricsEvent' && + call[1]?.[0].category === MetaMetricsEventCategory.Confirmations, + ); + + expect(confirmAccountDetailsModalMetricsEvent?.[0]).toBe( + 'trackMetaMetricsEvent', + ); + }); + + expect(confirmAccountDetailsModalMetricsEvent?.[1]).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + category: MetaMetricsEventCategory.Confirmations, + event: MetaMetricsEventName.AccountDetailsOpened, + properties: { + action: 'Confirm Screen', + location: MetaMetricsEventLocation.Transaction, + transaction_type: TransactionType.deployContract, + }, + }), + ]), + ); + + fireEvent.click( + screen.getByTestId('confirmation-account-details-modal__close-button'), + ); + + await waitFor(() => { + expect( + screen.queryByTestId( + 'confirmation-account-details-modal__account-name', + ), + ).not.toBeInTheDocument(); + }); + }); + + it('displays the transaction details section', async () => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitleDeployContract') as string), + ).toBeInTheDocument(); + + const simulationSection = screen.getByTestId('simulation-details-layout'); + expect(simulationSection).toBeInTheDocument(); + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsTitle') as string, + ); + const simulationDetailsRow = await screen.findByTestId( + 'simulation-rows-incoming', + ); + expect(simulationSection).toContainElement(simulationDetailsRow); + expect(simulationDetailsRow).toHaveTextContent( + tEn('simulationDetailsIncomingHeading') as string, + ); + expect(simulationDetailsRow).toContainElement( + screen.getByTestId('simulation-details-amount-pill'), + ); + + const transactionDetailsSection = screen.getByTestId( + 'transaction-details-section', + ); + expect(transactionDetailsSection).toBeInTheDocument(); + expect(transactionDetailsSection).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(transactionDetailsSection).toHaveTextContent( + tEn('interactingWith') as string, + ); + + const gasFeesSection = screen.getByTestId('gas-fee-section'); + expect(gasFeesSection).toBeInTheDocument(); + + const editGasFeesRow = + within(gasFeesSection).getByTestId('edit-gas-fees-row'); + expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); + + const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); + expect(firstGasField).toHaveTextContent('0.0001 ETH'); + const editGasFeeNativeCurrency = + within(editGasFeesRow).getByTestId('native-currency'); + expect(editGasFeeNativeCurrency).toHaveTextContent('$0.47'); + expect(editGasFeesRow).toContainElement( + screen.getByTestId('edit-gas-fee-icon'), + ); + + const gasFeeSpeed = within(gasFeesSection).getByTestId( + 'gas-fee-details-speed', + ); + expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string); + + const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time'); + expect(gasTimingTime).toHaveTextContent('~0 sec'); + }); + + it('sets the preference showConfirmationAdvancedDetails to true when advanced details button is clicked', async () => { + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ setPreference: {} }), + ); + + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + showConfirmationAdvancedDetails: false, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + fireEvent.click(screen.getByTestId('header-advanced-details-button')); + + await waitFor(() => { + expect( + mockedBackgroundConnection.callBackgroundMethod, + ).toHaveBeenCalledWith( + 'setPreference', + ['showConfirmationAdvancedDetails', true], + expect.anything(), + ); + }); + }); + + it('displays the advanced transaction details section', async () => { + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ setPreference: {} }), + ); + + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + showConfirmationAdvancedDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + await waitFor(() => { + expect( + mockedBackgroundConnection.submitRequestToBackground, + ).toHaveBeenCalledWith('getNextNonce', expect.anything()); + }); + + const gasFeesSection = screen.getByTestId('gas-fee-section'); + const maxFee = screen.getByTestId('gas-fee-details-max-fee'); + expect(gasFeesSection).toContainElement(maxFee); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); + expect(maxFee).toHaveTextContent('0.0023 ETH'); + expect(maxFee).toHaveTextContent('$7.72'); + + const nonceSection = screen.getByTestId('advanced-details-nonce-section'); + expect(nonceSection).toBeInTheDocument(); + expect(nonceSection).toHaveTextContent( + tEn('advancedDetailsNonceDesc') as string, + ); + expect(nonceSection).toContainElement( + screen.getByTestId('advanced-details-displayed-nonce'), + ); + expect( + screen.getByTestId('advanced-details-displayed-nonce'), + ).toHaveTextContent('9'); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('Deposit'); + + const transactionDataParams = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(transactionDataParams); + expect(transactionDataParams).toHaveTextContent('Number Of Tokens'); + expect(transactionDataParams).toHaveTextContent('1'); + }); +}); diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx index cd5953db50b8..1102cb21c67d 100644 --- a/test/integration/confirmations/transactions/contract-interaction.test.tsx +++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx @@ -1,25 +1,26 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; import { + act, fireEvent, + screen, waitFor, within, - screen, - act, } from '@testing-library/react'; -import { ApprovalType } from '@metamask/controller-utils'; import nock from 'nock'; -import { TransactionType } from '@metamask/transaction-controller'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; import { getMaliciousUnapprovedTransaction, - getUnapprovedTransaction, + getUnapprovedContractInteractionTransaction, } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -89,7 +90,7 @@ const getMetaMaskStateWithUnapprovedContractInteraction = ({ }, }, transactions: [ - getUnapprovedTransaction( + getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, @@ -261,18 +262,21 @@ describe('Contract Interaction Confirmation', () => { }); }); - expect(screen.getByText('Transaction request')).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleTransaction') as string), + ).toBeInTheDocument(); const simulationSection = screen.getByTestId('simulation-details-layout'); expect(simulationSection).toBeInTheDocument(); - expect(simulationSection).toHaveTextContent('Estimated changes'); + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsTitle') as string, + ); const simulationDetailsRow = await screen.findByTestId( 'simulation-rows-incoming', ); expect(simulationSection).toContainElement(simulationDetailsRow); - expect(simulationDetailsRow).toHaveTextContent('You receive'); - expect(simulationDetailsRow).toContainElement( - screen.getByTestId('simulation-details-asset-pill'), + expect(simulationDetailsRow).toHaveTextContent( + tEn('simulationDetailsIncomingHeading') as string, ); expect(simulationDetailsRow).toContainElement( screen.getByTestId('simulation-details-amount-pill'), @@ -282,15 +286,19 @@ describe('Contract Interaction Confirmation', () => { 'transaction-details-section', ); expect(transactionDetailsSection).toBeInTheDocument(); - expect(transactionDetailsSection).toHaveTextContent('Request from'); - expect(transactionDetailsSection).toHaveTextContent('Interacting with'); + expect(transactionDetailsSection).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(transactionDetailsSection).toHaveTextContent( + tEn('interactingWith') as string, + ); const gasFeesSection = screen.getByTestId('gas-fee-section'); expect(gasFeesSection).toBeInTheDocument(); const editGasFeesRow = within(gasFeesSection).getByTestId('edit-gas-fees-row'); - expect(editGasFeesRow).toHaveTextContent('Network fee'); + expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); expect(firstGasField).toHaveTextContent('0.0001 ETH'); @@ -304,7 +312,7 @@ describe('Contract Interaction Confirmation', () => { const gasFeeSpeed = within(gasFeesSection).getByTestId( 'gas-fee-details-speed', ); - expect(gasFeeSpeed).toHaveTextContent('Speed'); + expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string); const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time'); expect(gasTimingTime).toHaveTextContent('~0 sec'); @@ -393,13 +401,15 @@ describe('Contract Interaction Confirmation', () => { const gasFeesSection = screen.getByTestId('gas-fee-section'); const maxFee = screen.getByTestId('gas-fee-details-max-fee'); expect(gasFeesSection).toContainElement(maxFee); - expect(maxFee).toHaveTextContent('Max fee'); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); expect(maxFee).toHaveTextContent('0.0023 ETH'); expect(maxFee).toHaveTextContent('$7.72'); const nonceSection = screen.getByTestId('advanced-details-nonce-section'); expect(nonceSection).toBeInTheDocument(); - expect(nonceSection).toHaveTextContent('Nonce'); + expect(nonceSection).toHaveTextContent( + tEn('advancedDetailsNonceDesc') as string, + ); expect(nonceSection).toContainElement( screen.getByTestId('advanced-details-displayed-nonce'), ); @@ -414,7 +424,9 @@ describe('Contract Interaction Confirmation', () => { 'advanced-details-data-function', ); expect(dataSection).toContainElement(dataSectionFunction); - expect(dataSectionFunction).toHaveTextContent('Function'); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); expect(dataSectionFunction).toHaveTextContent('mintNFTs'); const transactionDataParams = screen.getByTestId( @@ -444,9 +456,8 @@ describe('Contract Interaction Confirmation', () => { }); }); - const headingText = 'This is a deceptive request'; - const bodyText = - 'If you approve this request, a third party known for scams will take all your assets.'; + const headingText = tEn('blockaidTitleDeceptive') as string; + const bodyText = tEn('blockaidDescriptionTransferFarming') as string; expect(screen.getByText(headingText)).toBeInTheDocument(); expect(screen.getByText(bodyText)).toBeInTheDocument(); }); diff --git a/test/integration/confirmations/transactions/erc20-approve.test.tsx b/test/integration/confirmations/transactions/erc20-approve.test.tsx index b6ab98774cb4..a2404ba75b09 100644 --- a/test/integration/confirmations/transactions/erc20-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc20-approve.test.tsx @@ -2,12 +2,13 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; -import { TokenStandard } from '../../../../shared/constants/transaction'; -import { createTestProviderTools } from '../../../stub/provider'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -161,9 +162,13 @@ describe('ERC20 Approve Confirmation', () => { }); }); - expect(screen.getByText('Spending cap request')).toBeInTheDocument(); expect( - screen.getByText('This site wants permission to withdraw your tokens'), + screen.getByText(tEn('confirmTitlePermitTokens') as string), + ).toBeInTheDocument(); + expect( + screen.getByText( + tEn('confirmTitleDescERC20ApproveTransaction') as string, + ), ).toBeInTheDocument(); }); @@ -184,9 +189,9 @@ describe('ERC20 Approve Confirmation', () => { expect(simulationSection).toBeInTheDocument(); expect(simulationSection).toHaveTextContent( - "You're giving someone else permission to spend this amount from your account.", + tEn('simulationDetailsERC20ApproveDesc') as string, ); - expect(simulationSection).toHaveTextContent('Spending cap'); + expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string); const spendingCapValue = screen.getByTestId('simulation-token-value'); expect(simulationSection).toContainElement(spendingCapValue); expect(spendingCapValue).toHaveTextContent('1'); @@ -213,7 +218,7 @@ describe('ERC20 Approve Confirmation', () => { ); expect(approveDetails).toContainElement(approveDetailsSpender); - expect(approveDetailsSpender).toHaveTextContent('Spender'); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); const spenderTooltip = screen.getByTestId( 'confirmation__approve-spender-tooltip', @@ -222,7 +227,7 @@ describe('ERC20 Approve Confirmation', () => { await testUser.hover(spenderTooltip); const spenderTooltipContent = await screen.findByText( - 'This is the address that will be able to spend your tokens on your behalf.', + tEn('spenderTooltipERC20ApproveDesc') as string, ); expect(spenderTooltipContent).toBeInTheDocument(); @@ -243,7 +248,7 @@ describe('ERC20 Approve Confirmation', () => { ); await testUser.hover(approveDetailsRequestFromTooltip); const requestFromTooltipContent = await screen.findByText( - 'This is the site asking for your confirmation.', + tEn('requestFromTransactionDescription') as string, ); expect(requestFromTooltipContent).toBeInTheDocument(); }); @@ -266,13 +271,15 @@ describe('ERC20 Approve Confirmation', () => { ); expect(spendingCapSection).toBeInTheDocument(); - expect(spendingCapSection).toHaveTextContent('Account balance'); + expect(spendingCapSection).toHaveTextContent( + tEn('accountBalance') as string, + ); expect(spendingCapSection).toHaveTextContent('0'); const spendingCapGroup = screen.getByTestId( 'confirmation__approve-spending-cap-group', ); expect(spendingCapSection).toContainElement(spendingCapGroup); - expect(spendingCapGroup).toHaveTextContent('Spending cap'); + expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string); expect(spendingCapGroup).toHaveTextContent('1'); const spendingCapGroupTooltip = screen.getByTestId( @@ -281,7 +288,7 @@ describe('ERC20 Approve Confirmation', () => { expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip); await testUser.hover(spendingCapGroupTooltip); const requestFromTooltipContent = await screen.findByText( - 'This is the amount of tokens the spender will be able to access on your behalf.', + tEn('spendingCapTooltipDesc') as string, ); expect(requestFromTooltipContent).toBeInTheDocument(); }); @@ -308,7 +315,9 @@ describe('ERC20 Approve Confirmation', () => { 'transaction-details-recipient-row', ); expect(approveDetails).toContainElement(approveDetailsRecipient); - expect(approveDetailsRecipient).toHaveTextContent('Interacting with'); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); const approveDetailsRecipientTooltip = screen.getByTestId( @@ -319,7 +328,7 @@ describe('ERC20 Approve Confirmation', () => { ); await testUser.hover(approveDetailsRecipientTooltip); const recipientTooltipContent = await screen.findByText( - "This is the contract you're interacting with. Protect yourself from scammers by verifying the details.", + tEn('interactingWithTransactionDescription') as string, ); expect(recipientTooltipContent).toBeInTheDocument(); @@ -327,7 +336,7 @@ describe('ERC20 Approve Confirmation', () => { 'transaction-details-method-data-row', ); expect(approveDetails).toContainElement(approveMethodData); - expect(approveMethodData).toHaveTextContent('Method'); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); expect(approveMethodData).toHaveTextContent('Approve'); const approveMethodDataTooltip = screen.getByTestId( 'transaction-details-method-data-row-tooltip', @@ -335,7 +344,7 @@ describe('ERC20 Approve Confirmation', () => { expect(approveMethodData).toContainElement(approveMethodDataTooltip); await testUser.hover(approveMethodDataTooltip); const approveMethodDataTooltipContent = await screen.findByText( - 'Function executed based on decoded input data.', + tEn('methodDataTransactionDesc') as string, ); expect(approveMethodDataTooltipContent).toBeInTheDocument(); @@ -351,7 +360,9 @@ describe('ERC20 Approve Confirmation', () => { 'advanced-details-data-function', ); expect(dataSection).toContainElement(dataSectionFunction); - expect(dataSectionFunction).toHaveTextContent('Function'); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); expect(dataSectionFunction).toHaveTextContent('Approve'); const approveDataParams1 = screen.getByTestId( diff --git a/test/integration/confirmations/transactions/erc721-approve.test.tsx b/test/integration/confirmations/transactions/erc721-approve.test.tsx index 8a836dbd7568..c3948d150b1d 100644 --- a/test/integration/confirmations/transactions/erc721-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc721-approve.test.tsx @@ -1,12 +1,14 @@ import { ApprovalType } from '@metamask/controller-utils'; -import { act, screen, waitFor } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; -import { TokenStandard } from '../../../../shared/constants/transaction'; -import { createTestProviderTools } from '../../../stub/provider'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -23,14 +25,21 @@ const backgroundConnectionMocked = { export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; export const pendingTransactionTime = new Date().getTime(); -const getMetaMaskStateWithUnapprovedApproveTransaction = ( - accountAddress: string, -) => { +const getMetaMaskStateWithUnapprovedApproveTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + return { ...mockMetaMaskState, preferences: { ...mockMetaMaskState.preferences, redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, }, pendingApprovals: { [pendingTransactionId]: { @@ -61,7 +70,7 @@ const getMetaMaskStateWithUnapprovedApproveTransaction = ( }, transactions: [ getUnapprovedApproveTransaction( - accountAddress, + account.address, pendingTransactionId, pendingTransactionTime, ), @@ -78,7 +87,7 @@ const advancedDetailsMockedRequests = { decodeTransactionData: { data: [ { - name: 'approve', + name: 'Approve', params: [ { type: 'address', @@ -129,7 +138,8 @@ describe('ERC721 Approve Confirmation', () => { }, }); const APPROVE_NFT_HEX_SIG = '0x095ea7b3'; - mock4byte(APPROVE_NFT_HEX_SIG); + const APPROVE_NFT_TEXT_SIG = 'approve(address,uint256)'; + mock4byte(APPROVE_NFT_HEX_SIG, APPROVE_NFT_TEXT_SIG); }); afterEach(() => { @@ -141,15 +151,28 @@ describe('ERC721 Approve Confirmation', () => { delete (global as any).ethereumProvider; }); - it('displays approve details with correct data', async () => { - const account = - mockMetaMaskState.internalAccounts.accounts[ - mockMetaMaskState.internalAccounts - .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts - ]; + it('displays spending cap request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction(); + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitleApproveTransaction') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); + + it('displays approve simulation section', async () => { const mockedMetaMaskState = - getMetaMaskStateWithUnapprovedApproveTransaction(account.address); + getMetaMaskStateWithUnapprovedApproveTransaction(); await act(async () => { await integrationTestRender({ @@ -158,12 +181,163 @@ describe('ERC721 Approve Confirmation', () => { }); }); - await waitFor(() => { - expect(screen.getByText('Allowance request')).toBeInTheDocument(); + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsApproveDesc') as string, + ); + expect(simulationSection).toHaveTextContent( + tEn('simulationApproveHeading') as string, + ); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent('1'); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); - await waitFor(() => { - expect(screen.getByText('Request from')).toBeInTheDocument(); + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('Approve'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('Approve'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('1'); }); }); diff --git a/test/integration/confirmations/transactions/increase-allowance.test.tsx b/test/integration/confirmations/transactions/increase-allowance.test.tsx new file mode 100644 index 000000000000..c288a5cc4e6d --- /dev/null +++ b/test/integration/confirmations/transactions/increase-allowance.test.tsx @@ -0,0 +1,384 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedIncreaseAllowanceTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, + }, + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'origin', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0x39509351': { + name: 'increaseAllowance', + params: [ + { + type: 'address', + }, + { + type: 'uint256', + }, + ], + }, + }, + transactions: [ + getUnapprovedIncreaseAllowanceTransaction( + account.address, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'increaseAllowance', + params: [ + { + type: 'address', + value: '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B', + }, + { + type: 'uint256', + value: 1, + }, + ], + }, + ], + source: 'FourByte', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); + + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ addKnownMethodData: {} }), + ); +}; + +describe('ERC20 increaseAllowance Confirmation', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'sepolia', + chainId: '0xaa36a7', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks({ + getTokenStandardAndDetails: { + standard: TokenStandard.ERC20, + }, + }); + const INCREASE_ALLOWANCE_ERC20_HEX_SIG = '0x39509351'; + const INCREASE_ALLOWANCE_ERC20_TEXT_SIG = + 'increaseAllowance(address,uint256)'; + mock4byte( + INCREASE_ALLOWANCE_ERC20_HEX_SIG, + INCREASE_ALLOWANCE_ERC20_TEXT_SIG, + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + afterAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).ethereumProvider; + }); + + it('displays spending cap request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitlePermitTokens') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescPermitSignature') as string), + ).toBeInTheDocument(); + }); + + it('displays increase allowance simulation section', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsERC20ApproveDesc') as string, + ); + expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent('1'); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipERC20ApproveDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent('Request from'); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays spending cap section with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const spendingCapSection = screen.getByTestId( + 'confirmation__approve-spending-cap-section', + ); + expect(spendingCapSection).toBeInTheDocument(); + + expect(spendingCapSection).toHaveTextContent( + tEn('accountBalance') as string, + ); + expect(spendingCapSection).toHaveTextContent('0'); + const spendingCapGroup = screen.getByTestId( + 'confirmation__approve-spending-cap-group', + ); + expect(spendingCapSection).toContainElement(spendingCapGroup); + expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string); + expect(spendingCapGroup).toHaveTextContent('1'); + + const spendingCapGroupTooltip = screen.getByTestId( + 'confirmation__approve-spending-cap-group-tooltip', + ); + expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip); + await testUser.hover(spendingCapGroupTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('spendingCapTooltipDesc') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('increaseAllowance'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('increaseAllowance'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('1'); + }); +}); diff --git a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx new file mode 100644 index 000000000000..a65688030e90 --- /dev/null +++ b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx @@ -0,0 +1,348 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedSetApprovalForAllTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, + }, + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'origin', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0xa22cb465': { + name: 'setApprovalForAll', + params: [ + { + type: 'address', + }, + { + type: 'bool', + }, + ], + }, + }, + transactions: [ + getUnapprovedSetApprovalForAllTransaction( + account.address, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'setApprovalForAll', + params: [ + { + type: 'address', + value: '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B', + }, + { + type: 'bool', + value: true, + }, + ], + }, + ], + source: 'FourByte', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); + + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ addKnownMethodData: {} }), + ); +}; + +describe('ERC721 setApprovalForAll Confirmation', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'sepolia', + chainId: '0xaa36a7', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks({ + getTokenStandardAndDetails: { + standard: TokenStandard.ERC721, + }, + }); + const INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG = '0xa22cb465'; + const INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG = + 'setApprovalForAll(address,bool)'; + mock4byte( + INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG, + INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG, + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + afterAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).ethereumProvider; + }); + + it('displays set approval for all request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('setApprovalForAllRedesignedTitle') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); + + it('displays set approval for all simulation section', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsSetApprovalForAllDesc') as string, + ); + expect(simulationSection).toHaveTextContent(tEn('withdrawing') as string); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent(tEn('all') as string); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent( + tEn('permissionFor') as string, + ); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('setApprovalForAll'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('setApprovalForAll'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('true'); + }); +}); diff --git a/test/integration/confirmations/transactions/transactionDataHelpers.tsx b/test/integration/confirmations/transactions/transactionDataHelpers.tsx index 12550ea5e563..e9bcd7b818f2 100644 --- a/test/integration/confirmations/transactions/transactionDataHelpers.tsx +++ b/test/integration/confirmations/transactions/transactionDataHelpers.tsx @@ -1,6 +1,6 @@ import { TransactionType } from '@metamask/transaction-controller'; -export const getUnapprovedTransaction = ( +export const getUnapprovedContractInteractionTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, @@ -70,37 +70,105 @@ export const getUnapprovedTransaction = ( }; }; +export const getUnapprovedContractDeploymentTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0xd0e30db0', + }, + type: TransactionType.deployContract, + }; +}; + export const getUnapprovedApproveTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, ) => { return { - ...getUnapprovedTransaction( + ...getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, ), txParams: { - from: accountAddress, + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', }, type: TransactionType.tokenMethodApprove, }; }; +export const getUnapprovedIncreaseAllowanceTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0x395093510000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000007530', + }, + type: TransactionType.tokenMethodIncreaseAllowance, + }; +}; + +export const getUnapprovedSetApprovalForAllTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0xa22cb4650000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000000001', + }, + type: TransactionType.tokenMethodSetApprovalForAll, + }; +}; + export const getMaliciousUnapprovedTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, ) => { return { - ...getUnapprovedTransaction( + ...getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, From d26968f7ea2fff46688a14173635fde5e667ccf3 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 2 Oct 2024 15:04:19 +0200 Subject: [PATCH 041/226] feat: aggregated balance feature (#27097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - [x] Removes the primary currency from Settings => General - [x] Removes usage of useNativeCurrencyAsPrimaryCurrency - [x] Adds a new toggle in settings: "show native tokens as main balance" which affects only the main coin overview. - [x] When new setting is ON we will show to users the aggregated balance of their tokens in Fiat, else we show the balance in crypto for the native token. ## This final PR is the combination of the following PRs: 1- Removal of useNativeCurrencyAsPrimaryCurrency setting and adding the new setting: https://github.com/MetaMask/metamask-extension/pull/26870 2- Aggregated balance logic: https://github.com/MetaMask/metamask-extension/pull/27108 3- Aggregated balance UI: https://github.com/MetaMask/metamask-extension/pull/27161 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26870?quickstart=1) ## **Related issues** Fixes: https://github.com/orgs/MetaMask/projects/85/views/24?filterQuery=label%3A%22assets-aggregated%22 Figma: https://www.figma.com/design/aMYisczaJyEsYl1TYdcPUL/Portfolio-View?node-id=4098-126568&node-type=instance&focus-id=4186-130770&m=dev https://github.com/MetaMask/metamask-extension/issues/27280 ## **Manual testing steps** 1. Switch to Ethereum mainnet and go to home page 2. You should see the popover telling you about the new feature. You should not be able to see the popover again once closed. 3. You should be able to see the aggregated balance in fiat of you native token and imported ERC20 tokens 4. Go to settings; notice that there is no primary currency setting and you should see the new setting. 5. Turn on "show native token as main balance" and go back to home page; you should see your balance in crypto for the native token. 6. Switch to any testnet, exp (Sepolia), notice that you wont be able to see aggregated balance unless you turn on the setting "Show conversion on test networks" in settings => Advanced ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/ddab8290-8f9f-4a27-8821-3f416ed35b53 ## **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. --- .storybook/test-data.js | 2 +- app/_locales/am/messages.json | 13 - app/_locales/ar/messages.json | 9 - app/_locales/bg/messages.json | 9 - app/_locales/bn/messages.json | 13 - app/_locales/ca/messages.json | 9 - app/_locales/cs/messages.json | 4 - app/_locales/da/messages.json | 9 - app/_locales/de/messages.json | 16 - app/_locales/el/messages.json | 16 - app/_locales/en/messages.json | 28 +- app/_locales/en_GB/messages.json | 16 - app/_locales/es/messages.json | 16 - app/_locales/es_419/messages.json | 16 - app/_locales/et/messages.json | 9 - app/_locales/fa/messages.json | 13 - app/_locales/fi/messages.json | 13 - app/_locales/fil/messages.json | 9 - app/_locales/fr/messages.json | 16 - app/_locales/he/messages.json | 13 - app/_locales/hi/messages.json | 16 - app/_locales/hn/messages.json | 4 - app/_locales/hr/messages.json | 9 - app/_locales/ht/messages.json | 13 - app/_locales/hu/messages.json | 9 - app/_locales/id/messages.json | 16 - app/_locales/it/messages.json | 13 - app/_locales/ja/messages.json | 16 - app/_locales/kn/messages.json | 13 - app/_locales/ko/messages.json | 16 - app/_locales/lt/messages.json | 13 - app/_locales/lv/messages.json | 9 - app/_locales/ms/messages.json | 9 - app/_locales/nl/messages.json | 4 - app/_locales/no/messages.json | 9 - app/_locales/ph/messages.json | 13 - app/_locales/pl/messages.json | 13 - app/_locales/pt/messages.json | 16 - app/_locales/pt_BR/messages.json | 16 - app/_locales/ro/messages.json | 9 - app/_locales/ru/messages.json | 16 - app/_locales/sk/messages.json | 13 - app/_locales/sl/messages.json | 13 - app/_locales/sr/messages.json | 13 - app/_locales/sv/messages.json | 9 - app/_locales/sw/messages.json | 9 - app/_locales/ta/messages.json | 4 - app/_locales/th/messages.json | 7 - app/_locales/tl/messages.json | 16 - app/_locales/tr/messages.json | 16 - app/_locales/uk/messages.json | 13 - app/_locales/vi/messages.json | 16 - app/_locales/zh_CN/messages.json | 16 - app/_locales/zh_TW/messages.json | 13 - app/scripts/constants/sentry-state.ts | 2 +- app/scripts/controllers/metametrics.js | 4 +- app/scripts/controllers/metametrics.test.js | 14 +- .../controllers/preferences-controller.ts | 4 + app/scripts/migrations/128.test.ts | 39 ++ app/scripts/migrations/128.ts | 42 ++ app/scripts/migrations/129.test.ts | 60 ++ app/scripts/migrations/129.ts | 47 ++ app/scripts/migrations/index.js | 2 + shared/constants/metametrics.ts | 6 +- shared/modules/currency-display.utils.test.ts | 38 -- shared/modules/currency-display.utils.ts | 31 - test/data/mock-send-state.json | 3 +- test/data/mock-state.json | 6 +- test/e2e/default-fixture.js | 3 +- test/e2e/fixture-builder.js | 19 +- test/e2e/restore/MetaMaskUserData.json | 3 +- .../dapp-interactions/encrypt-decrypt.spec.js | 8 +- ...rs-after-init-opt-in-background-state.json | 5 +- .../errors-after-init-opt-in-ui-state.json | 5 +- ...s-before-init-opt-in-background-state.json | 5 +- .../errors-before-init-opt-in-ui-state.json | 5 +- .../tests/settings/account-token-list.spec.js | 26 +- .../tests/settings/change-language.spec.ts | 4 +- test/e2e/tests/settings/localization.spec.js | 15 +- .../tests/settings/settings-search.spec.js | 2 +- .../show-native-as-main-balance.spec.ts | 240 +++++++ test/e2e/tests/transaction/send-eth.spec.js | 2 + .../data/integration-init-state.json | 1 - .../data/onboarding-completion-route.json | 1 - .../app/assets/asset-list/asset-list.tsx | 33 +- .../__snapshots__/token-cell.test.tsx.snap | 4 +- .../app/confirm/info/row/currency.stories.tsx | 4 +- .../app/confirm/info/row/currency.tsx | 3 +- ...ncel-transaction-gas-fee.component.test.js | 4 +- .../customize-nonce.test.js.snap | 2 +- .../transaction-breakdown.component.js | 2 +- .../transaction-list-item.component.test.js | 6 +- ...referenced-currency-display.component.d.ts | 2 + ...-preferenced-currency-display.component.js | 3 + .../user-preferenced-currency-display.test.js | 4 +- ...er-preferenced-currency-input.component.js | 14 +- ...er-preferenced-currency-input.container.js | 4 - .../user-preferenced-token-input.component.js | 12 +- .../user-preferenced-token-input.container.js | 9 +- ...gregated-percentage-overview.test.tsx.snap | 23 + .../aggregated-percentage-overview.test.tsx | 592 ++++++++++++++++++ .../aggregated-percentage-overview.tsx | 143 +++++ .../app/wallet-overview/btc-overview.test.tsx | 24 +- .../wallet-overview/coin-buttons.stories.js | 37 ++ .../app/wallet-overview/coin-buttons.tsx | 41 +- .../app/wallet-overview/coin-overview.tsx | 274 ++++++-- .../app/wallet-overview/eth-overview.test.js | 2 +- ui/components/app/wallet-overview/index.scss | 49 +- ...active-replacement-token-modal.stories.tsx | 4 +- ...teractive-replacement-token-modal.test.tsx | 4 +- ...replacement-token-notification.stories.tsx | 4 +- ...ve-replacement-token-notification.test.tsx | 4 +- .../asset-balance/asset-balance-text.test.tsx | 1 - .../asset-picker-modal/AssetList.tsx | 12 +- .../asset-picker-modal.test.tsx | 5 +- .../nft-input/nft-input.test.tsx | 9 +- .../swappable-currency-input.test.tsx | 10 +- .../asset-picker-amount/utils.test.ts | 30 +- .../multichain/asset-picker-amount/utils.ts | 19 +- .../multichain/pages/send/send.test.js | 1 - .../token-list-item.test.tsx.snap | 2 +- ...percentage-and-amount-change.test.tsx.snap | 4 +- .../percentage-and-amount-change.test.tsx | 4 +- .../percentage-and-amount-change.tsx | 9 +- .../percentage-change.test.tsx.snap | 2 +- .../percentage-change/percentage-change.tsx | 4 +- .../token-list-item/token-list-item.test.tsx | 10 +- .../token-list-item/token-list-item.tsx | 2 +- .../currency-display.component.js | 3 + ui/components/ui/dropdown/dropdown.scss | 6 +- ui/components/ui/icon-button/icon-button.js | 7 +- ui/components/ui/icon-button/icon-button.scss | 8 +- .../ui/text-field/text-field.component.js | 4 +- ui/ducks/app/app.ts | 2 +- ui/ducks/metamask/metamask.js | 1 - ui/helpers/constants/settings.js | 6 +- ui/helpers/utils/settings-search.js | 9 + ui/helpers/utils/settings-search.test.js | 14 + ui/helpers/utils/util.js | 15 + ui/helpers/utils/util.test.js | 33 + ui/hooks/useCurrencyDisplay.js | 7 + ui/hooks/useTransactionDisplayData.test.js | 1 - ui/hooks/useUserPreferencedCurrency.js | 20 +- ui/hooks/useUserPreferencedCurrency.test.js | 137 ++-- .../__snapshots__/asset-page.test.tsx.snap | 48 +- ui/pages/asset/components/asset-page.test.tsx | 4 +- ui/pages/asset/components/token-buttons.tsx | 28 +- .../confirm-decrypt-message.container.js | 9 +- ...cryption-public-key.component.test.js.snap | 246 -------- ...confirm-encryption-public-key.component.js | 36 +- ...rm-encryption-public-key.component.test.js | 13 - ...confirm-encryption-public-key.container.js | 9 - .../confirm-gas-display.test.js | 4 +- .../confirm-legacy-gas-display.js | 6 +- .../confirm-legacy-gas-display.test.js | 3 - .../confirm-detail-row.component.test.js | 4 +- .../confirm-subtitle/confirm-subtitle.js | 1 - .../edit-gas-fees-row/edit-gas-fees-row.tsx | 9 +- .../info/shared/gas-fees-row/gas-fees-row.tsx | 11 +- .../fee-details-component.js | 13 +- .../gas-details-item/gas-details-item.js | 7 +- .../gas-details-item/gas-details-item.test.js | 4 +- .../signature-request-header.js | 9 +- .../signature-request.test.js | 4 +- .../simulation-details.stories.tsx | 2 +- .../confirm-approve-content.component.js | 5 +- .../confirm-approve-content.component.test.js | 4 +- .../confirm-approve/confirm-approve.js | 5 - .../confirm-transaction-base.test.js.snap | 14 +- .../confirm-transaction-base.component.js | 14 +- .../confirm-transaction-base.container.js | 3 - .../confirm-transaction-base.test.js | 4 +- ui/pages/confirmations/hooks/test-utils.js | 4 +- .../send/gas-display/gas-display.js | 10 +- ui/pages/home/index.scss | 1 + .../confirm-add-custodian-token.test.tsx | 8 +- .../confirm-connect-custodian-modal.test.tsx | 4 +- .../institutional/custody/custody.test.tsx | 4 +- .../advanced-tab.component.test.js.snap | 2 +- ui/pages/settings/index.scss | 16 +- .../__snapshots__/security-tab.test.js.snap | 2 +- .../settings-search/settings-search.js | 29 +- .../settings-tab/settings-tab.component.js | 153 ++--- .../settings-tab/settings-tab.container.js | 14 +- .../settings-tab/settings-tab.test.js | 43 +- .../dropdown-input-pair.test.js.snap | 2 +- .../searchable-item-list.test.js.snap | 2 +- ui/selectors/selectors.js | 5 + ui/store/actions.ts | 14 +- 189 files changed, 2118 insertions(+), 1719 deletions(-) create mode 100644 app/scripts/migrations/128.test.ts create mode 100644 app/scripts/migrations/128.ts create mode 100644 app/scripts/migrations/129.test.ts create mode 100644 app/scripts/migrations/129.ts delete mode 100644 shared/modules/currency-display.utils.test.ts delete mode 100644 shared/modules/currency-display.utils.ts create mode 100644 test/e2e/tests/settings/show-native-as-main-balance.spec.ts create mode 100644 ui/components/app/wallet-overview/__snapshots__/aggregated-percentage-overview.test.tsx.snap create mode 100644 ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx create mode 100644 ui/components/app/wallet-overview/aggregated-percentage-overview.tsx create mode 100644 ui/components/app/wallet-overview/coin-buttons.stories.js diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 72a9bc3b78aa..de94b69f857e 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -676,7 +676,7 @@ const state = { welcomeScreenSeen: false, currentLocale: 'en', preferences: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }, incomingTransactionsPreferences: { [CHAIN_IDS.MAINNET]: true, diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 4ce44a388ac1..f118bc17df41 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -235,10 +235,6 @@ "fast": { "message": "ፈጣን" }, - "fiat": { - "message": "ፊያት", - "description": "Exchange type" - }, "fileImportFail": { "message": "ፋይል ማስመጣት እየሰራ አይደለም? እዚህ ላይ ጠቅ ያድርጉ!", "description": "Helps user import their account from a JSON file" @@ -493,12 +489,6 @@ "prev": { "message": "የቀደመ" }, - "primaryCurrencySetting": { - "message": "ተቀዳሚ የገንዘብ ዓይነት" - }, - "primaryCurrencySettingDescription": { - "message": "ዋጋዎች በራሳቸው የሰንሰለት ገንዘብ ዓይነት (ለምሳሌ ETH) በቅድሚያ እንዲታዪ ይምረጡ። ዋጋዎች በተመረጠ የፊያት ገንዘብ ዓይነት እንዲታዩ ደግሞ ፊያትን ይምረጡ።" - }, "privacyMsg": { "message": "የግለኝነት መጠበቂያ ህግ" }, @@ -750,9 +740,6 @@ "unlockMessage": { "message": "ያልተማከለ ድር ይጠባበቃል" }, - "updatedWithDate": { - "message": "የዘመነ $1" - }, "urlErrorMsg": { "message": "URIs አግባብነት ያለው የ HTTP/HTTPS ቅድመ ቅጥያ ይፈልጋል።" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 6cb79c56b136..d9717df6b190 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -505,12 +505,6 @@ "prev": { "message": "السابق" }, - "primaryCurrencySetting": { - "message": "العملة الأساسية" - }, - "primaryCurrencySettingDescription": { - "message": "حدد خيار \"المحلية\" لتحديد أولويات عرض القيم بالعملة المحلية للسلسلة (مثلاً ETH). حدد Fiat لتحديد أولويات عرض القيم بعملات fiat المحددة الخاصة بك." - }, "privacyMsg": { "message": "سياسة الخصوصية" }, @@ -762,9 +756,6 @@ "unlockMessage": { "message": "شبكة الويب اللامركزية بانتظارك" }, - "updatedWithDate": { - "message": "تم تحديث $1" - }, "urlErrorMsg": { "message": "تتطلب الروابط بادئة HTTP/HTTPS مناسبة." }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 1fa7a14393d4..749b1561dafe 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -504,12 +504,6 @@ "prev": { "message": "Предишен" }, - "primaryCurrencySetting": { - "message": "Основна валута" - }, - "primaryCurrencySettingDescription": { - "message": "Изберете местна, за да приоритизирате показването на стойности в основната валута на веригата (например ETH). Изберете Fiat, за да поставите приоритет на показването на стойности в избраната от вас fiat валута." - }, "privacyMsg": { "message": "Политика за поверителност" }, @@ -761,9 +755,6 @@ "unlockMessage": { "message": "Децентрализираната мрежа очаква" }, - "updatedWithDate": { - "message": "Актуализирано $1 " - }, "urlErrorMsg": { "message": "URI изискват съответния HTTP / HTTPS префикс." }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index a9cc5aa0d845..15acaa2e6765 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -241,10 +241,6 @@ "fast": { "message": "দ্রুত" }, - "fiat": { - "message": "ফিয়াট", - "description": "Exchange type" - }, "fileImportFail": { "message": "ফাইল আমদানি কাজ করছে না? এখানে ক্লিক করুন!", "description": "Helps user import their account from a JSON file" @@ -502,12 +498,6 @@ "prev": { "message": "পূর্ববর্তী" }, - "primaryCurrencySetting": { - "message": "প্রাথমিক মুদ্রা" - }, - "primaryCurrencySettingDescription": { - "message": "চেনটিতে (যেমন ETH) দেশীয় মুদ্রায় মানগুলি প্রদর্শনকে অগ্রাধিকার দিতে দেশীয় নির্বাচন করুন। আপনার নির্দেশিত মুদ্রায় মানগুলির প্রদর্শনকে অগ্রাধিকার দিতে নির্দেশিত নির্বাচন করুন।" - }, "privacyMsg": { "message": "সম্মত হয়েছেন" }, @@ -759,9 +749,6 @@ "unlockMessage": { "message": "ছড়িয়ে ছিটিয়ে থাকা ওয়েব অপেক্ষা করছে" }, - "updatedWithDate": { - "message": "আপডেট করা $1" - }, "urlErrorMsg": { "message": "URI গুলির যথাযথ HTTP/HTTPS প্রেফিক্সের প্রয়োজন।" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 92f2b2771ff9..fc9e2afb41e6 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -489,12 +489,6 @@ "personalAddressDetected": { "message": "Adreça personal detectada. Introduir l'adreça del contracte de fitxa." }, - "primaryCurrencySetting": { - "message": "Divisa principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecciona Natiu per a prioritzar la mostra de valors en la divisa nadiua de la cadena (p. ex. ETH). Selecciona Fiat per prioritzar la mostra de valors en la divisa fiduciària seleccionada." - }, "privacyMsg": { "message": "Política de privadesa" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "La web descentralitzada està esperant" }, - "updatedWithDate": { - "message": "Actualitzat $1" - }, "urlErrorMsg": { "message": "Els URIs requereixen el prefix HTTP/HTTPS apropiat." }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 6e3bfa315303..4113f8c5cc42 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -105,10 +105,6 @@ "failed": { "message": "Neúspěšné" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Import souboru nefunguje? Klikněte sem!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 12eba292e0a4..37e4663523cf 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -489,12 +489,6 @@ "prev": { "message": "Forrige" }, - "primaryCurrencySetting": { - "message": "Primær Valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Vælg lokal for fortrinsvis at vise værdier i kædens (f.eks. ETH) lokale valuta. Vælg Fiat for fortrinsvis at vise værdier i din valgte fiat valuta." - }, "privacyMsg": { "message": "Privatlivspolitik" }, @@ -734,9 +728,6 @@ "unlockMessage": { "message": "Det decentraliserede internet venter" }, - "updatedWithDate": { - "message": "Opdaterede $1" - }, "urlErrorMsg": { "message": "Links kræver det rette HTTP/HTTPS-præfix." }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 5d5f0e30923f..3ff80228ef4f 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Details zur Gebühr" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Dateiimport fehlgeschlagen? Bitte hier klicken!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, - "noConversionDateAvailable": { - "message": "Kein Umrechnungskursdaten verfügbar" - }, "noConversionRateAvailable": { "message": "Kein Umrechnungskurs verfügbar" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "Preis nicht verfügbar" }, - "primaryCurrencySetting": { - "message": "Hauptwährung" - }, - "primaryCurrencySettingDescription": { - "message": "Wählen Sie 'Nativ', um dem Anzeigen von Werten in der nativen Währung der Chain (z. B. ETH) Vorrang zu geben. Wählen Sie 'Fiat', um dem Anzeigen von Werten in Ihrer gewählten Fiat-Währung Vorrang zu geben." - }, "primaryType": { "message": "Primärer Typ" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Aktualisierungsanfrage" }, - "updatedWithDate": { - "message": "$1 aktualisiert" - }, "uploadDropFile": { "message": "Legen Sie Ihre Datei hier ab" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 340670a11bc9..c348e7bb6cbf 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Λεπτομέρειες χρεώσεων" }, - "fiat": { - "message": "Εντολή", - "description": "Exchange type" - }, "fileImportFail": { "message": "Η εισαγωγή αρχείων δεν λειτουργεί; Κάντε κλικ εδώ!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, - "noConversionDateAvailable": { - "message": "Δεν υπάρχει διαθέσιμη ημερομηνία μετατροπής νομίσματος" - }, "noConversionRateAvailable": { "message": "Δεν υπάρχει διαθέσιμη ισοτιμία μετατροπής" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "μη διαθέσιμη τιμή" }, - "primaryCurrencySetting": { - "message": "Κύριο νόμισμα" - }, - "primaryCurrencySettingDescription": { - "message": "Επιλέξτε εγχώριο για να δώσετε προτεραιότητα στην εμφάνιση των τιμών στο νόμισμα της αλυσίδας (π.χ. ETH). Επιλέξτε Παραστατικό για να δώσετε προτεραιότητα στην εμφάνιση τιμών στο επιλεγμένο παραστατικό νόμισμα." - }, "primaryType": { "message": "Βασικός τύπος" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Αίτημα ενημέρωσης" }, - "updatedWithDate": { - "message": "Ενημερώθηκε $1" - }, "uploadDropFile": { "message": "Αφήστε το αρχείο σας εδώ" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 42a5a108fdf1..1ddcd1c05a6b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -402,6 +402,10 @@ "advancedPriorityFeeToolTip": { "message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." }, + "aggregatedBalancePopover": { + "message": "This reflects the value of all tokens you own on a given network. If you prefer seeing this value in ETH or other currencies, go to $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "I agree to MetaMask's $1", "description": "$1 is the `terms` link" @@ -1347,7 +1351,7 @@ "message": "CryptoCompare" }, "currencyConversion": { - "message": "Currency conversion" + "message": "Currency" }, "currencyRateCheckToggle": { "message": "Show balance and token price checker" @@ -2032,10 +2036,6 @@ "feeDetails": { "message": "Fee details" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" @@ -3302,9 +3302,6 @@ "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" - }, "noConversionRateAvailable": { "message": "No conversion rate available" }, @@ -4145,12 +4142,6 @@ "priceUnavailable": { "message": "price unavailable" }, - "primaryCurrencySetting": { - "message": "Primary currency" - }, - "primaryCurrencySettingDescription": { - "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." - }, "primaryType": { "message": "Primary type" }, @@ -4885,6 +4876,9 @@ "showMore": { "message": "Show more" }, + "showNativeTokenAsMainBalance": { + "message": "Show native token as main balance" + }, "showNft": { "message": "Show NFT" }, @@ -6420,9 +6414,6 @@ "updatedRpcForNetworks": { "message": "Network RPCs Updated" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, @@ -6679,6 +6670,9 @@ "yourBalance": { "message": "Your balance" }, + "yourBalanceIsAggregated": { + "message": "Your balance is aggregated" + }, "yourNFTmayBeAtRisk": { "message": "Your NFT may be at risk" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 80a17b1c5e22..71599915880e 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1934,10 +1934,6 @@ "feeDetails": { "message": "Fee details" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" @@ -3166,9 +3162,6 @@ "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" - }, "noConversionRateAvailable": { "message": "No conversion rate available" }, @@ -4007,12 +4000,6 @@ "priceUnavailable": { "message": "price unavailable" }, - "primaryCurrencySetting": { - "message": "Primary currency" - }, - "primaryCurrencySettingDescription": { - "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." - }, "primaryType": { "message": "Primary type" }, @@ -6225,9 +6212,6 @@ "updateRequest": { "message": "Update request" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 599168440ee5..05015a3de622 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1870,10 +1870,6 @@ "feeDetails": { "message": "Detalles de la tarifa" }, - "fiat": { - "message": "Fiduciaria", - "description": "Exchange type" - }, "fileImportFail": { "message": "¿No funciona la importación del archivo? Haga clic aquí.", "description": "Helps user import their account from a JSON file" @@ -3089,9 +3085,6 @@ "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -3912,12 +3905,6 @@ "priceUnavailable": { "message": "precio no disponible" }, - "primaryCurrencySetting": { - "message": "Moneda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." - }, "primaryType": { "message": "Tipo principal" }, @@ -6082,9 +6069,6 @@ "updateRequest": { "message": "Solicitud de actualización" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "uploadDropFile": { "message": "Ingrese su archivo aquí" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 1b8bc945343b..0bba1bd69551 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -782,10 +782,6 @@ "feeAssociatedRequest": { "message": "Esta solicitud tiene asociada una tarifa." }, - "fiat": { - "message": "Fiduciaria", - "description": "Exchange type" - }, "fileImportFail": { "message": "¿No funciona la importación del archivo? ¡Haga clic aquí!", "description": "Helps user import their account from a JSON file" @@ -1321,9 +1317,6 @@ "noAccountsFound": { "message": "No se encuentran cuentas para la consulta de búsqueda determinada" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -1489,12 +1482,6 @@ "prev": { "message": "Ant." }, - "primaryCurrencySetting": { - "message": "Moneda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." - }, "priorityFee": { "message": "Tarifa de prioridad" }, @@ -2405,9 +2392,6 @@ "message": "El envío de tokens coleccionables (ERC-721) no se admite actualmente", "description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "urlErrorMsg": { "message": "Las direcciones URL requieren el prefijo HTTP/HTTPS adecuado." }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index acebcc9091da..38125572b8ec 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -498,12 +498,6 @@ "prev": { "message": "Eelm" }, - "primaryCurrencySetting": { - "message": "Põhivaluuta" - }, - "primaryCurrencySettingDescription": { - "message": "Valige omavääring, et prioriseerida vääringu kuvamist ahela omavääringus (nt ETH). Valige Fiat, et prioriseerida vääringu kuvamist valitud fiat-vääringus." - }, "privacyMsg": { "message": "privaatsuspoliitika" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "Detsentraliseeritud veeb ootab" }, - "updatedWithDate": { - "message": "Värskendatud $1" - }, "urlErrorMsg": { "message": "URI-d nõuavad sobivat HTTP/HTTPS-i prefiksit." }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index c9c1bafdc7bf..c1a4deb11ce4 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "سریع" }, - "fiat": { - "message": "حکم قانونی", - "description": "Exchange type" - }, "fileImportFail": { "message": "وارد کردن فایل کار نمیکند؟ اینجا کلیک نمایید!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "قبلی" }, - "primaryCurrencySetting": { - "message": "واحد پول اصلی" - }, - "primaryCurrencySettingDescription": { - "message": "برای اولویت دهی نمایش قیمت ها در واحد پولی اصلی زنجیره (مثلًا ETH)، اصلی را انتخاب کنید. برای اولویت دهی نمایش قیمت ها در فیات واحد پولی شما، فیات را انتخاب کنید." - }, "privacyMsg": { "message": "خط‌مشی رازداری" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "وب غیر متمرکز شده انتظار میکشد" }, - "updatedWithDate": { - "message": "بروزرسانی شد 1$1" - }, "urlErrorMsg": { "message": "URl ها نیازمند پیشوند مناسب HTTP/HTTPS اند." }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 1c9cdb7c7a43..89e274dd4466 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Nopea" }, - "fiat": { - "message": "Kiinteä", - "description": "Exchange type" - }, "fileImportFail": { "message": "Eikö tiedoston tuominen onnistu? Klikkaa tästä!", "description": "Helps user import their account from a JSON file" @@ -505,12 +501,6 @@ "prev": { "message": "Aiemp." }, - "primaryCurrencySetting": { - "message": "Ensisijainen valuutta" - }, - "primaryCurrencySettingDescription": { - "message": "Valitse natiivivaihtoehto näyttääksesi arvot ensisijaisesti ketjun natiivivaluutalla (esim. ETH). Valitse oletusmääräys asettaaksesi valitsemasi oletusvaluutan ensisijaiseksi." - }, "privacyMsg": { "message": "Tietosuojakäytäntö" }, @@ -762,9 +752,6 @@ "unlockMessage": { "message": "Hajautettu verkko odottaa" }, - "updatedWithDate": { - "message": "$1 päivitetty" - }, "urlErrorMsg": { "message": "URI:t vaativat asianmukaisen HTTP/HTTPS-etuliitteen." }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index e08c88bd7ffa..498c1878fd10 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -436,12 +436,6 @@ "prev": { "message": "Nakaraan" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para bigyang priyoridad ang pagpapakita ng mga halaga sa native currency ng chain (hal. ETH). Piliin ang Fiat para bigyang priyoridad ang pagpapakita ng mga halaga sa napili mong fiat currency." - }, "privacyMsg": { "message": "Patakaran sa Privacy" }, @@ -677,9 +671,6 @@ "unlockMessage": { "message": "Naghihintay ang decentralized web" }, - "updatedWithDate": { - "message": "Na-update ang $1" - }, "urlErrorMsg": { "message": "Kinakailangan ng mga URI ang naaangkop na HTTP/HTTPS prefix." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 84124b8f1fff..8301ad348b07 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Détails des frais" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "L’importation de fichier ne fonctionne pas ? Cliquez ici !", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, - "noConversionDateAvailable": { - "message": "Aucune date de conversion des devises n’est disponible" - }, "noConversionRateAvailable": { "message": "Aucun taux de conversion disponible" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "prix non disponible" }, - "primaryCurrencySetting": { - "message": "Devise principale" - }, - "primaryCurrencySettingDescription": { - "message": "Sélectionnez « natif » pour donner la priorité à l’affichage des valeurs dans la devise native de la chaîne (par ex. ETH). Sélectionnez « fiduciaire » pour donner la priorité à l’affichage des valeurs dans la devise de votre choix." - }, "primaryType": { "message": "Type principal" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Demande de mise à jour" }, - "updatedWithDate": { - "message": "Mis à jour $1" - }, "uploadDropFile": { "message": "Déposez votre fichier ici" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 9d118e31c098..413bf21d586b 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "מהיר" }, - "fiat": { - "message": "פיאט", - "description": "Exchange type" - }, "fileImportFail": { "message": "ייבוא הקובץ לא עובד? לחצ/י כאן!", "description": "Helps user import their account from a JSON file" @@ -505,12 +501,6 @@ "prev": { "message": "הקודם" }, - "primaryCurrencySetting": { - "message": "מטבע ראשי" - }, - "primaryCurrencySettingDescription": { - "message": "בחר/י 'מקומי' כדי לתעדף הצגת ערכים במטבע המקומי של הצ'יין (למשל ETH). בחר/י פיאט כדי לתעדף הצגת ערכים במטבע הפיאט שבחרת." - }, "privacyMsg": { "message": "מדיניות הפרטיות" }, @@ -762,9 +752,6 @@ "unlockMessage": { "message": "הרשת המבוזרת מחכה" }, - "updatedWithDate": { - "message": "עודכן $1" - }, "urlErrorMsg": { "message": "כתובות URI דורשות את קידומת HTTP/HTTPS המתאימה." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index fd957c6925df..0a5423979efa 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "फ़ीस का ब्यौरा" }, - "fiat": { - "message": "फिएट", - "description": "Exchange type" - }, "fileImportFail": { "message": "फाइल इम्पोर्ट काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, - "noConversionDateAvailable": { - "message": "कोई करेंसी कन्वर्शन तारीख उपलब्ध नहीं है" - }, "noConversionRateAvailable": { "message": "कोई भी कन्वर्शन दर उपलब्ध नहीं है" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "प्राइस अनुपलब्ध है" }, - "primaryCurrencySetting": { - "message": "प्राथमिक मुद्रा" - }, - "primaryCurrencySettingDescription": { - "message": "चेन की ओरिजिनल करेंसी (जैसे ETH) में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए ओरिजिनल को चुनें। अपनी चुना गया फिएट करेंसी में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए फिएट को चुनें।" - }, "primaryType": { "message": "प्राइमरी टाइप" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "अपडेट का अनुरोध" }, - "updatedWithDate": { - "message": "अपडेट किया गया $1" - }, "uploadDropFile": { "message": "अपनी फ़ाइल यहां छोड़ें" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 8e9091f911db..a4e2e37bde22 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -87,10 +87,6 @@ "failed": { "message": "विफल" }, - "fiat": { - "message": "FIAT एक्सचेंज टाइप", - "description": "Exchange type" - }, "fileImportFail": { "message": "फ़ाइल आयात काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 4463408d9435..7f9334f49f5c 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -501,12 +501,6 @@ "prev": { "message": "Prethodno" }, - "primaryCurrencySetting": { - "message": "Glavna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Odaberite da se prvo prikazuju valute u osnovnoj valuti bloka (npr. ETH). Odaberite mogućnost Fiat za prikazivanje valuta u odabranoj valuti Fiat." - }, "privacyMsg": { "message": "Pravilnik o zaštiti privatnosti" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "Decentralizirani internet čeka" }, - "updatedWithDate": { - "message": "Ažurirano $1" - }, "urlErrorMsg": { "message": "URI-jevima se zahtijeva prikladan prefiks HTTP/HTTPS." }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 700f6debed18..7309b04dbd05 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -153,10 +153,6 @@ "failed": { "message": "Tonbe" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Enpòte dosye ki pa travay? Klike la a!", "description": "Helps user import their account from a JSON file" @@ -357,12 +353,6 @@ "prev": { "message": "Avan" }, - "primaryCurrencySetting": { - "message": "Lajan ou itilize pi plis la" - }, - "primaryCurrencySettingDescription": { - "message": "Chwazi ETH pou bay priyorite montre valè nan ETH. Chwazi Fiat priyorite montre valè nan lajan ou chwazi a." - }, "privacyMsg": { "message": "Règleman sou enfòmasyon prive" }, @@ -548,9 +538,6 @@ "unlockMessage": { "message": "Entènèt desantralize a ap tann" }, - "updatedWithDate": { - "message": "Mete ajou $1" - }, "urlErrorMsg": { "message": "URIs mande pou apwopriye prefiks HTTP / HTTPS a." }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 4786cfb9703d..7b2b429ae5ed 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -501,12 +501,6 @@ "prev": { "message": "Előző" }, - "primaryCurrencySetting": { - "message": "Elsődleges pénznem" - }, - "primaryCurrencySettingDescription": { - "message": "Válaszd a helyit, hogy az értékek elsősorban a helyi pénznemben jelenjenek meg (pl. ETH). Válaszd a Fiatot, hogy az értékek elsősorban a választott fiat pénznemben jelenjenek meg." - }, "privacyMsg": { "message": "Adatvédelmi szabályzat" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "A decentralizált hálózat csak önre vár" }, - "updatedWithDate": { - "message": "$1 frissítve" - }, "urlErrorMsg": { "message": "Az URI-hez szükség van a megfelelő HTTP/HTTPS előtagra." }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index a9a34d9b9a4e..054150ae5b7a 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Detail biaya" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Impor file tidak bekerja? Klik di sini!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, - "noConversionDateAvailable": { - "message": "Tanggal konversi mata uang tidak tersedia" - }, "noConversionRateAvailable": { "message": "Nilai konversi tidak tersedia" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "harga tidak tersedia" }, - "primaryCurrencySetting": { - "message": "Mata uang primer" - }, - "primaryCurrencySettingDescription": { - "message": "Pilih asal untuk memprioritaskan nilai yang ditampilkan dalam mata uang asal chain (contoh, ETH). Pilih Fiat untuk memprioritaskan nilai yang ditampilkan dalam mata uang fiat yang Anda pilih." - }, "primaryType": { "message": "Tipe primer" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Permintaan pembaruan" }, - "updatedWithDate": { - "message": "Diperbarui $1" - }, "uploadDropFile": { "message": "Letakkan fail di sini" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 7c413941da92..71b07590d6d1 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -808,10 +808,6 @@ "feeAssociatedRequest": { "message": "Una tassa è associata a questa richiesta." }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Importazione file non funziona? Clicca qui!", "description": "Helps user import their account from a JSON file" @@ -1165,12 +1161,6 @@ "prev": { "message": "Precedente" }, - "primaryCurrencySetting": { - "message": "Moneta Primaria" - }, - "primaryCurrencySettingDescription": { - "message": "Seleziona ETH per privilegiare la visualizzazione dei valori nella moneta nativa della blockhain. Seleziona Fiat per privilegiare la visualizzazione dei valori nella moneta selezionata." - }, "privacyMsg": { "message": "Politica sulla Privacy" }, @@ -1698,9 +1688,6 @@ "unlockMessage": { "message": "Il web decentralizzato ti attende" }, - "updatedWithDate": { - "message": "Aggiornata $1" - }, "urlErrorMsg": { "message": "Gli URI richiedono un prefisso HTTP/HTTPS." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index f491ac9aa280..74f281b7f873 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "手数料の詳細" }, - "fiat": { - "message": "法定通貨", - "description": "Exchange type" - }, "fileImportFail": { "message": "ファイルのインポートが機能していない場合、ここをクリックしてください!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, - "noConversionDateAvailable": { - "message": "通貨換算日がありません" - }, "noConversionRateAvailable": { "message": "利用可能な換算レートがありません" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "価格が利用できません" }, - "primaryCurrencySetting": { - "message": "プライマリ通貨" - }, - "primaryCurrencySettingDescription": { - "message": "チェーンのネイティブ通貨 (ETHなど) による値の表示を優先するには、「ネイティブ」を選択します。選択した法定通貨による値の表示を優先するには、「法定通貨」を選択します。" - }, "primaryType": { "message": "基本型" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "更新リクエスト" }, - "updatedWithDate": { - "message": "$1が更新されました" - }, "uploadDropFile": { "message": "ここにファイルをドロップします" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 6471b738b5b9..120651f0b759 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "ವೇಗ" }, - "fiat": { - "message": "ಫಿಯೆಟ್", - "description": "Exchange type" - }, "fileImportFail": { "message": "ಫೈಲ್ ಆಮದು ಮಾಡುವಿಕೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತಿಲ್ಲವೇ? ಇಲ್ಲಿ ಕ್ಲಿಕ್ ಮಾಡಿ!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "ಹಿಂದಿನ" }, - "primaryCurrencySetting": { - "message": "ಪ್ರಾಥಮಿಕ ಕರೆನ್ಸಿ" - }, - "primaryCurrencySettingDescription": { - "message": "ಸರಪಳಿಯ ಸ್ಥಳೀಯ ಕರೆನ್ಸಿಯಲ್ಲಿ ಮೌಲ್ಯಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಆದ್ಯತೆ ನೀಡಲು ಸ್ಥಳೀಯವನ್ನು ಆಯ್ಕೆಮಾಡಿ (ಉದಾ. ETH). ನಿಮ್ಮ ಆಯ್ಕೆಮಾಡಿದ ಫಿಯೆಟ್ ಕರೆನ್ಸಿಯಲ್ಲಿ ಮೌಲ್ಯಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಆದ್ಯತೆ ನೀಡಲು ಫಿಯೆಟ್ ಆಯ್ಕೆಮಾಡಿ." - }, "privacyMsg": { "message": "ಗೌಪ್ಯತೆ ನೀತಿ" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "ವಿಕೇಂದ್ರೀಕೃತ ವೆಬ್ ನಿರೀಕ್ಷಿಸುತ್ತಿದೆ" }, - "updatedWithDate": { - "message": "$1 ನವೀಕರಿಸಲಾಗಿದೆ" - }, "urlErrorMsg": { "message": "URI ಗಳಿಗೆ ಸೂಕ್ತವಾದ HTTP/HTTPS ಪೂರ್ವಪ್ರತ್ಯಯದ ಅಗತ್ಯವಿದೆ." }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 615db337e09e..ce11ea4bebf4 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "수수료 세부 정보" }, - "fiat": { - "message": "명목", - "description": "Exchange type" - }, "fileImportFail": { "message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요.", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, - "noConversionDateAvailable": { - "message": "사용 가능한 통화 변환 날짜 없음" - }, "noConversionRateAvailable": { "message": "사용 가능한 환율 없음" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "가격 사용 불가" }, - "primaryCurrencySetting": { - "message": "기본 통화" - }, - "primaryCurrencySettingDescription": { - "message": "체인의 고유 통화(예: ETH)로 값을 우선 표시하려면 고유를 선택합니다. 선택한 명목 통화로 값을 우선 표시하려면 명목을 선택합니다." - }, "primaryType": { "message": "기본 유형" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "업데이트 요청" }, - "updatedWithDate": { - "message": "$1에 업데이트됨" - }, "uploadDropFile": { "message": "여기에 파일을 드롭" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 0668166eb8fe..fe825ae6b798 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Greitas" }, - "fiat": { - "message": "Standartinė valiuta", - "description": "Exchange type" - }, "fileImportFail": { "message": "Failo importavimas neveikia? Spustelėkite čia!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "Peržiūra" }, - "primaryCurrencySetting": { - "message": "Pagrindinė valiuta" - }, - "primaryCurrencySettingDescription": { - "message": "Rinkitės vietinę, kad vertės pirmiausia būtų rodomos vietine grandinės valiuta (pvz., ETH). Rinkitės standartinę, kad vertės pirmiausia būtų rodomos jūsų pasirinkta standartine valiuta." - }, "privacyMsg": { "message": "Privatumo politika" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "Laukiančios decentralizuotos svetainės" }, - "updatedWithDate": { - "message": "Atnaujinta $1" - }, "urlErrorMsg": { "message": "URI reikia atitinkamo HTTP/HTTPS priešdėlio." }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 3939c9145c12..697af7849327 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -504,12 +504,6 @@ "prev": { "message": "Iepr." }, - "primaryCurrencySetting": { - "message": "Primārā valūta" - }, - "primaryCurrencySettingDescription": { - "message": "Atlasīt vietējo, lai piešķirtu attēlotajām vērtībām prioritātes ķēdes vietējā vērtībā (piemēram, ETH). Atlasiet Fiat, lai piešķirtu augstāku prioritāti vērtībām jūsu atlasītajā fiat valūtā." - }, "privacyMsg": { "message": "Privātuma politika" }, @@ -761,9 +755,6 @@ "unlockMessage": { "message": "Decentralizētais tīkls jau gaida" }, - "updatedWithDate": { - "message": "Atjaunināts $1" - }, "urlErrorMsg": { "message": "URI jāsākas ar atbilstošo HTTP/HTTPS priedēkli." }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index cfad6a22d73d..dc42e639ff2a 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -488,12 +488,6 @@ "prev": { "message": "Sebelumnya" }, - "primaryCurrencySetting": { - "message": "Mata Wang Utama" - }, - "primaryCurrencySettingDescription": { - "message": "Pilih natif untuk mengutamakan nilai paparan dalam mata wang natif rantaian (cth. ETH). Pilih Fiat untuk mengutamakan nilai paparan dalam mata wang fiat yang anda pilih." - }, "privacyMsg": { "message": "Dasar Privasi" }, @@ -742,9 +736,6 @@ "unlockMessage": { "message": "Web ternyahpusat menanti" }, - "updatedWithDate": { - "message": "Dikemaskini $1" - }, "urlErrorMsg": { "message": "URI memerlukan awalan HTTP/HTTPS yang sesuai." }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 946976a7a93e..cbebb9a14563 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -84,10 +84,6 @@ "failed": { "message": "mislukt" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Bestandsimport werkt niet? Klik hier!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 0d1fa5173a9f..45a101fc83a5 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -492,12 +492,6 @@ "prev": { "message": "Tidligere" }, - "primaryCurrencySetting": { - "message": "Hovedvaluta " - }, - "primaryCurrencySettingDescription": { - "message": "Velg nasjonal for å prioritere å vise verdier i nasjonal valuta i kjeden (f.eks. ETH). Velg Fiat for å prioritere visning av verdier i den valgte fiat-valutaen." - }, "privacyMsg": { "message": "Personvernerklæring" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "Det desentraliserte internett venter deg" }, - "updatedWithDate": { - "message": "Oppdatert $1" - }, "urlErrorMsg": { "message": "URI-er krever det aktuelle HTTP/HTTPS-prefikset." }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index df15e05a0bb6..454facde8524 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -513,10 +513,6 @@ "feeAssociatedRequest": { "message": "May nauugnay na bayarin para sa request na ito." }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" @@ -955,12 +951,6 @@ "prev": { "message": "Nakaraan" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para maisapriyoridad ang pagpapakita ng mga value sa native na currency ng chain (hal. ETH). Piliin ang Fiat para maisapriyoridad ang pagpapakita ng mga value sa napili mong fiat currency." - }, "privacyMsg": { "message": "Patakaran sa Pagkapribado" }, @@ -1655,9 +1645,6 @@ "message": "Hindi kinikilala ang custom na network na ito. Inirerekomenda naming $1 ka bago magpatuloy", "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "urlErrorMsg": { "message": "Kinakailangan ng mga URL ang naaangkop na HTTP/HTTPS prefix." }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index cb82388a8634..d22673fa9f1f 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Szybko" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Importowanie pliku nie działa? Kliknij tutaj!", "description": "Helps user import their account from a JSON file" @@ -502,12 +498,6 @@ "prev": { "message": "Poprzednie" }, - "primaryCurrencySetting": { - "message": "Waluta podstawowa" - }, - "primaryCurrencySettingDescription": { - "message": "Wybierz walutę natywną, aby preferować wyświetlanie wartości w walucie natywnej łańcucha (np. ETH). Wybierz walutę fiat, aby preferować wyświetlanie wartości w wybranej przez siebie walucie fiat." - }, "privacyMsg": { "message": "Polityka prywatności" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Zdecentralizowana sieć oczekuje" }, - "updatedWithDate": { - "message": "Zaktualizowano $1" - }, "urlErrorMsg": { "message": "URI wymaga prawidłowego prefiksu HTTP/HTTPS." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index d5039ee7e604..770d517c8b25 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Detalhes da taxa" }, - "fiat": { - "message": "Fiduciária", - "description": "Exchange type" - }, "fileImportFail": { "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, - "noConversionDateAvailable": { - "message": "Não há uma data de conversão de moeda disponível" - }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "preço não disponível" }, - "primaryCurrencySetting": { - "message": "Moeda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecione Nativa para priorizar a exibição de valores na moeda nativa da cadeia (por ex., ETH). Selecione Fiduciária para priorizar a exibição de valores na moeda fiduciária selecionada." - }, "primaryType": { "message": "Tipo primário" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Solicitação de atualização" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "uploadDropFile": { "message": "Solte seu arquivo aqui" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 4c4b17d74f6e..0f6efb88d348 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -782,10 +782,6 @@ "feeAssociatedRequest": { "message": "Há uma taxa associada a essa solicitação." }, - "fiat": { - "message": "Fiduciária", - "description": "Exchange type" - }, "fileImportFail": { "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" @@ -1321,9 +1317,6 @@ "noAccountsFound": { "message": "Nenhuma conta encontrada para a busca efetuada" }, - "noConversionDateAvailable": { - "message": "Não há uma data de conversão de moeda disponível" - }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -1493,12 +1486,6 @@ "prev": { "message": "Anterior" }, - "primaryCurrencySetting": { - "message": "Moeda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecione Nativa para priorizar a exibição de valores na moeda nativa da cadeia (por ex., ETH). Selecione Fiduciária para priorizar a exibição de valores na moeda fiduciária selecionada." - }, "priorityFee": { "message": "Taxa de prioridade" }, @@ -2409,9 +2396,6 @@ "message": "O envio de tokens colecionáveis (ERC-721) não é suportado no momento", "description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "urlErrorMsg": { "message": "Os URLs precisam do prefixo HTTP/HTTPS adequado." }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index aad720151da6..912accba29be 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -495,12 +495,6 @@ "prev": { "message": "Ant" }, - "primaryCurrencySetting": { - "message": "Moneda principală" - }, - "primaryCurrencySettingDescription": { - "message": "Selectați nativ pentru a prioritiza valorile afișate în moneda nativă a lanțului (ex. ETH). Selectați Fiat pentru a prioritiza valorile afișate în moneda selectată fiat." - }, "privacyMsg": { "message": "Politica de Confidențialitate" }, @@ -746,9 +740,6 @@ "unlockMessage": { "message": "Web-ul descentralizat așteaptă" }, - "updatedWithDate": { - "message": "Actualizat $1" - }, "urlErrorMsg": { "message": "URL necesită prefixul potrivit HTTP/HTTPS." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 794cd777afd1..6ce21329f244 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Сведения о комиссии" }, - "fiat": { - "message": "Фиатная", - "description": "Exchange type" - }, "fileImportFail": { "message": "Импорт файлов не работает? Нажмите здесь!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, - "noConversionDateAvailable": { - "message": "Дата обмена валюты недоступна" - }, "noConversionRateAvailable": { "message": "Нет доступного обменного курса" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "цена недоступна" }, - "primaryCurrencySetting": { - "message": "Основная валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Выберите «собственная», чтобы установить приоритет отображения значений в собственной валюте блокчейна (например, ETH). Выберите «Фиатная», чтобы установить приоритет отображения значений в выбранной фиатной валюте." - }, "primaryType": { "message": "Основной тип" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Запрос обновления" }, - "updatedWithDate": { - "message": "Обновлено $1" - }, "uploadDropFile": { "message": "Переместите свой файл сюда" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 564e7cb12d94..829435f28ff7 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -238,10 +238,6 @@ "fast": { "message": "Rýchle" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Import souboru nefunguje? Klikněte sem!", "description": "Helps user import their account from a JSON file" @@ -480,12 +476,6 @@ "prev": { "message": "Predchádzajúce" }, - "primaryCurrencySetting": { - "message": "Primárna mena" - }, - "primaryCurrencySettingDescription": { - "message": "Vyberte natívne, ak chcete priorizovať zobrazovanie hodnôt v natívnej mene reťazca (napr. ETH). Ak chcete priorizovať zobrazovanie hodnôt vo svojej vybranej mene fiat, zvoľte možnosť Fiat." - }, "privacyMsg": { "message": "Zásady ochrany osobních údajů" }, @@ -731,9 +721,6 @@ "unlockMessage": { "message": "Decentralizovaný web čaká" }, - "updatedWithDate": { - "message": "Aktualizované $1" - }, "urlErrorMsg": { "message": "URI vyžadují korektní HTTP/HTTPS prefix." }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 8d43d184c427..cb82e0358212 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Hiter" }, - "fiat": { - "message": "Klasične", - "description": "Exchange type" - }, "fileImportFail": { "message": "Uvoz z datoteko ne deluje? Kliknite tukaj!", "description": "Helps user import their account from a JSON file" @@ -496,12 +492,6 @@ "prev": { "message": "Prej" }, - "primaryCurrencySetting": { - "message": "Glavna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Izberite Native za prikaz vrednosti v privzeti valuti verige (npr. ETH). Izberite Klasične za prikaz vrednosti v izbrani klasični valuti." - }, "privacyMsg": { "message": "Zasebnost" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Decentralizirana spletna denarnica" }, - "updatedWithDate": { - "message": "Posodobljeno $1" - }, "urlErrorMsg": { "message": "URI zahtevajo ustrezno HTTP/HTTPS predpono." }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index d3f5e27c6235..e15ae23086b3 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -241,10 +241,6 @@ "fast": { "message": "Брзо" }, - "fiat": { - "message": "Dekret", - "description": "Exchange type" - }, "fileImportFail": { "message": "Uvoz datoteke ne radi? Kliknite ovde!", "description": "Helps user import their account from a JSON file" @@ -499,12 +495,6 @@ "prev": { "message": "Prethodno" }, - "primaryCurrencySetting": { - "message": "Primarna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Izaberite primarnu da biste postavili prioritete u prikazivanju vrednosti u primarnoj valuti lanca (npr. ETH). Izaberite Fiat da biste postavili prioritete u prikazivanju vrednosti u vašoj izabranoj fiat valuti." - }, "privacyMsg": { "message": "Smernice za privatnost" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Decentralizovani veb čeka" }, - "updatedWithDate": { - "message": "Ažuriran $1" - }, "urlErrorMsg": { "message": "URI-ovi zahtevaju odgovarajući prefiks HTTP / HTTPS." }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index a98db5bea015..163cdebc426e 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -492,12 +492,6 @@ "prev": { "message": "Föregående" }, - "primaryCurrencySetting": { - "message": "Primär valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Välj native för att prioritera visning av värden i den ursprungliga valutan i kedjan (t.ex. ETH). Välj Fiat för att prioritera visning av värden i din valda fiatvaluta." - }, "privacyMsg": { "message": "Integritetspolicy" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "Den decentraliserade webben väntar" }, - "updatedWithDate": { - "message": "Uppdaterat $1" - }, "urlErrorMsg": { "message": "URI:er kräver lämpligt HTTP/HTTPS-prefix." }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index d8ad6258e8ba..c1535d76cdd8 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -486,12 +486,6 @@ "prev": { "message": "Hakiki" }, - "primaryCurrencySetting": { - "message": "Sarafu ya Msingi" - }, - "primaryCurrencySettingDescription": { - "message": "Chagua mzawa ili kuweka kipaumbele kuonyesha thamani kwenye sarafu mzawa ya mnyororo (k.m ETH). Chagua Fiat ili uwelke kipaumbale kuonyesha thamani kwenye sarafu yako ya fiat uliyoichagua." - }, "privacyMsg": { "message": "Sera ya Faragha" }, @@ -743,9 +737,6 @@ "unlockMessage": { "message": "Wavuti uliotenganishwa unasubiri" }, - "updatedWithDate": { - "message": "Imesasishwa $1" - }, "urlErrorMsg": { "message": "URI huhitaji kiambishi sahihi cha HTTP/HTTPS." }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 8e38061bebb2..d5d1929a2dc4 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -129,10 +129,6 @@ "fast": { "message": "வேகமான" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "கோப்பு இறக்குமதி வேலை செய்யவில்லையா? இங்கே கிளிக் செய்யவும்!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index c3b4a8a6e3fa..e6c074fe1264 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -120,10 +120,6 @@ "fast": { "message": "เร็ว" }, - "fiat": { - "message": "เงินตรา", - "description": "Exchange type" - }, "fileImportFail": { "message": "นำเข้าไฟล์ไม่สำเหร็จ กดที่นี่!", "description": "Helps user import their account from a JSON file" @@ -371,9 +367,6 @@ "unlock": { "message": "ปลดล็อก" }, - "updatedWithDate": { - "message": "อัปเดต $1 แล้ว" - }, "urlErrorMsg": { "message": "URI ต้องมีคำนำหน้าเป็น HTTP หรือ HTTPS" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 958fa1345041..57b62731724d 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Mga detalye ng singil" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, - "noConversionDateAvailable": { - "message": "Walang available na petsa sa pagpapapalit ng currency" - }, "noConversionRateAvailable": { "message": "Hindi available ang rate ng palitan" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "hindi available ang presyo" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para maisapriyoridad ang pagpapakita ng mga value sa native na currency ng chain (hal. ETH). Piliin ang Fiat para maisapriyoridad ang pagpapakita ng mga value sa napili mong fiat na salapi." - }, "primaryType": { "message": "Pangunahing uri" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Hiling sa pag-update" }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "uploadDropFile": { "message": "I-drop ang file mo rito" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3fbbc1bc51ff..9f8e90a386e3 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Ücret bilgileri" }, - "fiat": { - "message": "Fiat Para", - "description": "Exchange type" - }, "fileImportFail": { "message": "Dosya içe aktarma çalışmıyor mu? Buraya tıklayın!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, - "noConversionDateAvailable": { - "message": "Para birimi dönüşüm tarihi mevcut değil" - }, "noConversionRateAvailable": { "message": "Dönüşüm oranı mevcut değil" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "fiyat mevcut değil" }, - "primaryCurrencySetting": { - "message": "Öncelikli para birimi" - }, - "primaryCurrencySettingDescription": { - "message": "Değerlerin zincirin yerli para biriminde (ör. ETH) görüntülenmesini önceliklendirmek için yerli seçimi yapın. Seçtiğiniz fiat parada değerlerin gösterilmesini önceliklendirmek için Fiat Para seçin." - }, "primaryType": { "message": "Öncelikli tür" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Talebi güncelle" }, - "updatedWithDate": { - "message": "$1 güncellendi" - }, "uploadDropFile": { "message": "Dosyanızı buraya sürükleyin" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 37bab506a87d..b0c011690910 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Швидка" }, - "fiat": { - "message": "Вказівка", - "description": "Exchange type" - }, "fileImportFail": { "message": "Не працює імпорт файлу? Натисніть тут!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "Попередній" }, - "primaryCurrencySetting": { - "message": "Первісна валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Оберіть \"рідна\", щоб пріоритезувати показ сум у рідних валютах мережі (напр.ETH). \nОберіть \"фіатна\", щоб пріоритезувати показ сум у ваших обраних фіатних валютах." - }, "privacyMsg": { "message": "Політика конфіденційності" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "Децентралізована мережа очікує" }, - "updatedWithDate": { - "message": "Оновлено $1" - }, "urlErrorMsg": { "message": "URIs вимагають відповідного префікса HTTP/HTTPS." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 52e7a3a18792..273089bb4343 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Chi tiết phí" }, - "fiat": { - "message": "Pháp định", - "description": "Exchange type" - }, "fileImportFail": { "message": "Tính năng nhập tập tin không hoạt động? Nhấp vào đây!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, - "noConversionDateAvailable": { - "message": "Hiện không có ngày quy đổi tiền tệ nào" - }, "noConversionRateAvailable": { "message": "Không có sẵn tỷ lệ quy đổi nào" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "giá không khả dụng" }, - "primaryCurrencySetting": { - "message": "Tiền tệ chính" - }, - "primaryCurrencySettingDescription": { - "message": "Chọn Gốc để ưu tiên hiển thị giá trị bằng đơn vị tiền tệ gốc của chuỗi (ví dụ: ETH). Chọn Pháp định để ưu tiên hiển thị giá trị bằng đơn vị tiền pháp định mà bạn chọn." - }, "primaryType": { "message": "Loại chính" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "Yêu cầu cập nhật" }, - "updatedWithDate": { - "message": "Đã cập nhật $1" - }, "uploadDropFile": { "message": "Thả tập tin của bạn vào đây" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 302eb5648224..6a8f8d9f4df6 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "费用详情" }, - "fiat": { - "message": "法币", - "description": "Exchange type" - }, "fileImportFail": { "message": "文件导入失败?点击这里!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, - "noConversionDateAvailable": { - "message": "没有可用的货币转换日期" - }, "noConversionRateAvailable": { "message": "无可用汇率" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "价格不可用" }, - "primaryCurrencySetting": { - "message": "主要货币" - }, - "primaryCurrencySettingDescription": { - "message": "选择原生以优先显示链的原生货币(例如 ETH)的值。选择法币以优先显示以您所选法币显示的值。" - }, "primaryType": { "message": "主要类型" }, @@ -6085,9 +6072,6 @@ "updateRequest": { "message": "更新请求" }, - "updatedWithDate": { - "message": "已于 $1 更新" - }, "uploadDropFile": { "message": "将您的文件放在此处" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 0924d284b529..dee06a7aef16 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -512,10 +512,6 @@ "feeAssociatedRequest": { "message": "這個請求會附帶一筆手續費。" }, - "fiat": { - "message": "法定貨幣", - "description": "Exchange type" - }, "fileImportFail": { "message": "檔案匯入失敗?點擊這裡!", "description": "Helps user import their account from a JSON file" @@ -944,12 +940,6 @@ "prev": { "message": "前一頁" }, - "primaryCurrencySetting": { - "message": "主要貨幣" - }, - "primaryCurrencySettingDescription": { - "message": "選擇原生來優先使用鏈上原生貨幣 (例如 ETH) 顯示金額。選擇法定貨幣來優先使用您選擇的法定貨幣顯示金額。" - }, "privacyMsg": { "message": "隱私政策" }, @@ -1380,9 +1370,6 @@ "message": "無法辨識這個自訂網路。我們建議您先$1再繼續。", "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." }, - "updatedWithDate": { - "message": "更新時間 $1" - }, "urlErrorMsg": { "message": "URL 需要以適當的 HTTP/HTTPS 作為開頭" }, diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 33bf9bac0f22..9763d152eb39 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -226,7 +226,7 @@ export const SENTRY_BACKGROUND_STATE = { showFiatInTestnets: true, showTestNetworks: true, smartTransactionsOptInStatus: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showConfirmationAdvancedDetails: true, }, diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index bfe7f79d1ac4..ef1dbe02789a 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -846,8 +846,8 @@ export default class MetaMetricsController { [MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: metamaskState.useTokenDetection, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: - metamaskState.useNativeCurrencyAsPrimaryCurrency, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: + metamaskState.showNativeTokenAsMainBalance, [MetaMetricsUserTrait.CurrentCurrency]: metamaskState.currentCurrency, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: this.extension?.runtime?.id, diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 2113efd1715b..3d4845e056d0 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1088,7 +1088,7 @@ describe('MetaMetricsController', function () { securityAlertsEnabled: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, security_providers: [], names: { [NameType.ETHEREUM_ADDRESS]: { @@ -1143,7 +1143,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.ThreeBoxEnabled]: false, [MetaMetricsUserTrait.Theme]: 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: true, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: true, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: true, [MetaMetricsUserTrait.SecurityProviders]: ['blockaid'], ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: 'testid', @@ -1181,7 +1181,7 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ @@ -1208,7 +1208,7 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }); expect(updatedTraits).toStrictEqual({ @@ -1216,7 +1216,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.NumberOfAccounts]: 3, [MetaMetricsUserTrait.NumberOfTokens]: 1, [MetaMetricsUserTrait.OpenseaApiEnabled]: false, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: false, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: false, }); }); @@ -1245,7 +1245,7 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ @@ -1267,7 +1267,7 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }); expect(updatedTraits).toStrictEqual(null); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index ab6c3e959215..d4fdc5e70d1e 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -96,6 +96,7 @@ export type Preferences = { showFiatInTestnets: boolean; showTestNetworks: boolean; smartTransactionsOptInStatus: boolean | null; + showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; petnamesEnabled: boolean; @@ -105,6 +106,7 @@ export type Preferences = { showMultiRpcModal: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; + shouldShowAggregatedBalancePopover: boolean; }; export type PreferencesControllerState = { @@ -223,6 +225,7 @@ export default class PreferencesController { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, @@ -232,6 +235,7 @@ export default class PreferencesController { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + shouldShowAggregatedBalancePopover: true, // by default user should see popover; }, // ENS decentralized website resolution ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, diff --git a/app/scripts/migrations/128.test.ts b/app/scripts/migrations/128.test.ts new file mode 100644 index 000000000000..f2658bfc6bd9 --- /dev/null +++ b/app/scripts/migrations/128.test.ts @@ -0,0 +1,39 @@ +import { migrate, version } from './128'; + +const oldVersion = 127; + +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('Removes useNativeCurrencyAsPrimaryCurrency from the PreferencesController.preferences state', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + delete ( + oldState.PreferencesController.preferences as { + useNativeCurrencyAsPrimaryCurrency?: boolean; + } + ).useNativeCurrencyAsPrimaryCurrency; + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/128.ts b/app/scripts/migrations/128.ts new file mode 100644 index 000000000000..89f14606af7f --- /dev/null +++ b/app/scripts/migrations/128.ts @@ -0,0 +1,42 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 128; + +/** + * This migration removes `useNativeCurrencyAsPrimaryCurrency` from preferences in PreferencesController. + * + * @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, 'PreferencesController') && + isObject(state.PreferencesController) && + isObject(state.PreferencesController.preferences) + ) { + delete state.PreferencesController.preferences + .useNativeCurrencyAsPrimaryCurrency; + } + + return state; +} diff --git a/app/scripts/migrations/129.test.ts b/app/scripts/migrations/129.test.ts new file mode 100644 index 000000000000..740add0e7e4e --- /dev/null +++ b/app/scripts/migrations/129.test.ts @@ -0,0 +1,60 @@ +import { migrate, version } from './129'; + +const oldVersion = 128; + +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('Adds shouldShowAggregatedBalancePopover to the PreferencesController.preferences state when its undefined', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual({ + ...oldState, + PreferencesController: { + ...oldState.PreferencesController, + preferences: { + ...oldState.PreferencesController.preferences, + shouldShowAggregatedBalancePopover: true, + }, + }, + }); + }); + + it('Does not add shouldShowAggregatedBalancePopover to the PreferencesController.preferences state when its defined', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + shouldShowAggregatedBalancePopover: false, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/129.ts b/app/scripts/migrations/129.ts new file mode 100644 index 000000000000..b4323798a006 --- /dev/null +++ b/app/scripts/migrations/129.ts @@ -0,0 +1,47 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 129; + +/** + * This migration adds `shouldShowAggregatedBalancePopover` to preferences in PreferencesController and set it to true when its undefined. + * + * @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, 'PreferencesController') && + isObject(state.PreferencesController) && + isObject(state.PreferencesController.preferences) + ) { + if ( + state.PreferencesController.preferences + .shouldShowAggregatedBalancePopover === undefined + ) { + state.PreferencesController.preferences.shouldShowAggregatedBalancePopover = + true; + } + } + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 80407ecf232e..296ff8077613 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -147,6 +147,8 @@ const migrations = [ require('./125.1'), require('./126'), require('./127'), + require('./128'), + require('./129'), ]; export default migrations; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index d9287da93a12..663032649f86 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -446,9 +446,9 @@ export enum MetaMetricsUserTrait { */ TokenDetectionEnabled = 'token_detection_enabled', /** - * Identified when the user enables native currency. + * Identified when show native token as main balance is toggled. */ - UseNativeCurrencyAsPrimaryCurrency = 'use_native_currency_as_primary_currency', + ShowNativeTokenAsMainBalance = 'show_native_token_as_main_balance', /** * Identified when the security provider feature is enabled. */ @@ -632,7 +632,7 @@ export enum MetaMetricsEventName { TokenHidden = 'Token Hidden', TokenImportCanceled = 'Token Import Canceled', TokenImportClicked = 'Token Import Clicked', - UseNativeCurrencyAsPrimaryCurrency = 'Use Native Currency as Primary Currency', + ShowNativeTokenAsMainBalance = 'Show native token as main balance', WalletSetupStarted = 'Wallet Setup Selected', WalletSetupCanceled = 'Wallet Setup Canceled', WalletSetupFailed = 'Wallet Setup Failed', diff --git a/shared/modules/currency-display.utils.test.ts b/shared/modules/currency-display.utils.test.ts deleted file mode 100644 index b2fdbc456593..000000000000 --- a/shared/modules/currency-display.utils.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - showPrimaryCurrency, - showSecondaryCurrency, -} from './currency-display.utils'; - -describe('showPrimaryCurrency', () => { - it('should return true when useNativeCurrencyAsPrimaryCurrency is true', () => { - const result = showPrimaryCurrency(true, true); - expect(result).toBe(true); - }); - - it('should return true when isOriginalNativeSymbol is true', () => { - const result = showPrimaryCurrency(true, false); - expect(result).toBe(true); - }); - - it('should return false when useNativeCurrencyAsPrimaryCurrency and isOriginalNativeSymbol are false', () => { - const result = showPrimaryCurrency(false, false); - expect(result).toBe(false); - }); -}); - -describe('showSecondaryCurrency', () => { - it('should return true when useNativeCurrencyAsPrimaryCurrency is false', () => { - const result = showSecondaryCurrency(true, false); - expect(result).toBe(true); - }); - - it('should return true when isOriginalNativeSymbol is true', () => { - const result = showSecondaryCurrency(true, true); - expect(result).toBe(true); - }); - - it('should return false when useNativeCurrencyAsPrimaryCurrency is true and isOriginalNativeSymbol is false', () => { - const result = showSecondaryCurrency(false, true); - expect(result).toBe(false); - }); -}); diff --git a/shared/modules/currency-display.utils.ts b/shared/modules/currency-display.utils.ts deleted file mode 100644 index 3f50a2364e6d..000000000000 --- a/shared/modules/currency-display.utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const showPrimaryCurrency = ( - isOriginalNativeSymbol: boolean, - useNativeCurrencyAsPrimaryCurrency: boolean, -): boolean => { - // crypto is the primary currency in this case , so we have to display it always - if (useNativeCurrencyAsPrimaryCurrency) { - return true; - } - // if the primary currency corresponds to a fiat value, check that the symbol is correct. - if (isOriginalNativeSymbol) { - return true; - } - - return false; -}; - -export const showSecondaryCurrency = ( - isOriginalNativeSymbol: boolean, - useNativeCurrencyAsPrimaryCurrency: boolean, -): boolean => { - // crypto is the secondary currency in this case , so we have to display it always - if (!useNativeCurrencyAsPrimaryCurrency) { - return true; - } - // if the secondary currency corresponds to a fiat value, check that the symbol is correct. - if (isOriginalNativeSymbol) { - return true; - } - - return false; -}; diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 6629d8c6ac67..96cd95cfbd84 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -131,8 +131,7 @@ "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, - "showTestNetworks": true, - "useNativeCurrencyAsPrimaryCurrency": true + "showTestNetworks": true }, "seedPhraseBackedUp": null, "ensResolutionsByAddress": {}, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 4b6a2f506215..c2d18bcb76dc 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -367,12 +367,12 @@ "preferences": { "hideZeroBalanceTokens": false, "isRedesignedConfirmationsDeveloperEnabled": false, + "petnamesEnabled": false, "showExtensionInFullSizeView": false, "showFiatInTestnets": false, + "showNativeTokenAsMainBalance": true, "showTestNetworks": true, - "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, - "petnamesEnabled": false + "smartTransactionsOptInStatus": false }, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 470e260ff959..4605e0bb0295 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -206,11 +206,12 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + shouldShowAggregatedBalancePopover: true, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', theme: 'light', diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index cc30c261d22d..edce958fab11 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -72,11 +72,12 @@ function onboardingFixture() { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + shouldShowAggregatedBalancePopover: true, }, useExternalServices: true, theme: 'light', @@ -186,6 +187,14 @@ class FixtureBuilder { }); } + withShowFiatTestnetEnabled() { + return this.withPreferencesController({ + preferences: { + showFiatInTestnets: true, + }, + }); + } + withConversionRateEnabled() { return this.withPreferencesController({ useCurrencyRateCheck: true, @@ -594,6 +603,14 @@ class FixtureBuilder { }); } + withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() { + return this.withPreferencesController({ + preferences: { + showNativeTokenAsMainBalance: false, + }, + }); + } + withPreferencesControllerTxSimulationsDisabled() { return this.withPreferencesController({ useTransactionSimulations: false, diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 0f400e8a34e7..846acc8164cd 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -36,8 +36,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true + "smartTransactionsOptInStatus": false }, "theme": "light", "useBlockie": false, diff --git a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js index 2f595138d0cf..fbf11b16cd40 100644 --- a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js +++ b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js @@ -195,7 +195,9 @@ describe('Encrypt Decrypt', function () { ); }); - it('should show balance correctly as Fiat', async function () { + it('should show balance correctly in native tokens', async function () { + // In component ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js, after removing useNativeCurrencyAsPrimaryCurrency; + // We will display native balance in the confirm-encryption-public-key.component.js await withFixtures( { dapp: true, @@ -203,7 +205,7 @@ describe('Encrypt Decrypt', function () { .withPermissionControllerConnectedToTestDapp() .withPreferencesController({ preferences: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, }) .build(), @@ -231,7 +233,7 @@ describe('Encrypt Decrypt', function () { const accountBalanceLabel = await driver.findElement( '.request-encryption-public-key__balance-value', ); - assert.equal(await accountBalanceLabel.getText(), '$42,500.00 USD'); + assert.equal(await accountBalanceLabel.getText(), '25 ETH'); }, ); }); 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 c133de6128ca..a4c6ebe05c6d 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 @@ -211,12 +211,13 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", 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 dfee54fbd6cb..4ae178caa4a7 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 @@ -32,12 +32,13 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "firstTimeFlowType": "import", "completedOnboarding": true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index b6354922add0..f21b237a1c46 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -112,11 +112,12 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 51910b2057a7..833584fd8c6d 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -112,11 +112,12 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", diff --git a/test/e2e/tests/settings/account-token-list.spec.js b/test/e2e/tests/settings/account-token-list.spec.js index 0fae71ae1d85..9e4822d0dbbc 100644 --- a/test/e2e/tests/settings/account-token-list.spec.js +++ b/test/e2e/tests/settings/account-token-list.spec.js @@ -3,6 +3,7 @@ const { withFixtures, defaultGanacheOptions, logInWithBalanceValidation, + unlockWallet, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -41,26 +42,18 @@ describe('Settings', function () { it('Should match the value of token list item and account list item for fiat conversion', async function () { await withFixtures( { - fixtures: new FixtureBuilder().withConversionRateEnabled().build(), + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withShowFiatTestnetEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.clickElement({ - text: 'General', - tag: 'div', - }); - await driver.clickElement({ text: 'Fiat', tag: 'label' }); + async ({ driver }) => { + await unlockWallet(driver); - await driver.clickElement( - '.settings-page__header__title-container__close-button', - ); + await driver.clickElement('[data-testid="popover-close"]'); await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); @@ -70,7 +63,6 @@ describe('Settings', function () { ); await driver.delay(1000); assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); - await driver.clickElement('[data-testid="account-menu-icon"]'); const accountTokenValue = await driver.waitForSelector( '.multichain-account-list-item .multichain-account-list-item__asset', diff --git a/test/e2e/tests/settings/change-language.spec.ts b/test/e2e/tests/settings/change-language.spec.ts index aafb36059c9b..1bd9915a33da 100644 --- a/test/e2e/tests/settings/change-language.spec.ts +++ b/test/e2e/tests/settings/change-language.spec.ts @@ -16,8 +16,8 @@ const selectors = { ethOverviewSend: '[data-testid="eth-overview-send"]', ensInput: '[data-testid="ens-input"]', nftsTab: '[data-testid="account-overview__nfts-tab"]', - labelSpanish: { tag: 'span', text: 'Idioma actual' }, - currentLanguageLabel: { tag: 'span', text: 'Current language' }, + labelSpanish: { tag: 'p', text: 'Idioma actual' }, + currentLanguageLabel: { tag: 'p', text: 'Current language' }, advanceText: { text: 'Avanceret', tag: 'div' }, waterText: '[placeholder="Søg"]', headerTextDansk: { text: 'Indstillinger', tag: 'h3' }, diff --git a/test/e2e/tests/settings/localization.spec.js b/test/e2e/tests/settings/localization.spec.js index 707cc120e578..57dbfd5f68cf 100644 --- a/test/e2e/tests/settings/localization.spec.js +++ b/test/e2e/tests/settings/localization.spec.js @@ -17,6 +17,7 @@ describe('Localization', function () { .withPreferencesController({ preferences: { showFiatInTestnets: true, + showNativeTokenAsMainBalance: false, }, }) .build(), @@ -26,15 +27,13 @@ describe('Localization', function () { async ({ driver }) => { await unlockWallet(driver); - const secondaryBalance = await driver.findElement( - '[data-testid="eth-overview__secondary-currency"]', + // After the removal of displaying secondary currency in coin-overview.tsx, we will test localization on main balance with showNativeTokenAsMainBalance = false + const primaryBalance = await driver.findElement( + '[data-testid="eth-overview__primary-currency"]', ); - const secondaryBalanceText = await secondaryBalance.getText(); - const [fiatAmount, fiatUnit] = secondaryBalanceText - .trim() - .split(/\s+/u); - assert.ok(fiatAmount.startsWith('₱')); - assert.equal(fiatUnit, 'PHP'); + const balanceText = await primaryBalance.getText(); + assert.ok(balanceText.startsWith('₱')); + assert.ok(balanceText.endsWith('PHP')); }, ); }); diff --git a/test/e2e/tests/settings/settings-search.spec.js b/test/e2e/tests/settings/settings-search.spec.js index 7a9207dcbf9f..fb67fbffd23b 100644 --- a/test/e2e/tests/settings/settings-search.spec.js +++ b/test/e2e/tests/settings/settings-search.spec.js @@ -9,7 +9,7 @@ const FixtureBuilder = require('../../fixture-builder'); describe('Settings Search', function () { const settingsSearch = { - general: 'Primary currency', + general: 'Show native token as main balance', advanced: 'State logs', contacts: 'Contacts', security: 'Reveal Secret', diff --git a/test/e2e/tests/settings/show-native-as-main-balance.spec.ts b/test/e2e/tests/settings/show-native-as-main-balance.spec.ts new file mode 100644 index 000000000000..d81e590cc5db --- /dev/null +++ b/test/e2e/tests/settings/show-native-as-main-balance.spec.ts @@ -0,0 +1,240 @@ +import { strict as assert } from 'assert'; +import { expect } from '@playwright/test'; +import { + withFixtures, + defaultGanacheOptions, + logInWithBalanceValidation, + unlockWallet, + getEventPayloads, +} from '../../helpers'; +import { MockedEndpoint, Mockttp } from '../../mock-e2e'; +import { Driver } from '../../webdriver/driver'; + +import FixtureBuilder from '../../fixture-builder'; + +async function mockSegment(mockServer: Mockttp) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Show native token as main balance' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} + +describe('Settings: Show native token as main balance', function () { + it('Should show balance in crypto when toggle is on', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().withConversionRateDisabled().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer: unknown; + }) => { + await logInWithBalanceValidation(driver, ganacheServer); + + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + const tokenValue = '25 ETH'; + const tokenListAmount = await driver.findElement( + '[data-testid="multichain-token-list-item-value"]', + ); + await driver.waitForNonEmptyElement(tokenListAmount); + assert.equal(await tokenListAmount.getText(), tokenValue); + }, + ); + }); + + it('Should show balance in fiat when toggle is OFF', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + await driver.clickElement({ + text: 'Advanced', + tag: 'div', + }); + await driver.clickElement('.show-fiat-on-testnets-toggle'); + + await driver.delay(1000); + + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // close popover + await driver.clickElement('[data-testid="popover-close"]'); + + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + + const tokenListAmount = await driver.findElement( + '.eth-overview__primary-container', + ); + assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); + }, + ); + }); + + it('Should not show popover twice', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + await driver.clickElement({ + text: 'Advanced', + tag: 'div', + }); + await driver.clickElement('.show-fiat-on-testnets-toggle'); + + await driver.delay(1000); + + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // close popover for the first time + await driver.clickElement('[data-testid="popover-close"]'); + // go to setting and back to home page and make sure popover is not shown again + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + // close setting + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // assert popover does not exist + await driver.assertElementNotPresent('[data-testid="popover-close"]'); + }, + ); + }); + + it('Should Successfully track the event when toggle is turned off', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-fd20', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint[]; + }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ + text: 'General', + tag: 'div', + }); + await driver.clickElement('.show-native-token-as-main-balance'); + + const events = await getEventPayloads(driver, mockedEndpoints); + expect(events[0].properties).toMatchObject({ + show_native_token_as_main_balance: false, + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + }, + ); + }); + + it('Should Successfully track the event when toggle is turned on', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-fd20', + participateInMetaMetrics: true, + }) + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint[]; + }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ + text: 'General', + tag: 'div', + }); + await driver.clickElement('.show-native-token-as-main-balance'); + + const events = await getEventPayloads(driver, mockedEndpoints); + expect(events[0].properties).toMatchObject({ + show_native_token_as_main_balance: true, + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/transaction/send-eth.spec.js b/test/e2e/tests/transaction/send-eth.spec.js index 36872115dcbe..5cbcb8309a18 100644 --- a/test/e2e/tests/transaction/send-eth.spec.js +++ b/test/e2e/tests/transaction/send-eth.spec.js @@ -189,7 +189,9 @@ describe('Send ETH', function () { const balance = await driver.findElement( '[data-testid="eth-overview__primary-currency"]', ); + assert.ok(/^[\d.]+\sETH$/u.test(await balance.getText())); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index b031611a06ea..82c55c9bd7e0 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -782,7 +782,6 @@ "showFiatInTestnets": false, "showTestNetworks": true, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": false, "showConfirmationAdvancedDetails": false }, diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index 06d85e298409..e651e9c2ce29 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -224,7 +224,6 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": null, - "useNativeCurrencyAsPrimaryCurrency": true, "hideZeroBalanceTokens": false, "petnamesEnabled": true, "redesignedConfirmationsEnabled": true, diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index ebc78c3ab378..a84ec99037f9 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -39,10 +39,6 @@ import { import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { - showPrimaryCurrency, - showSecondaryCurrency, -} from '../../../../../shared/modules/currency-display.utils'; import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -70,7 +66,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const nativeCurrency = useSelector(getMultichainNativeCurrency); const showFiat = useSelector(getMultichainShouldShowFiat); const isMainnet = useSelector(getMultichainIsMainnet); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); + const { showNativeTokenAsMainBalance } = useSelector(getPreferences); const { chainId, ticker, type, rpcUrl } = useSelector( getMultichainCurrentNetwork, ); @@ -92,11 +88,17 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const { currency: primaryCurrency, numberOfDecimals: primaryNumberOfDecimals, - } = useUserPreferencedCurrency(PRIMARY, { ethNumberOfDecimals: 4 }); + } = useUserPreferencedCurrency(PRIMARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); const { currency: secondaryCurrency, numberOfDecimals: secondaryNumberOfDecimals, - } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }); + } = useUserPreferencedCurrency(SECONDARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); const [primaryCurrencyDisplay, primaryCurrencyProperties] = useCurrencyDisplay(balance, { @@ -195,25 +197,14 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { title={nativeCurrency} // The primary and secondary currencies are subject to change based on the user's settings // TODO: rename this primary/secondary concept here to be more intuitive, regardless of setting - primary={ - showSecondaryCurrency( - isOriginalNativeSymbol, - useNativeCurrencyAsPrimaryCurrency, - ) - ? secondaryCurrencyDisplay - : undefined - } + primary={isOriginalNativeSymbol ? secondaryCurrencyDisplay : undefined} tokenSymbol={ - useNativeCurrencyAsPrimaryCurrency + showNativeTokenAsMainBalance ? primaryCurrencyProperties.suffix : secondaryCurrencyProperties.suffix } secondary={ - showFiat && - showPrimaryCurrency( - isOriginalNativeSymbol, - useNativeCurrencyAsPrimaryCurrency, - ) + showFiat && isOriginalNativeSymbol ? primaryCurrencyDisplay : undefined } diff --git a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap index 4eeeb5603d46..dfed6aeffa98 100644 --- a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap +++ b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap @@ -52,7 +52,7 @@ exports[`Token Cell should match snapshot 1`] = ` class="mm-box mm-box--display-flex" >

@@ -67,7 +67,7 @@ exports[`Token Cell should match snapshot 1`] = ` 5.00

5 diff --git a/ui/components/app/confirm/info/row/currency.stories.tsx b/ui/components/app/confirm/info/row/currency.stories.tsx index 2a520ca5bd35..ca9926e5cc6b 100644 --- a/ui/components/app/confirm/info/row/currency.stories.tsx +++ b/ui/components/app/confirm/info/row/currency.stories.tsx @@ -12,7 +12,7 @@ const store = configureStore({ ...mockState.metamask, preferences: { ...mockState.metamask.preferences, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, }, }); @@ -29,7 +29,7 @@ const ConfirmInfoRowCurrencyStory = { control: 'text', }, }, - decorators: [(story: any) => {story()}] + decorators: [(story: any) => {story()}], }; export const DefaultStory = ({ variant, value }) => ( diff --git a/ui/components/app/confirm/info/row/currency.tsx b/ui/components/app/confirm/info/row/currency.tsx index 82ce82c3a113..51ce1fceba28 100644 --- a/ui/components/app/confirm/info/row/currency.tsx +++ b/ui/components/app/confirm/info/row/currency.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { PRIMARY } from '../../../../../helpers/constants/common'; import { AlignItems, Display, @@ -38,7 +37,7 @@ export const ConfirmInfoRowCurrency = ({ {currency ? ( ) : ( - + )} ); diff --git a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js index 05bce9e841a0..8966fa01b749 100644 --- a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js +++ b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js @@ -11,9 +11,7 @@ describe('CancelTransactionGasFee Component', () => { metamask: { ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), currencyRates: {}, - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, completedOnboarding: true, internalAccounts: mockState.metamask.internalAccounts, }, diff --git a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap index cab80e399a43..020adaa0c952 100644 --- a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap +++ b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap @@ -75,7 +75,7 @@ exports[`Customize Nonce should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >

(selector) => { } else if (selector === getCurrentNetwork) { return { nickname: 'Ethereum Mainnet' }; } else if (selector === getPreferences) { - return ( - opts.preferences ?? { - useNativeCurrencyAsPrimaryCurrency: true, - } - ); + return opts.preferences ?? {}; } else if (selector === getShouldShowFiat) { return opts.shouldShowFiat ?? false; } else if (selector === getTokens) { diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 9e4ca5565733..3bf65d98d19c 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -12,6 +12,8 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showFiat?: boolean; showNative?: boolean; showCurrencySuffix?: boolean; + shouldCheckShowNativeToken?: boolean; + isAggregatedFiatOverviewBalance?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 294e6063f0ad..4b5492091288 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -25,6 +25,7 @@ export default function UserPreferencedCurrencyDisplay({ showFiat, showNative, showCurrencySuffix, + shouldCheckShowNativeToken, ...restProps }) { const currentNetwork = useMultichainSelector( @@ -42,6 +43,7 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals: propsNumberOfDecimals, showFiatOverride: showFiat, showNativeOverride: showNative, + shouldCheckShowNativeToken, }); const prefixComponent = useMemo(() => { return ( @@ -112,6 +114,7 @@ const UserPreferencedCurrencyDisplayPropTypes = { prefixComponentWrapperProps: PropTypes.object, textProps: PropTypes.object, suffixProps: PropTypes.object, + shouldCheckShowNativeToken: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.test.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.test.js index 51ee63d40c17..2a6193847fa4 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.test.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.test.js @@ -13,9 +13,7 @@ describe('UserPreferencedCurrencyDisplay Component', () => { ...mockState.metamask, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), currencyRates: {}, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, }, }; const mockStore = configureMockStore()(defaultState); diff --git a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js index 70b232848d16..7e34a90bba27 100644 --- a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js +++ b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js @@ -2,27 +2,21 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import CurrencyInput from '../currency-input'; +// Noticed this component is not used in codebase; +// removing usage of useNativeCurrencyAsPrimaryCurrency because its being removed in this PR export default class UserPreferencedCurrencyInput extends PureComponent { static propTypes = { - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, sendInputCurrencySwitched: PropTypes.bool, ...CurrencyInput.propTypes, }; render() { - const { - useNativeCurrencyAsPrimaryCurrency, - sendInputCurrencySwitched, - ...restProps - } = this.props; + const { sendInputCurrencySwitched, ...restProps } = this.props; return ( ); } diff --git a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js index 042b73c249ae..7bec54e5dd8f 100644 --- a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js +++ b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js @@ -1,13 +1,9 @@ import { connect } from 'react-redux'; import { toggleCurrencySwitch } from '../../../ducks/app/app'; -import { getPreferences } from '../../../selectors'; import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component'; const mapStateToProps = (state) => { - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - return { - useNativeCurrencyAsPrimaryCurrency, sendInputCurrencySwitched: state.appState.sendInputCurrencySwitched, }; }; diff --git a/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js index a285446100ed..ee8178664564 100644 --- a/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js +++ b/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import TokenInput from '../../ui/token-input'; import { getTokenSymbol } from '../../../store/actions'; +// This component is not used in codebase, removing usage of useNativeCurrencyAsPrimaryCurrency in this PR export default class UserPreferencedTokenInput extends PureComponent { static propTypes = { token: PropTypes.shape({ @@ -10,7 +11,6 @@ export default class UserPreferencedTokenInput extends PureComponent { decimals: PropTypes.number, symbol: PropTypes.string, }).isRequired, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, }; state = { @@ -28,16 +28,10 @@ export default class UserPreferencedTokenInput extends PureComponent { } render() { - const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props; + const { ...restProps } = this.props; return ( - + ); } } diff --git a/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js index 33afd8ce24a4..03c0c5eda7bf 100644 --- a/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js +++ b/ui/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js @@ -1,15 +1,8 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { getPreferences } from '../../../selectors'; import UserPreferencedTokenInput from './user-preferenced-token-input.component'; -const mapStateToProps = (state) => { - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - - return { - useNativeCurrencyAsPrimaryCurrency, - }; -}; +const mapStateToProps = (state) => state; const UserPreferencedTokenInputContainer = connect(mapStateToProps)( UserPreferencedTokenInput, diff --git a/ui/components/app/wallet-overview/__snapshots__/aggregated-percentage-overview.test.tsx.snap b/ui/components/app/wallet-overview/__snapshots__/aggregated-percentage-overview.test.tsx.snap new file mode 100644 index 000000000000..59dac675d1df --- /dev/null +++ b/ui/components/app/wallet-overview/__snapshots__/aggregated-percentage-overview.test.tsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggregatedPercentageOverview render renders correctly 1`] = ` +
+
+

+ +$0.00 +

+

+ (+0.00%) +

+
+
+`; diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx new file mode 100644 index 000000000000..95e0d92fa2b8 --- /dev/null +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx @@ -0,0 +1,592 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import { + getCurrentCurrency, + getSelectedAccount, + getShouldHideZeroBalanceTokens, + getTokensMarketData, +} from '../../../selectors'; +import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn((selector) => selector()), +})); + +jest.mock('../../../ducks/locale/locale', () => ({ + getIntlLocale: jest.fn(), +})); + +jest.mock('../../../selectors', () => ({ + getCurrentCurrency: jest.fn(), + getSelectedAccount: jest.fn(), + getShouldHideZeroBalanceTokens: jest.fn(), + getTokensMarketData: jest.fn(), +})); + +jest.mock('../../../hooks/useAccountTotalFiatBalance', () => ({ + useAccountTotalFiatBalance: jest.fn(), +})); + +const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock; +const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock; +const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock; +const mockGetShouldHideZeroBalanceTokens = + getShouldHideZeroBalanceTokens as jest.Mock; + +const mockGetTokensMarketData = getTokensMarketData as jest.Mock; + +const selectedAccountMock = { + id: 'd51c0116-de36-4e77-b35b-408d4ea82d01', + address: '0xa259af9db8172f62ef0373d7dfa893a3e245ace9', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 2', + importTime: 1725467263902, + lastSelected: 1725467263905, + keyring: { + type: 'Simple Key Pair', + }, + }, + balance: '0x0f7e2a03e67666', +}; + +const marketDataMock = { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.999893213343359, + pricePercentChange1d: -0.7173299395012226, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00041861840136257403, + pricePercentChange1d: -0.0862498076183525, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.0004185384042093742, + pricePercentChange1d: -0.07612981257899307, + }, + '0xdAC17F958D2ee523a2206206994597C13D831ec7': { + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + currency: 'ETH', + id: 'tether', + price: 0.0004183549552402562, + pricePercentChange1d: -0.1357979347463155, + }, +}; + +const positiveMarketDataMock = { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.999893213343359, + pricePercentChange1d: 0.7173299395012226, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00041861840136257403, + pricePercentChange1d: 0.0862498076183525, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.0004185384042093742, + pricePercentChange1d: 0.07612981257899307, + }, + '0xdAC17F958D2ee523a2206206994597C13D831ec7': { + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + currency: 'ETH', + id: 'tether', + price: 0.0004183549552402562, + pricePercentChange1d: 0.1357979347463155, + }, +}; + +const mixedMarketDataMock = { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.999893213343359, + pricePercentChange1d: -0.7173299395012226, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00041861840136257403, + pricePercentChange1d: 0.0862498076183525, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.0004185384042093742, + pricePercentChange1d: -0.07612981257899307, + }, + '0xdAC17F958D2ee523a2206206994597C13D831ec7': { + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + currency: 'ETH', + id: 'tether', + price: 0.0004183549552402562, + pricePercentChange1d: 0.1357979347463155, + }, +}; + +describe('AggregatedPercentageOverview', () => { + beforeEach(() => { + mockGetIntlLocale.mockReturnValue('en-US'); + mockGetCurrentCurrency.mockReturnValue('USD'); + mockGetSelectedAccount.mockReturnValue(selectedAccountMock); + mockGetShouldHideZeroBalanceTokens.mockReturnValue(false); + mockGetTokensMarketData.mockReturnValue(marketDataMock); + + jest.clearAllMocks(); + }); + + describe('render', () => { + it('renders correctly', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + fiatBalance: '0', + }, + ], + totalFiatBalance: 0, + }); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + it('should display zero percentage and amount if balance is zero', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + fiatBalance: '0', + }, + ], + totalFiatBalance: 0, + }); + + render(); + const percentageElement = screen.getByText('(+0.00%)'); + const numberElement = screen.getByText('+$0.00'); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display negative aggregated amount and percentage change with all negative market data', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', + name: 'USDC', + occurrences: 16, + symbol: 'USDC', + balance: '11754897', + string: '11.75489', + balanceError: null, + fiatBalance: '11.77', + }, + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + fiatBalance: '10.45', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + name: 'Dai Stablecoin', + occurrences: 17, + symbol: 'DAI', + balance: '6520850325578202013', + string: '6.52085', + balanceError: null, + fiatBalance: '6.53', + }, + { + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png', + name: 'Tether USD', + occurrences: 15, + symbol: 'USDT', + balance: '3379966', + string: '3.37996', + balanceError: null, + fiatBalance: '3.38', + }, + ], + totalFiatBalance: 32.13, + }); + const expectedAmountChange = '-$0.09'; + const expectedPercentageChange = '(-0.29%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display positive aggregated amount and percentage change with all positive market data', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', + name: 'USDC', + occurrences: 16, + symbol: 'USDC', + balance: '11754897', + string: '11.75489', + balanceError: null, + fiatBalance: '11.77', + }, + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + fiatBalance: '10.45', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + name: 'Dai Stablecoin', + occurrences: 17, + symbol: 'DAI', + balance: '6520850325578202013', + string: '6.52085', + balanceError: null, + fiatBalance: '6.53', + }, + { + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png', + name: 'Tether USD', + occurrences: 15, + symbol: 'USDT', + balance: '3379966', + string: '3.37996', + balanceError: null, + fiatBalance: '3.38', + }, + ], + totalFiatBalance: 32.13, + }); + mockGetTokensMarketData.mockReturnValue(positiveMarketDataMock); + const expectedAmountChange = '+$0.09'; + const expectedPercentageChange = '(+0.29%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display correct aggregated amount and percentage change with positive and negative market data', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', + name: 'USDC', + occurrences: 16, + symbol: 'USDC', + balance: '11754897', + string: '11.75489', + balanceError: null, + fiatBalance: '11.77', + }, + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + fiatBalance: '10.45', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + name: 'Dai Stablecoin', + occurrences: 17, + symbol: 'DAI', + balance: '6520850325578202013', + string: '6.52085', + balanceError: null, + fiatBalance: '6.53', + }, + { + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png', + name: 'Tether USD', + occurrences: 15, + symbol: 'USDT', + balance: '3379966', + string: '3.37996', + balanceError: null, + fiatBalance: '3.38', + }, + ], + totalFiatBalance: 32.13, + }); + mockGetTokensMarketData.mockReturnValue(mixedMarketDataMock); + const expectedAmountChange = '-$0.07'; + const expectedPercentageChange = '(-0.23%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + + it('should display correct aggregated amount and percentage when one ERC20 fiatBalance is undefined', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + fiatBalance: '21.12', + }, + { + symbol: 'USDC', + decimals: 6, + occurrences: 16, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + name: 'USDC', + balance: '11411142', + string: '11.41114', + balanceError: null, + fiatBalance: '11.4', + }, + { + symbol: 'DAI', + decimals: 18, + occurrences: 17, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + name: 'Dai Stablecoin', + balance: '3000000000000000000', + string: '3', + balanceError: null, + fiatBalance: '3', + }, + { + symbol: 'OMNI', + decimals: 18, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x36e66fbbce51e4cd5bd3c62b637eb411b18949d4.png', + address: '0x36e66fbbce51e4cd5bd3c62b637eb411b18949d4', + name: 'Omni Network', + balance: '2161382310000000000', + string: '2.16138', + balanceError: null, + }, + ], + totalFiatBalance: 35.52, + }); + mockGetTokensMarketData.mockReturnValue({ + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999598743668833, + marketCap: 120194359.82507178, + allTimeHigh: 2.070186924097962, + allTimeLow: 0.00018374327407907974, + totalVolume: 5495085.267342095, + high1d: 1.022994674939226, + low1d: 0.9882430202069277, + circulatingSupply: 120317181.32366, + dilutedMarketCap: 120194359.82507178, + marketCapPercentChange1d: -1.46534, + priceChange1d: -43.27897193472654, + pricePercentChange1h: 0.39406716228961414, + pricePercentChange1d: -1.8035792813549656, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00042436994422149745, + marketCap: 2179091.2357524647, + allTimeHigh: 0.0005177313319502269, + allTimeLow: 0.0003742773160055919, + totalVolume: 25770.310026921918, + high1d: 0.00042564305405416193, + low1d: 0.000422254035679609, + circulatingSupply: 5131139277.03183, + dilutedMarketCap: 2179157.495602445, + marketCapPercentChange1d: -2.78163, + priceChange1d: -0.000450570064429501, + pricePercentChange1h: 0.044140824068107716, + pricePercentChange1d: -0.045030461437871275, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.00042436994422149745, + marketCap: 14845337.78504687, + allTimeHigh: 0.000496512834739152, + allTimeLow: 0.00037244700843616456, + totalVolume: 2995848.8988073817, + high1d: 0.0004252186841099404, + low1d: 0.00042304081755619566, + circulatingSupply: 34942418774.2545, + dilutedMarketCap: 14849047.51464122, + marketCapPercentChange1d: 0.25951, + priceChange1d: -0.000469409459860959, + }, + }); + const expectedAmountChange = '-$0.39'; + const expectedPercentageChange = '(-1.08%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); + it('should display correct aggregated amount and percentage when the native fiatBalance is undefined', () => { + (useAccountTotalFiatBalance as jest.Mock).mockReturnValue({ + orderedTokenList: [ + { + iconUrl: './images/eth_logo.svg', + symbol: 'ETH', + }, + { + symbol: 'USDC', + decimals: 6, + occurrences: 16, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + name: 'USDC', + balance: '11411142', + string: '11.41114', + balanceError: null, + fiatBalance: '11.4', + }, + { + symbol: 'DAI', + decimals: 18, + occurrences: 17, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + name: 'Dai Stablecoin', + balance: '3000000000000000000', + string: '3', + balanceError: null, + fiatBalance: '20', + }, + ], + totalFiatBalance: 31.4, + }); + mockGetTokensMarketData.mockReturnValue({ + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + currency: 'ETH', + id: 'ethereum', + price: 0.9999598743668833, + marketCap: 120194359.82507178, + allTimeHigh: 2.070186924097962, + allTimeLow: 0.00018374327407907974, + totalVolume: 5495085.267342095, + high1d: 1.022994674939226, + low1d: 0.9882430202069277, + circulatingSupply: 120317181.32366, + dilutedMarketCap: 120194359.82507178, + marketCapPercentChange1d: -1.46534, + priceChange1d: -43.27897193472654, + pricePercentChange1h: 0.39406716228961414, + pricePercentChange1d: -1.8035792813549656, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + currency: 'ETH', + id: 'dai', + price: 0.00042436994422149745, + marketCap: 2179091.2357524647, + allTimeHigh: 0.0005177313319502269, + allTimeLow: 0.0003742773160055919, + totalVolume: 25770.310026921918, + high1d: 0.00042564305405416193, + low1d: 0.000422254035679609, + circulatingSupply: 5131139277.03183, + dilutedMarketCap: 2179157.495602445, + marketCapPercentChange1d: -2.78163, + priceChange1d: -0.000450570064429501, + pricePercentChange1h: 0.044140824068107716, + pricePercentChange1d: -0.045030461437871275, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + currency: 'ETH', + id: 'usd-coin', + price: 0.00042436994422149745, + marketCap: 14845337.78504687, + allTimeHigh: 0.000496512834739152, + allTimeLow: 0.00037244700843616456, + totalVolume: 2995848.8988073817, + high1d: 0.0004252186841099404, + low1d: 0.00042304081755619566, + circulatingSupply: 34942418774.2545, + dilutedMarketCap: 14849047.51464122, + marketCapPercentChange1d: 0.25951, + priceChange1d: -0.000469409459860959, + }, + }); + const expectedAmountChange = '-$0.01'; + const expectedPercentageChange = '(-0.03%)'; + render(); + const percentageElement = screen.getByText(expectedPercentageChange); + const numberElement = screen.getByText(expectedAmountChange); + expect(percentageElement).toBeInTheDocument(); + expect(numberElement).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx new file mode 100644 index 000000000000..e69ff1ed514d --- /dev/null +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx @@ -0,0 +1,143 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; + +import { zeroAddress, toChecksumAddress } from 'ethereumjs-util'; +import { + getCurrentCurrency, + getSelectedAccount, + getShouldHideZeroBalanceTokens, + getTokensMarketData, +} from '../../../selectors'; + +import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { formatValue, isValidAmount } from '../../../../app/scripts/lib/util'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import { + Display, + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { Box, Text } from '../../component-library'; +import { getCalculatedTokenAmount1dAgo } from '../../../helpers/utils/util'; + +// core already has this exported type but its not yet available in this version +// todo remove this and use core type once available +type MarketDataDetails = { + tokenAddress: string; + pricePercentChange1d: number; +}; + +export const AggregatedPercentageOverview = () => { + const tokensMarketData: Record = + useSelector(getTokensMarketData); + const locale = useSelector(getIntlLocale); + const fiatCurrency = useSelector(getCurrentCurrency); + const selectedAccount = useSelector(getSelectedAccount); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + // Get total balance (native + tokens) + const { totalFiatBalance, orderedTokenList } = useAccountTotalFiatBalance( + selectedAccount, + shouldHideZeroBalanceTokens, + ); + + // Memoize the calculation to avoid recalculating unless orderedTokenList or tokensMarketData changes + const totalFiat1dAgo = useMemo(() => { + return orderedTokenList.reduce((total1dAgo, item) => { + if (item.address) { + // This is a regular ERC20 token + // find the relevant pricePercentChange1d in tokensMarketData + // Find the corresponding market data for the token by filtering the values of the tokensMarketData object + const found = tokensMarketData[toChecksumAddress(item.address)]; + + const tokenFiat1dAgo = getCalculatedTokenAmount1dAgo( + item.fiatBalance, + found?.pricePercentChange1d, + ); + return total1dAgo + Number(tokenFiat1dAgo); + } + // native token + const nativePricePercentChange1d = + tokensMarketData?.[zeroAddress()]?.pricePercentChange1d; + const nativeFiat1dAgo = getCalculatedTokenAmount1dAgo( + item.fiatBalance, + nativePricePercentChange1d, + ); + return total1dAgo + Number(nativeFiat1dAgo); + }, 0); // Initial total1dAgo is 0 + }, [orderedTokenList, tokensMarketData]); // Dependencies: recalculate if orderedTokenList or tokensMarketData changes + + const totalBalance: number = Number(totalFiatBalance); + const totalBalance1dAgo = totalFiat1dAgo; + + const amountChange = totalBalance - totalBalance1dAgo; + const percentageChange = (amountChange / totalBalance1dAgo) * 100 || 0; + + const formattedPercentChange = formatValue( + amountChange === 0 ? 0 : percentageChange, + true, + ); + + let formattedAmountChange = ''; + if (isValidAmount(amountChange)) { + formattedAmountChange = (amountChange as number) >= 0 ? '+' : ''; + + const options = { + notation: 'compact', + compactDisplay: 'short', + maximumFractionDigits: 2, + } as const; + + try { + // For currencies compliant with ISO 4217 Standard + formattedAmountChange += `${Intl.NumberFormat(locale, { + ...options, + style: 'currency', + currency: fiatCurrency, + }).format(amountChange as number)} `; + } catch { + // Non-standard Currency Codes + formattedAmountChange += `${Intl.NumberFormat(locale, { + ...options, + minimumFractionDigits: 2, + style: 'decimal', + }).format(amountChange as number)} `; + } + } + + let color = TextColor.textDefault; + + if (isValidAmount(amountChange)) { + if ((amountChange as number) === 0) { + color = TextColor.textDefault; + } else if ((amountChange as number) > 0) { + color = TextColor.successDefault; + } else { + color = TextColor.errorDefault; + } + } + return ( + + + {formattedAmountChange} + + + {formattedPercentChange} + + + ); +}; diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index 5adbe8dcc927..2bc93e5e54eb 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -24,6 +24,7 @@ const BTC_OVERVIEW_PRIMARY_CURRENCY = 'coin-overview__primary-currency'; const mockMetaMetricsId = 'deadbeef'; const mockNonEvmBalance = '1'; +const mockNonEvmBalanceUsd = '1.00'; const mockNonEvmAccount = { address: 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', id: '542490c8-d178-433b-9f31-f680b11f45a5', @@ -112,7 +113,7 @@ describe('BtcOverview', () => { setBackgroundConnection({ setBridgeFeatureFlags: jest.fn() } as never); }); - it('shows the primary balance', async () => { + it('shows the primary balance as BTC when showNativeTokenAsMainBalance if true', async () => { const { queryByTestId, queryByText } = renderWithProvider( , getStore(), @@ -125,6 +126,27 @@ describe('BtcOverview', () => { expect(queryByText('*')).toBeInTheDocument(); }); + it('shows the primary balance as fiat when showNativeTokenAsMainBalance if false', async () => { + const { queryByTestId, queryByText } = renderWithProvider( + , + getStore({ + metamask: { + ...mockMetamaskStore, + // The balances won't be available + preferences: { + showNativeTokenAsMainBalance: false, + }, + }, + }), + ); + + const primaryBalance = queryByTestId(BTC_OVERVIEW_PRIMARY_CURRENCY); + expect(primaryBalance).toBeInTheDocument(); + expect(primaryBalance).toHaveTextContent(`$${mockNonEvmBalanceUsd}USD`); + // For now we consider balance to be always cached + expect(queryByText('*')).toBeInTheDocument(); + }); + it('shows a spinner if balance is not available', async () => { const { container } = renderWithProvider( , diff --git a/ui/components/app/wallet-overview/coin-buttons.stories.js b/ui/components/app/wallet-overview/coin-buttons.stories.js new file mode 100644 index 000000000000..40a6879673a8 --- /dev/null +++ b/ui/components/app/wallet-overview/coin-buttons.stories.js @@ -0,0 +1,37 @@ +import React from 'react'; +import CoinButtons from './coin-buttons'; + +export default { + title: 'Components/App/WalletOverview/CoinButtons', + args: { + chainId: '1', + trackingLocation: 'home', + isSwapsChain: true, + isSigningEnabled: true, + isBridgeChain: true, + isBuyableChain: true, + defaultSwapsToken: { + symbol: 'ETH', + name: 'Ether', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + balance: '3093640202103801', + string: '0.0031', + }, + classPrefix: 'coin', + iconButtonClassName: '', + }, + component: CoinButtons, + parameters: { + docs: { + description: { + component: 'A component that displays coin buttons', + }, + }, + }, +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 426713f5d086..0e1947d023f3 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -60,7 +60,7 @@ import { IconColor, JustifyContent, } from '../../../helpers/constants/design-system'; -import { Box, Icon, IconName } from '../../component-library'; +import { Box, Icon, IconName, IconSize } from '../../component-library'; import IconButton from '../../ui/icon-button'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import useRamps from '../../../hooks/ramps/useRamps/useRamps'; @@ -79,6 +79,7 @@ const CoinButtons = ({ defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix = 'coin', + iconButtonClassName = '', }: { chainId: `0x${string}` | CaipChainId | number; trackingLocation: string; @@ -90,6 +91,7 @@ const CoinButtons = ({ defaultSwapsToken?: SwapsEthToken; ///: END:ONLY_INCLUDE_IF classPrefix?: string; + iconButtonClassName?: string; }) => { const t = useContext(I18nContext); const dispatch = useDispatch(); @@ -191,15 +193,27 @@ const CoinButtons = ({ <> } + iconButtonClassName={iconButtonClassName} + Icon={ + + } label={t('stake')} onClick={handleMmiStakingOnClick} /> {mmiPortfolioEnabled && ( + } label={t('portfolio')} onClick={handleMmiPortfolioOnClick} @@ -308,8 +322,13 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + } disabled={!isBuyableChain} data-testid={`${classPrefix}-overview-buy`} @@ -327,9 +346,9 @@ const CoinButtons = ({ renderInstitutionalButtons() ///: END:ONLY_INCLUDE_IF } - } onClick={handleSwapOnClick} @@ -350,10 +370,15 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + } label={t('bridge')} onClick={handleBridgeOnClick} @@ -365,11 +390,13 @@ const CoinButtons = ({ } } disabled={!isSigningEnabled} @@ -389,11 +416,13 @@ const CoinButtons = ({ )} } label={t('receive')} diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 0293876539e1..c369ef0e89fd 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -1,17 +1,35 @@ import React, { useContext, + useState, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) useCallback, ///: END:ONLY_INCLUDE_IF } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; import { zeroAddress } from 'ethereumjs-util'; import { CaipChainId } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; + +import { + Box, + ButtonIcon, + ButtonIconSize, + ButtonLink, + ButtonLinkSize, + IconName, + Popover, + PopoverPosition, + Text, +} from '../../component-library'; +import { + AlignItems, + Display, + JustifyContent, + TextAlign, + TextVariant, +} from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -import { Icon, IconName, IconSize } from '../../component-library'; -import { IconColor } from '../../../helpers/constants/design-system'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { @@ -23,10 +41,14 @@ import { import { I18nContext } from '../../../contexts/i18n'; import Tooltip from '../../ui/tooltip'; import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; -import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; +import { PRIMARY } from '../../../helpers/constants/common'; import { getPreferences, + getSelectedAccount, + getShouldHideZeroBalanceTokens, getTokensMarketData, + getIsTestnet, + getShouldShowAggregatedBalancePopover, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getDataCollectionForMarketing, getMetaMetricsId, @@ -35,16 +57,17 @@ import { ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; import Spinner from '../../ui/spinner'; -import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; -import { showPrimaryCurrency } from '../../../../shared/modules/currency-display.utils'; + import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; -import { - getMultichainIsEvm, - getMultichainProviderConfig, - getMultichainShouldShowFiat, -} from '../../../selectors/multichain'; +import { getMultichainIsEvm } from '../../../selectors/multichain'; +import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { setAggregatedBalancePopoverShown } from '../../../store/actions'; +import { useTheme } from '../../../hooks/useTheme'; +import { getSpecificSettingsRoute } from '../../../helpers/utils/settings-search'; +import { useI18nContext } from '../../../hooks/useI18nContext'; import WalletOverview from './wallet-overview'; import CoinButtons from './coin-buttons'; +import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; export type CoinOverviewProps = { balance: string; @@ -83,7 +106,7 @@ export const CoinOverview = ({ } ///: END:ONLY_INCLUDE_IF - const t = useContext(I18nContext); + const t: ReturnType = useContext(I18nContext); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const trackEvent = useContext(MetaMetricsContext); @@ -91,19 +114,61 @@ export const CoinOverview = ({ const metaMetricsId = useSelector(getMetaMetricsId); const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); + ///: END:ONLY_INCLUDE_IF - const isEvm = useSelector(getMultichainIsEvm); - const showFiat = useSelector(getMultichainShouldShowFiat); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const { ticker, type, rpcUrl } = useSelector(getMultichainProviderConfig); - const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( - chainId, - ticker, - type, - rpcUrl, + const showNativeTokenAsMainBalanceRoute = getSpecificSettingsRoute( + t, + t('general'), + t('showNativeTokenAsMainBalance'), ); + const theme = useTheme(); + const dispatch = useDispatch(); + + const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover); + const isTestnet = useSelector(getIsTestnet); + const { showFiatInTestnets } = useSelector(getPreferences); + + const selectedAccount = useSelector(getSelectedAccount); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const { totalFiatBalance, loading } = useAccountTotalFiatBalance( + selectedAccount, + shouldHideZeroBalanceTokens, + ); + + const { showNativeTokenAsMainBalance } = useSelector(getPreferences); + + const isEvm = useSelector(getMultichainIsEvm); + const isNotAggregatedFiatBalance = + showNativeTokenAsMainBalance || isTestnet || !isEvm; + let balanceToDisplay; + if (isNotAggregatedFiatBalance) { + balanceToDisplay = balance; + } else if (!loading) { + balanceToDisplay = totalFiatBalance; + } + const tokensMarketData = useSelector(getTokensMarketData); + const [isOpen, setIsOpen] = useState(true); + + const handleMouseEnter = () => { + setIsOpen(true); + }; + + const handleClick = () => { + setIsOpen(!isOpen); + dispatch(setAggregatedBalancePopoverShown()); + }; + + const [referenceElement, setReferenceElement] = + useState(null); + const setBoxRef = (ref: HTMLSpanElement | null) => { + if (ref) { + setReferenceElement(ref); + } + }; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const handlePortfolioOnClick = useCallback(() => { @@ -126,6 +191,52 @@ export const CoinOverview = ({ }, [isMarketingEnabled, isMetaMetricsEnabled, metaMetricsId, trackEvent]); ///: END:ONLY_INCLUDE_IF + const renderPercentageAndAmountChange = () => { + if (isEvm) { + if (showNativeTokenAsMainBalance) { + return ( + + + { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + + {t('portfolio')} + + ///: END:ONLY_INCLUDE_IF + } + + ); + } + return ( + + + { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + + {t('portfolio')} + + ///: END:ONLY_INCLUDE_IF + } + + ); + } + return null; + }; + return (
-
- {balance ? ( +
+ {balanceToDisplay ? ( ) : ( @@ -168,43 +280,66 @@ export const CoinOverview = ({ )}
-
- {showFiat && isOriginalNativeSymbol && balance && ( - - )} - { - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -
- {t('portfolio')} - -
- ///: END:ONLY_INCLUDE_IF - } -
- {isEvm && ( - - )} + {shouldShowPopover && + (!isTestnet || (isTestnet && showFiatInTestnets)) && + !showNativeTokenAsMainBalance ? ( + + + + + {t('yourBalanceIsAggregated')} + + + + + + {t('aggregatedBalancePopover', [ + + {t('settings')} + , + ])} + + + + ) : null} + + {renderPercentageAndAmountChange()}
} @@ -221,6 +356,7 @@ export const CoinOverview = ({ defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix, + iconButtonClassName: `${classPrefix}-overview__icon-button`, }} /> } diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index a30654796aa4..cea749a366db 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -47,7 +47,7 @@ describe('EthOverview', () => { }, }, preferences: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }, useExternalServices: true, useCurrencyRateCheck: true, diff --git a/ui/components/app/wallet-overview/index.scss b/ui/components/app/wallet-overview/index.scss index bbbf57075c24..4759af1ffa8c 100644 --- a/ui/components/app/wallet-overview/index.scss +++ b/ui/components/app/wallet-overview/index.scss @@ -3,11 +3,9 @@ .wallet-overview { display: flex; justify-content: space-between; - align-items: center; + align-items: start; flex: 1; - min-height: 209px; min-width: 0; - padding-top: 10px; flex-direction: column; width: 100%; @@ -16,30 +14,26 @@ display: flex; gap: 4px; flex-direction: column; - align-items: center; + align-items: start; width: 100%; } - &__buttons { - display: flex; - flex-direction: row; - height: 68px; - margin-bottom: 24px; + &__icon_button { + margin-top: 0 !important; } - &__portfolio_button { + &__buttons { display: flex; flex-direction: row; - gap: 6px; - cursor: pointer; - align-items: center; - color: var(--color-primary-default); + height: 100%; + margin-bottom: 16px; + padding: 0 16px; } &__currency-wrapper { display: flex; flex-direction: row; - gap: 10px; + gap: 8px; } } @@ -61,14 +55,17 @@ display: flex; flex-direction: column; min-width: 0; - gap: 4px; position: relative; - align-items: center; + align-items: start; margin: 16px 0; padding: 0 16px; max-width: 326px; } + &__icon-button { + margin-top: 0 !important; + } + &__primary-container { display: flex; max-width: inherit; @@ -80,17 +77,13 @@ @include design-system.H2; color: var(--color-text-default); + font-weight: 700; } &__cached-star { margin-left: 4px; } - &__portfolio-button { - height: inherit; - padding-inline-start: 16px; - } - &__cached-balance, &__cached-star { color: var(--color-warning-default); @@ -158,12 +151,14 @@ color: var(--color-text-alternative); } - &__portfolio-button { - height: inherit; - padding-inline-start: 16px; - } - &__button:last-of-type { margin-right: 0; } } + +.balance-popover { + &__container { + z-index: design-system.$modal-z-index; + margin-top: -4px; + } +} diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.tsx index 862b98d350f3..f0327aaa227f 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.tsx @@ -47,9 +47,7 @@ const customData = { oldRefreshToken: 'abc', url: 'https://saturn-custody-ui.dev.metamask-institutional.io', }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, }, }; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx index 535b21f2e69b..97eaa364f104 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx @@ -48,9 +48,7 @@ describe('Interactive Replacement Token Modal', function () { oldRefreshToken: 'abc', url: 'https://saturn-custody-ui.dev.metamask-institutional.io', }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, }, }; diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx index fd169d23881f..381c4573add3 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx @@ -14,9 +14,7 @@ const customData = { '81f96a88b6cbc5f50d3864122349fa9a9755833ee82a7e3cf6f268c78aab51ab', url: 'url', }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, keyrings: [ { type: 'Custody - Saturn', diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx index 77424a95e86e..4c132ecbd414 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx @@ -72,9 +72,7 @@ describe('Interactive Replacement Token Notification', () => { }, isUnlocked: false, interactiveReplacementToken: { oldRefreshToken: 'abc' }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, keyrings: [ { type: KeyringType.imported, diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.test.tsx b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.test.tsx index 6613bf785b91..be9d58f23968 100644 --- a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.test.tsx @@ -11,7 +11,6 @@ const store = configureStore({ ...mockSendState, metamask: { ...mockSendState.metamask, - preferences: { useNativeCurrencyAsPrimaryCurrency: true }, }, appState: { ...mockSendState.appState, sendInputCurrencySwitched: false }, }); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx index 1ea66e917437..9061592cf37c 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx @@ -1,10 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; -import { - getPreferences, - getSelectedAccountCachedBalance, -} from '../../../../selectors'; +import { getSelectedAccountCachedBalance } from '../../../../selectors'; import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; @@ -46,7 +43,6 @@ export default function AssetList({ const nativeCurrency = useSelector(getNativeCurrency); const balanceValue = useSelector(getSelectedAccountCachedBalance); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const { currency: primaryCurrency, @@ -121,11 +117,7 @@ export default function AssetList({ primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value } - tokenSymbol={ - useNativeCurrencyAsPrimaryCurrency - ? primaryCurrency - : secondaryCurrency - } + tokenSymbol={primaryCurrency} secondary={secondaryCurrencyDisplay} tokenImage={token.image} isOriginalTokenSymbol diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index be0eb8282258..15cc339775c9 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -14,7 +14,6 @@ import { getCurrentChainId, getCurrentCurrency, getNativeCurrencyImage, - getPreferences, getSelectedAccountCachedBalance, getSelectedInternalAccount, getShouldHideZeroBalanceTokens, @@ -134,9 +133,7 @@ describe('AssetPickerModal', () => { if (selector === getTopAssets) { return []; } - if (selector === getPreferences) { - return { useNativeCurrencyAsPrimaryCurrency: false }; - } + if (selector === getSwapsBlockedTokens) { return new Set(['0xtoken1']); } diff --git a/ui/components/multichain/asset-picker-amount/nft-input/nft-input.test.tsx b/ui/components/multichain/asset-picker-amount/nft-input/nft-input.test.tsx index 05289c604aa3..08e4875ce149 100644 --- a/ui/components/multichain/asset-picker-amount/nft-input/nft-input.test.tsx +++ b/ui/components/multichain/asset-picker-amount/nft-input/nft-input.test.tsx @@ -5,15 +5,11 @@ import mockSendState from '../../../../../test/data/mock-send-state.json'; import configureStore from '../../../../store/store'; import { NFTInput } from './nft-input'; -const createStore = ({ - useNativeCurrencyAsPrimaryCurrency, - sendInputCurrencySwitched, -}: Record) => +const createStore = ({ sendInputCurrencySwitched }: Record) => configureStore({ ...mockSendState, metamask: { ...mockSendState.metamask, - preferences: { useNativeCurrencyAsPrimaryCurrency }, }, appState: { ...mockSendState.appState, sendInputCurrencySwitched }, }); @@ -25,7 +21,6 @@ describe('NFTInput', () => { const { asFragment } = render( @@ -39,7 +34,6 @@ describe('NFTInput', () => { const { getByTestId } = render( @@ -56,7 +50,6 @@ describe('NFTInput', () => { const { queryByTestId } = render( diff --git a/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.test.tsx b/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.test.tsx index 693361354f91..8dadccbaf247 100644 --- a/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.test.tsx +++ b/ui/components/multichain/asset-picker-amount/swappable-currency-input/swappable-currency-input.test.tsx @@ -6,15 +6,11 @@ import mockSendState from '../../../../../test/data/mock-send-state.json'; import configureStore from '../../../../store/store'; import { SwappableCurrencyInput } from './swappable-currency-input'; -const createStore = ({ - useNativeCurrencyAsPrimaryCurrency, - sendInputCurrencySwitched, -}: Record) => +const createStore = ({ sendInputCurrencySwitched }: Record) => configureStore({ ...mockSendState, metamask: { ...mockSendState.metamask, - preferences: { useNativeCurrencyAsPrimaryCurrency }, marketData: { ...mockSendState.metamask.marketData, '0x5': { @@ -37,7 +33,6 @@ describe('SwappableCurrencyInput', () => { const { asFragment, getByText } = render( @@ -68,7 +63,6 @@ describe('SwappableCurrencyInput', () => { const { asFragment, getByText } = render( @@ -101,7 +95,6 @@ describe('SwappableCurrencyInput', () => { const { asFragment } = render( @@ -134,7 +127,6 @@ describe('SwappableCurrencyInput', () => { const { asFragment } = render( diff --git a/ui/components/multichain/asset-picker-amount/utils.test.ts b/ui/components/multichain/asset-picker-amount/utils.test.ts index 91f25dc33d15..c83fe3db797b 100644 --- a/ui/components/multichain/asset-picker-amount/utils.test.ts +++ b/ui/components/multichain/asset-picker-amount/utils.test.ts @@ -2,23 +2,18 @@ import configureStore from '../../../store/store'; import mockSendState from '../../../../test/data/mock-send-state.json'; import { getIsFiatPrimary } from './utils'; -const createStore = ({ - useNativeCurrencyAsPrimaryCurrency, - sendInputCurrencySwitched, -}: Record) => +const createStore = ({ sendInputCurrencySwitched }: Record) => configureStore({ ...mockSendState, metamask: { ...mockSendState.metamask, - preferences: { useNativeCurrencyAsPrimaryCurrency }, }, appState: { ...mockSendState.appState, sendInputCurrencySwitched }, }); describe('getIsFiatPrimary selector', () => { - it('returns true when useNativeCurrencyAsPrimaryCurrency and sendInputCurrencySwitched are both true', () => { + it('returns true when sendInputCurrencySwitched is true', () => { const store = createStore({ - useNativeCurrencyAsPrimaryCurrency: true, sendInputCurrencySwitched: true, }); @@ -26,30 +21,11 @@ describe('getIsFiatPrimary selector', () => { expect(getIsFiatPrimary(state as never)).toBe(true); }); - it('returns true when useNativeCurrencyAsPrimaryCurrency and sendInputCurrencySwitched are both false', () => { + it('returns false when sendInputCurrencySwitched is false', () => { const store = createStore({ - useNativeCurrencyAsPrimaryCurrency: false, sendInputCurrencySwitched: false, }); const state = store.getState(); - expect(getIsFiatPrimary(state as never)).toBe(true); - }); - - it('returns false when useNativeCurrencyAsPrimaryCurrency and sendInputCurrencySwitched have different values', () => { - let store = createStore({ - useNativeCurrencyAsPrimaryCurrency: true, - sendInputCurrencySwitched: false, - }); - - let state = store.getState(); - expect(getIsFiatPrimary(state as never)).toBe(false); - - store = createStore({ - useNativeCurrencyAsPrimaryCurrency: false, - sendInputCurrencySwitched: true, - }); - - state = store.getState(); expect(getIsFiatPrimary(state as never)).toBe(false); }); }); diff --git a/ui/components/multichain/asset-picker-amount/utils.ts b/ui/components/multichain/asset-picker-amount/utils.ts index 664cba0d71f5..ed644c8a86d8 100644 --- a/ui/components/multichain/asset-picker-amount/utils.ts +++ b/ui/components/multichain/asset-picker-amount/utils.ts @@ -1,17 +1,14 @@ import { createSelector } from 'reselect'; +import { AppSliceState } from '../../../ducks/app/app'; -export const getIsFiatPrimary = createSelector( - (state: { - metamask: { preferences: { useNativeCurrencyAsPrimaryCurrency: boolean } }; - appState: { sendInputCurrencySwitched: boolean }; - }) => state.metamask.preferences, - (state) => state.appState.sendInputCurrencySwitched, - ({ useNativeCurrencyAsPrimaryCurrency }, sendInputCurrencySwitched) => { - const isFiatPrimary = Boolean( - (useNativeCurrencyAsPrimaryCurrency && sendInputCurrencySwitched) || - (!useNativeCurrencyAsPrimaryCurrency && !sendInputCurrencySwitched), - ); +function getSendInputCurrencySwitched(state: AppSliceState) { + return state.appState.sendInputCurrencySwitched; +} +export const getIsFiatPrimary = createSelector( + getSendInputCurrencySwitched, + (sendInputCurrencySwitched) => { + const isFiatPrimary = Boolean(sendInputCurrencySwitched); return isFiatPrimary; }, ); diff --git a/ui/components/multichain/pages/send/send.test.js b/ui/components/multichain/pages/send/send.test.js index cdba904090b2..5195ee15de5b 100644 --- a/ui/components/multichain/pages/send/send.test.js +++ b/ui/components/multichain/pages/send/send.test.js @@ -167,7 +167,6 @@ const baseStore = { }), tokens: [], preferences: { - useNativeCurrencyAsPrimaryCurrency: false, showFiatInTestnets: true, }, currentCurrency: 'USD', diff --git a/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap b/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap index 5c75df62eef1..1330a1490244 100644 --- a/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap +++ b/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap @@ -59,7 +59,7 @@ exports[`TokenListItem should render correctly 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

diff --git a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/__snapshots__/percentage-and-amount-change.test.tsx.snap b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/__snapshots__/percentage-and-amount-change.test.tsx.snap index e8ca2345490f..7f88ca5456ec 100644 --- a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/__snapshots__/percentage-and-amount-change.test.tsx.snap +++ b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/__snapshots__/percentage-and-amount-change.test.tsx.snap @@ -6,14 +6,14 @@ exports[`PercentageChange Component render renders correctly 1`] = ` class="mm-box mm-box--display-flex" >

+$12.21

(+5.12%) diff --git a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx index c22ff3d724f6..abff9f40da8d 100644 --- a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx +++ b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx @@ -39,7 +39,7 @@ const mockGetSelectedAccountCachedBalance = getSelectedAccountCachedBalance as jest.Mock; const mockGetConversionRate = getConversionRate as jest.Mock; const mockGetNativeCurrency = getNativeCurrency as jest.Mock; -const mockGetTOkensMarketData = getTokensMarketData as jest.Mock; +const mockGetTokensMarketData = getTokensMarketData as jest.Mock; describe('PercentageChange Component', () => { beforeEach(() => { @@ -48,7 +48,7 @@ describe('PercentageChange Component', () => { mockGetSelectedAccountCachedBalance.mockReturnValue('0x02e8ac1ede6ade83'); mockGetConversionRate.mockReturnValue(2913.15); mockGetNativeCurrency.mockReturnValue('ETH'); - mockGetTOkensMarketData.mockReturnValue({ + mockGetTokensMarketData.mockReturnValue({ [zeroAddress()]: { pricePercentChange1d: 2, }, diff --git a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx index 1dbf656986f3..be9921e88793 100644 --- a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx +++ b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx @@ -5,7 +5,6 @@ import { isHexString, zeroAddress } from 'ethereumjs-util'; import { Text, Box } from '../../../../component-library'; import { Display, - FontWeight, TextColor, TextVariant, } from '../../../../../helpers/constants/design-system'; @@ -28,7 +27,7 @@ import { // eslint-disable-next-line import/no-restricted-paths } from '../../../../../../app/scripts/lib/util'; -const renderPercentageWithNumber = ( +export const renderPercentageWithNumber = ( value: string, formattedValuePrice: string, color: TextColor, @@ -36,8 +35,7 @@ const renderPercentageWithNumber = ( return (

+5.12% diff --git a/ui/components/multichain/token-list-item/price/percentage-change/percentage-change.tsx b/ui/components/multichain/token-list-item/price/percentage-change/percentage-change.tsx index 71f54d2ae5dc..616814886078 100644 --- a/ui/components/multichain/token-list-item/price/percentage-change/percentage-change.tsx +++ b/ui/components/multichain/token-list-item/price/percentage-change/percentage-change.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Box, Text } from '../../../../component-library'; import { Display, - FontWeight, TextColor, TextVariant, } from '../../../../../helpers/constants/design-system'; @@ -37,8 +36,7 @@ export const PercentageChange = ({ return ( { ).toBeInTheDocument(); }); - it('should render crypto balance if useNativeCurrencyAsPrimaryCurrency is false', () => { + it('should render crypto balance', () => { const store = configureMockStore()({ ...state, - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, }); const propsToUse = { primary: '11.9751 ETH', diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index 1198d3bdd165..a653a803dc1c 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -396,7 +396,7 @@ export const TokenListItem = ({ {primary} {isNativeCurrency ? '' : tokenSymbol} diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index 15b40ecd8ae5..ca9322661d79 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -32,6 +32,7 @@ export default function CurrencyDisplay({ prefixComponentWrapperProps = {}, textProps = {}, suffixProps = {}, + isAggregatedFiatOverviewBalance = false, ...props }) { const [title, parts] = useCurrencyDisplay(value, { @@ -43,6 +44,7 @@ export default function CurrencyDisplay({ denomination, currency, suffix, + isAggregatedFiatOverviewBalance, }); return ( @@ -112,6 +114,7 @@ const CurrencyDisplayPropTypes = { prefixComponentWrapperProps: PropTypes.object, textProps: PropTypes.object, suffixProps: PropTypes.object, + isAggregatedFiatOverviewBalance: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/components/ui/dropdown/dropdown.scss b/ui/components/ui/dropdown/dropdown.scss index e76d7936d303..395a61b68eee 100644 --- a/ui/components/ui/dropdown/dropdown.scss +++ b/ui/components/ui/dropdown/dropdown.scss @@ -3,7 +3,7 @@ .dropdown { position: relative; display: inline-block; - height: 36px; + height: 48px; &__select { appearance: none; @@ -15,9 +15,9 @@ color: var(--color-text-default); border: 1px solid var(--color-border-default); - border-radius: 4px; + border-radius: 8px; background-color: var(--color-background-default); - padding: 8px 40px 8px 16px; + padding: 12px 40px 12px 16px; width: 100%; [dir='rtl'] & { diff --git a/ui/components/ui/icon-button/icon-button.js b/ui/components/ui/icon-button/icon-button.js index 69830128ebcd..30b14c0aa205 100644 --- a/ui/components/ui/icon-button/icon-button.js +++ b/ui/components/ui/icon-button/icon-button.js @@ -16,6 +16,7 @@ export default function IconButton(props) { label, tooltipRender, className, + iconButtonClassName = '', ...otherProps } = props; const renderWrapper = tooltipRender ?? defaultRender; @@ -31,7 +32,10 @@ export default function IconButton(props) { > {renderWrapper( <> -

+
{Icon}
{label.length > 10 ? ( @@ -66,5 +70,6 @@ IconButton.propTypes = { label: PropTypes.string.isRequired, tooltipRender: PropTypes.func, className: PropTypes.string, + iconButtonClassName: PropTypes.string, 'data-testid': PropTypes.string, }; diff --git a/ui/components/ui/icon-button/icon-button.scss b/ui/components/ui/icon-button/icon-button.scss index 97529366befa..e09373b23744 100644 --- a/ui/components/ui/icon-button/icon-button.scss +++ b/ui/components/ui/icon-button/icon-button.scss @@ -6,11 +6,11 @@ align-items: center; background-color: unset; text-align: center; - width: 60px; + width: 64px; @include design-system.H7; - font-size: 13px; + font-size: 12px; cursor: pointer; color: var(--color-primary-default); @@ -21,9 +21,9 @@ height: 36px; width: 36px; background: var(--color-primary-default); - border-radius: 18px; + border-radius: 99px; margin-top: 6px; - margin-bottom: 5px; + margin-bottom: 4px; margin-inline: auto; } diff --git a/ui/components/ui/text-field/text-field.component.js b/ui/components/ui/text-field/text-field.component.js index 6ed0a9bff6fb..0a74978973b3 100644 --- a/ui/components/ui/text-field/text-field.component.js +++ b/ui/components/ui/text-field/text-field.component.js @@ -86,13 +86,14 @@ const styles = { border: '1px solid var(--color-border-default)', color: 'var(--color-text-default)', height: '48px', - borderRadius: '6px', padding: '0 16px', display: 'flex', alignItems: 'center', '&$inputFocused': { border: '1px solid var(--color-primary-default)', }, + borderRadius: '8px', + fontSize: '0.875rem', }, largeInputLabel: { ...inputLabelBase, @@ -212,6 +213,7 @@ const getBorderedThemeInputProps = ({ max, autoComplete, }, + disableUnderline: 'true', }, }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index a2e553f34012..182ba426a3d7 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -105,7 +105,7 @@ type AppState = { isMultiRpcOnboarding: boolean; }; -type AppSliceState = { +export type AppSliceState = { appState: AppState; }; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 4765a87df61d..05cc6d46cb27 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -48,7 +48,6 @@ const initialState = { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, petnamesEnabled: true, featureNotificationsEnabled: false, showMultiRpcModal: false, diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index f4529ed1d6ad..569999f8900e 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -34,9 +34,9 @@ const SETTINGS_CONSTANTS = [ }, { tabMessage: (t) => t('general'), - sectionMessage: (t) => t('primaryCurrencySetting'), - descriptionMessage: (t) => t('primaryCurrencySettingDescription'), - route: `${GENERAL_ROUTE}#primary-currency`, + sectionMessage: (t) => t('showNativeTokenAsMainBalance'), + descriptionMessage: (t) => t('showNativeTokenAsMainBalance'), + route: `${GENERAL_ROUTE}#show-native-token-as-main-balance`, iconName: IconName.Setting, }, { diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 447a39901059..af13e1d71c65 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -25,6 +25,15 @@ function getFilteredSettingsRoutes(t, tabMessage) { }); } +export function getSpecificSettingsRoute(t, tabMessage, sectionMessage) { + return getSettingsRoutes().find((routeObject) => { + return ( + routeObject.tabMessage(t) === tabMessage && + routeObject.sectionMessage(t) === sectionMessage + ); + }); +} + /** * @param {Function} t - context.t function * @param {string} tabMessage diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 709b0354a4e7..1e440b8f0aff 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -4,6 +4,7 @@ import { getSettingsRoutes, getNumberOfSettingRoutesInTab, handleSettingsRefs, + getSpecificSettingsRoute, } from './settings-search'; const t = (key) => { @@ -209,4 +210,17 @@ describe('Settings Search Utils', () => { expect(handleSettingsRefs(t, t('general'), settingsRefs)).toBeUndefined(); }); }); + + describe('getSpecificSettingsRoute', () => { + it('should return show native token as main balance route', () => { + const result = getSpecificSettingsRoute( + t, + t('general'), + t('showNativeTokenAsMainBalance'), + ); + expect(result.route).toBe( + '/settings/general#show-native-token-as-main-balance', + ); + }); + }); }); diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 01cffeea3cdc..fe01bd15f5b1 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -823,3 +823,18 @@ export const getFilteredSnapPermissions = ( return filteredPermissions; }; +/** + * Helper function to calculate the token amount 1dAgo using price percentage a day ago. + * + * @param {*} tokenFiatBalance - current token fiat balance + * @param {*} tokenPricePercentChange1dAgo - price percentage 1day ago + * @returns token amount 1day ago + */ +export const getCalculatedTokenAmount1dAgo = ( + tokenFiatBalance, + tokenPricePercentChange1dAgo, +) => { + return tokenPricePercentChange1dAgo !== undefined && tokenFiatBalance + ? tokenFiatBalance / (1 + tokenPricePercentChange1dAgo / 100) + : tokenFiatBalance ?? 0; +}; diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index e10c70630dba..dd2282efa531 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -1226,4 +1226,37 @@ describe('util', () => { ]); }); }); + + describe('getCalculatedTokenAmount1dAgo', () => { + it('should return successfully balance of token 1dago', () => { + const mockTokenFiatAmount = '10'; + const mockTokenPercent1dAgo = 1; + const expectedRes = 9.900990099009901; + const result = util.getCalculatedTokenAmount1dAgo( + mockTokenFiatAmount, + mockTokenPercent1dAgo, + ); + expect(result).toBe(expectedRes); + }); + + it('should return token balance if percentage is undefined', () => { + const mockTokenFiatAmount = '10'; + const mockTokenPercent1dAgo = undefined; + const result = util.getCalculatedTokenAmount1dAgo( + mockTokenFiatAmount, + mockTokenPercent1dAgo, + ); + expect(result).toBe(mockTokenFiatAmount); + }); + + it('should return zero if token amount is undefined', () => { + const mockTokenFiatAmount = undefined; + const mockTokenPercent1dAgo = 1; + const result = util.getCalculatedTokenAmount1dAgo( + mockTokenFiatAmount, + mockTokenPercent1dAgo, + ); + expect(result).toBe(0); + }); + }); }); diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index a7798d5ff10e..12b2cfc06ec3 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -135,6 +135,7 @@ export function useCurrencyDisplay( numberOfDecimals, denomination, currency, + isAggregatedFiatOverviewBalance, ...opts }, ) { @@ -151,6 +152,7 @@ export function useCurrencyDisplay( getMultichainConversionRate, account, ); + const isUserPreferredCurrency = currency === currentCurrency; const isNativeCurrency = currency === nativeCurrency; @@ -172,6 +174,10 @@ export function useCurrencyDisplay( }); } + if (isAggregatedFiatOverviewBalance) { + return formatCurrency(inputValue, currency); + } + return formatEthCurrencyDisplay({ isNativeCurrency, isUserPreferredCurrency, @@ -194,6 +200,7 @@ export function useCurrencyDisplay( denomination, numberOfDecimals, currentCurrency, + isAggregatedFiatOverviewBalance, ]); let suffix; diff --git a/ui/hooks/useTransactionDisplayData.test.js b/ui/hooks/useTransactionDisplayData.test.js index 0aa77675bd7c..68a4d829bcf8 100644 --- a/ui/hooks/useTransactionDisplayData.test.js +++ b/ui/hooks/useTransactionDisplayData.test.js @@ -211,7 +211,6 @@ const renderHookWithRouter = (cb, tokenAddress) => { currentCurrency: 'ETH', useCurrencyRateCheck: false, // to force getShouldShowFiat to return false preferences: { - useNativeCurrencyAsPrimaryCurrency: true, getShowFiatInTestnets: false, }, allNfts: [], diff --git a/ui/hooks/useUserPreferencedCurrency.js b/ui/hooks/useUserPreferencedCurrency.js index 732d4ec726bb..e9e8133cf9e2 100644 --- a/ui/hooks/useUserPreferencedCurrency.js +++ b/ui/hooks/useUserPreferencedCurrency.js @@ -6,7 +6,7 @@ import { getMultichainShouldShowFiat, } from '../selectors/multichain'; -import { PRIMARY, SECONDARY } from '../helpers/constants/common'; +import { PRIMARY } from '../helpers/constants/common'; import { EtherDenomination } from '../../shared/constants/common'; import { ETH_DEFAULT_DECIMALS } from '../constants'; import { useMultichainSelector } from './useMultichainSelector'; @@ -20,6 +20,8 @@ import { useMultichainSelector } from './useMultichainSelector'; * when using ETH * @property {number} [fiatNumberOfDecimals] - Number of significant decimals to display * when using fiat + * @property {boolean} [shouldCheckShowNativeToken] - Boolean to know if checking the setting + * show native token as main balance is needed */ /** @@ -34,9 +36,10 @@ import { useMultichainSelector } from './useMultichainSelector'; * useUserPreferencedCurrency * * returns an object that contains what currency to use for displaying values based - * on the user's preference settings, as well as the significant number of decimals + * on whether the user needs to check showNativeTokenAsMainBalance setting, as well as the significant number of decimals * to display based on the currency * + * * @param {"PRIMARY" | "SECONDARY"} type - what display type is being rendered * @param {UseUserPreferencedCurrencyOptions} opts - options to override default values * @returns {UserPreferredCurrency} @@ -49,7 +52,7 @@ export function useUserPreferencedCurrency(type, opts = {}) { account, ); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector( + const { showNativeTokenAsMainBalance } = useSelector( getPreferences, shallowEqual, ); @@ -74,12 +77,13 @@ export function useUserPreferencedCurrency(type, opts = {}) { return nativeReturn; } else if (opts.showFiatOverride) { return fiatReturn; + } else if (!showFiat) { + return nativeReturn; } else if ( - !showFiat || - (type === PRIMARY && useNativeCurrencyAsPrimaryCurrency) || - (type === SECONDARY && !useNativeCurrencyAsPrimaryCurrency) + (opts.shouldCheckShowNativeToken && showNativeTokenAsMainBalance) || + !opts.shouldCheckShowNativeToken ) { - return nativeReturn; + return type === PRIMARY ? nativeReturn : fiatReturn; } - return fiatReturn; + return type === PRIMARY ? fiatReturn : nativeReturn; } diff --git a/ui/hooks/useUserPreferencedCurrency.test.js b/ui/hooks/useUserPreferencedCurrency.test.js index 12785d44b5ff..9421834b0e31 100644 --- a/ui/hooks/useUserPreferencedCurrency.test.js +++ b/ui/hooks/useUserPreferencedCurrency.test.js @@ -8,16 +8,43 @@ import { mockNetworkState } from '../../test/stub/networks'; import { CHAIN_IDS } from '../../shared/constants/network'; import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; +const renderUseUserPreferencedCurrency = (state, value, restProps) => { + const defaultState = { + ...mockState, + metamask: { + ...mockState.metamask, + completedOnboarding: true, + ...mockNetworkState({ + chainId: state.showFiat ? CHAIN_IDS.MAINNET : CHAIN_IDS.SEPOLIA, + ticker: state?.nativeCurrency, + }), + currentCurrency: state.currentCurrency, + currencyRates: { ETH: { conversionRate: 280.45 } }, + preferences: { + showFiatInTestnets: state.showFiat, + showNativeTokenAsMainBalance: state.showNativeTokenAsMainBalance, + }, + }, + }; + + const wrapper = ({ children }) => ( + {children} + ); + + return renderHook(() => useUserPreferencedCurrency(value, restProps), { + wrapper, + }); +}; const tests = [ { state: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + showNativeOverride: true, }, result: { currency: 'ETH', @@ -26,13 +53,13 @@ const tests = [ }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + showFiatOverride: true, }, result: { currency: 'usd', @@ -41,45 +68,59 @@ const tests = [ }, { state: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { - type: 'SECONDARY', - fiatNumberOfDecimals: 4, - fiatPrefix: '-', + type: 'PRIMARY', + shouldCheckShowNativeToken: true, }, result: { - currency: undefined, - numberOfDecimals: 4, + currency: 'ETH', + numberOfDecimals: 8, }, }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { - type: 'SECONDARY', - fiatNumberOfDecimals: 4, - numberOfDecimals: 3, - fiatPrefix: 'a', + type: 'PRIMARY', }, result: { currency: 'ETH', - numberOfDecimals: 3, + numberOfDecimals: 8, + }, + }, + { + state: { + showNativeTokenAsMainBalance: false, + nativeCurrency: 'ETH', + showFiat: true, + currentCurrency: 'usd', + }, + params: { + type: 'SECONDARY', + }, + result: { + currency: 'usd', + numberOfDecimals: 2, }, }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, nativeCurrency: 'ETH', showFiat: false, + currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + type: 'SECONDARY', }, result: { currency: 'ETH', @@ -88,66 +129,54 @@ const tests = [ }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { type: 'PRIMARY', }, result: { - currency: undefined, + currency: 'ETH', + numberOfDecimals: 8, + }, + }, + { + state: { + showNativeTokenAsMainBalance: true, + nativeCurrency: 'ETH', + showFiat: true, + currentCurrency: 'usd', + }, + params: { + type: 'SECONDARY', + }, + result: { + currency: 'usd', numberOfDecimals: 2, }, }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + type: 'SECONDARY', + shouldCheckShowNativeToken: true, }, result: { - currency: undefined, + currency: 'usd', numberOfDecimals: 2, }, }, ]; - -const renderUseUserPreferencedCurrency = (state, value, restProps) => { - const defaultState = { - ...mockState, - metamask: { - ...mockState.metamask, - completedOnboarding: true, - ...mockNetworkState({ - chainId: state.showFiat ? CHAIN_IDS.MAINNET : CHAIN_IDS.SEPOLIA, - ticker: state?.nativeCurrency, - }), - currentCurrency: state.currentCurrency, - currencyRates: { ETH: { conversionRate: 280.45 } }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: - state.useNativeCurrencyAsPrimaryCurrency, - showFiatInTestnets: state.showFiat, - }, - }, - }; - - const wrapper = ({ children }) => ( - {children} - ); - - return renderHook(() => useUserPreferencedCurrency(value, restProps), { - wrapper, - }); -}; - describe('useUserPreferencedCurrency', () => { tests.forEach(({ params: { type, ...otherParams }, state, result }) => { - describe(`when showFiat is ${state.showFiat}, useNativeCurrencyAsPrimary is ${state.useNativeCurrencyAsPrimaryCurrency} and type is ${type}`, () => { + describe(`when showFiat is ${state.showFiat}, shouldCheckShowNativeToken is ${otherParams.shouldCheckShowNativeToken}, showNativeTokenAsMainBalance is ${state.showNativeTokenAsMainBalance} and type is ${type}`, () => { const { result: hookResult } = renderUseUserPreferencedCurrency( state, type, diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index ef6e6331d2ba..95828e3e250e 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -43,7 +43,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -61,7 +61,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -79,7 +79,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -99,7 +99,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -118,7 +118,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -137,7 +137,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -156,7 +156,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -236,7 +236,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 TEST @@ -352,7 +352,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >

@@ -371,7 +371,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -390,7 +390,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -409,7 +409,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -427,7 +427,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -446,7 +446,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -515,7 +515,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` class="mm-box mm-box--display-flex" >

@@ -528,7 +528,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 @@ -835,7 +835,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >

@@ -854,7 +854,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -873,7 +873,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -892,7 +892,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -910,7 +910,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -929,7 +929,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -998,7 +998,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` class="mm-box mm-box--display-flex" >

@@ -1011,7 +1011,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 diff --git a/ui/pages/asset/components/asset-page.test.tsx b/ui/pages/asset/components/asset-page.test.tsx index bf616d0aaac9..35721a30a1c2 100644 --- a/ui/pages/asset/components/asset-page.test.tsx +++ b/ui/pages/asset/components/asset-page.test.tsx @@ -49,9 +49,7 @@ describe('AssetPage', () => { }, }, useCurrencyRateCheck: true, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, internalAccounts: { accounts: { 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index a07cdaca2d48..bb3f129bade8 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -48,7 +48,12 @@ import { JustifyContent, } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; -import { Box, Icon, IconName } from '../../../components/component-library'; +import { + Box, + Icon, + IconName, + IconSize, +} from '../../../components/component-library'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF @@ -115,7 +120,11 @@ const TokenButtons = ({ + } label={t('buyAndSell')} data-testid="token-overview-buy" @@ -144,7 +153,11 @@ const TokenButtons = ({ + } label={t('stake')} data-testid="token-overview-mmi-stake" @@ -163,6 +176,7 @@ const TokenButtons = ({ } label={t('portfolio')} @@ -215,6 +229,7 @@ const TokenButtons = ({ } label={t('send')} @@ -229,6 +244,7 @@ const TokenButtons = ({ } onClick={() => { @@ -281,7 +297,11 @@ const TokenButtons = ({ className="token-overview__button" data-testid="token-overview-bridge" Icon={ - + } label={t('bridge')} onClick={() => { diff --git a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js index fd6585381a27..88d7c8deb40b 100644 --- a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js +++ b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js @@ -10,9 +10,7 @@ import { decryptMsgInline, } from '../../store/actions'; import { - conversionRateSelector, getCurrentCurrency, - getPreferences, getTargetAccountWithSendEtherInfo, unconfirmedTransactionsListSelector, } from '../../selectors'; @@ -21,13 +19,12 @@ import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getNativeCurrency } from '../../ducks/metamask/metamask'; import ConfirmDecryptMessage from './confirm-decrypt-message.component'; +// ConfirmDecryptMessage component is not used in codebase, removing usage of useNativeCurrencyAsPrimaryCurrency function mapStateToProps(state) { const { metamask: { subjectMetadata = {} }, } = state; - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - const unconfirmedTransactions = unconfirmedTransactionsListSelector(state); const txData = cloneDeep(unconfirmedTransactions[0]); @@ -43,9 +40,7 @@ function mapStateToProps(state) { fromAccount, requester: null, requesterAddress: null, - conversionRate: useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateSelector(state), + conversionRate: null, mostRecentOverviewPage: getMostRecentOverviewPage(state), nativeCurrency: getNativeCurrency(state), currentCurrency: getCurrentCurrency(state), diff --git a/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap b/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap index 05cee4e7016f..b6ae6ed60c6f 100644 --- a/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap +++ b/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap @@ -208,252 +208,6 @@ exports[`ConfirmDecryptMessage Component should match snapshot when preference i -

- - test - -
- would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you. - - -
- -
-
- -
-
-`; - -exports[`ConfirmDecryptMessage Component should match snapshot when preference is Fiat currency 1`] = ` -
-
-
-
-
- Request encryption public key -
-
-
-
-
-
- -
-
- - T - -
- - -
diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js index 1595576a4fac..dd84bea68360 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js @@ -10,8 +10,6 @@ import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics' import SiteOrigin from '../../components/ui/site-origin'; import { Numeric } from '../../../shared/modules/Numeric'; import { EtherDenomination } from '../../../shared/constants/common'; -import { formatCurrency } from '../../helpers/utils/confirm-tx.util'; -import { getValueFromWeiHex } from '../../../shared/modules/conversion.utils'; export default class ConfirmEncryptionPublicKey extends Component { static contextTypes = { @@ -34,8 +32,6 @@ export default class ConfirmEncryptionPublicKey extends Component { subjectMetadata: PropTypes.object, mostRecentOverviewPage: PropTypes.string.isRequired, nativeCurrency: PropTypes.string.isRequired, - currentCurrency: PropTypes.string.isRequired, - conversionRate: PropTypes.number, }; renderHeader = () => { @@ -73,30 +69,20 @@ export default class ConfirmEncryptionPublicKey extends Component { renderBalance = () => { const { - conversionRate, nativeCurrency, - currentCurrency, fromAccount: { balance }, } = this.props; const { t } = this.context; - const nativeCurrencyBalance = conversionRate - ? formatCurrency( - getValueFromWeiHex({ - value: balance, - fromCurrency: nativeCurrency, - toCurrency: currentCurrency, - conversionRate, - numberOfDecimals: 6, - toDenomination: EtherDenomination.ETH, - }), - currentCurrency, - ) - : new Numeric(balance, 16, EtherDenomination.WEI) - .toDenomination(EtherDenomination.ETH) - .round(6) - .toBase(10) - .toString(); + const nativeCurrencyBalance = new Numeric( + balance, + 16, + EtherDenomination.WEI, + ) + .toDenomination(EtherDenomination.ETH) + .round(6) + .toBase(10) + .toString(); return (
@@ -104,9 +90,7 @@ export default class ConfirmEncryptionPublicKey extends Component { {`${t('balance')}:`}
- {`${nativeCurrencyBalance} ${ - conversionRate ? currentCurrency?.toUpperCase() : nativeCurrency - }`} + {`${nativeCurrencyBalance} ${nativeCurrency}`}
); diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js index 771a5a5f5ef7..3edc10e9d313 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js @@ -61,19 +61,6 @@ describe('ConfirmDecryptMessage Component', () => { ).toMatchInlineSnapshot(`"966.987986 ABC"`); }); - it('should match snapshot when preference is Fiat currency', () => { - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - expect( - container.querySelector('.request-encryption-public-key__balance-value') - .textContent, - ).toMatchInlineSnapshot(`"1520956.064158 DEF"`); - }); - it('should match snapshot when there is no txData', () => { const newProps = { ...baseProps, diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js index 489d7d088033..554ba41fdaa4 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js @@ -9,11 +9,8 @@ import { } from '../../store/actions'; import { - conversionRateSelector, unconfirmedTransactionsListSelector, getTargetAccountWithSendEtherInfo, - getPreferences, - getCurrentCurrency, } from '../../selectors'; import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; @@ -26,8 +23,6 @@ function mapStateToProps(state) { metamask: { subjectMetadata = {} }, } = state; - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - const unconfirmedTransactions = unconfirmedTransactionsListSelector(state); const txData = unconfirmedTransactions[0]; @@ -43,12 +38,8 @@ function mapStateToProps(state) { fromAccount, requester: null, requesterAddress: null, - conversionRate: useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateSelector(state), mostRecentOverviewPage: getMostRecentOverviewPage(state), nativeCurrency: getNativeCurrency(state), - currentCurrency: getCurrentCurrency(state), }; } diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js index c80ef735222b..c1d4ae838301 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js @@ -39,9 +39,7 @@ const render = async ({ transactionProp = {}, contextProps = {} } = {}) => { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, gasFeeEstimatesByChainId: { diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js index 34440be28693..70d5ed1070f4 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js @@ -5,7 +5,6 @@ import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { getIsMainnet, - getPreferences, getUnapprovedTransactions, getUseCurrencyRateCheck, transactionFeeSelector, @@ -34,7 +33,6 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => { // state selectors const isMainnet = useSelector(getIsMainnet); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); const transactionData = useDraftTransactionWithTxParams(); const txData = useSelector((state) => txDataSelector(state)); @@ -108,7 +106,7 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => {
) @@ -119,7 +117,6 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => { { key="editGasSubTextFeeAmount" type={PRIMARY} value={estimatedHexMaxFeeTotal} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} />
diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js index df5b9ea0e50f..4952fb87edca 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js @@ -21,9 +21,6 @@ const mmState = { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, }, confirmTransaction: { txData: { diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js index 5b1e505ddc14..5d3065e8a3d3 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js @@ -11,9 +11,7 @@ describe('Confirm Detail Row Component', () => { metamask: { currencyRates: {}, ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, internalAccounts: defaultMockState.metamask.internalAccounts, }, }; diff --git a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js index e1219d299288..da99ade8210c 100644 --- a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js +++ b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js @@ -29,7 +29,6 @@ const ConfirmSubTitle = ({ if (subtitleComponent) { return subtitleComponent; } - return ( { const t = useI18nContext(); - const { useNativeCurrencyAsPrimaryCurrency: isNativeCurrencyUsed } = - useSelector(getPreferences); - const { currentConfirmation: transactionMeta } = useConfirmContext(); @@ -56,14 +51,14 @@ export const EditGasFeesRow = ({ color={TextColor.textDefault} data-testid="first-gas-field" > - {isNativeCurrencyUsed ? nativeFee : fiatFee} + {nativeFee} - {isNativeCurrencyUsed ? fiatFee : nativeFee} + {fiatFee} { - const { useNativeCurrencyAsPrimaryCurrency: isNativeCurrencyUsed } = - useSelector(getPreferences); - return ( - {isNativeCurrencyUsed ? nativeFee : fiatFee} - - - {isNativeCurrencyUsed ? fiatFee : nativeFee} + {nativeFee} + {fiatFee} ); diff --git a/ui/pages/confirmations/components/fee-details-component/fee-details-component.js b/ui/pages/confirmations/components/fee-details-component/fee-details-component.js index 19bcc25e445f..84ea244ec8cd 100644 --- a/ui/pages/confirmations/components/fee-details-component/fee-details-component.js +++ b/ui/pages/confirmations/components/fee-details-component/fee-details-component.js @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { AlignItems, Display, @@ -19,7 +19,7 @@ import { Text, } from '../../../../components/component-library'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; -import { getPreferences, getShouldShowFiat } from '../../../../selectors'; +import { getShouldShowFiat } from '../../../../selectors'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import LoadingHeartBeat from '../../../../components/ui/loading-heartbeat'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display/user-preferenced-currency-display.component'; @@ -36,8 +36,6 @@ export default function FeeDetailsComponent({ const [expandFeeDetails, setExpandFeeDetails] = useState(false); const shouldShowFiat = useSelector(getShouldShowFiat); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const t = useI18nContext(); const { minimumCostInHexWei: hexMinimumTransactionFee } = useGasFeeContext(); @@ -64,13 +62,13 @@ export default function FeeDetailsComponent({ color: TextColor.textAlternative, variant: TextVariant.bodySmBold, }} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel /> )}
); }, - [txData, useNativeCurrencyAsPrimaryCurrency], + [txData], ); const renderTotalDetailValue = useCallback( @@ -91,13 +89,12 @@ export default function FeeDetailsComponent({ color: TextColor.textAlternative, variant: TextVariant.bodySm, }} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> )} ); }, - [txData, useNativeCurrencyAsPrimaryCurrency], + [txData], ); const hasLayer1GasFee = layer1GasFee !== null; diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.js index 90ccbb09ce23..c861a9dce9d9 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.js @@ -17,7 +17,6 @@ import { import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; import { PriorityLevels } from '../../../../../shared/constants/gas'; import { - getPreferences, getShouldShowFiat, getTxData, transactionFeeSelector, @@ -68,7 +67,6 @@ const GasDetailsItem = ({ supportsEIP1559, } = useGasFeeContext(); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const getTransactionFeeTotal = useMemo(() => { if (layer1GasFee) { return sumHexes(hexMinimumTransactionFee, layer1GasFee); @@ -148,7 +146,7 @@ const GasDetailsItem = ({ }} type={SECONDARY} value={getTransactionFeeTotal} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel // Label not required here as it will always display fiat value. /> )}
@@ -168,7 +166,7 @@ const GasDetailsItem = ({ }} type={PRIMARY} value={getTransactionFeeTotal || draftHexMinimumTransactionFee} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} + // Label required here as it will always display crypto value />
} @@ -216,7 +214,6 @@ const GasDetailsItem = ({ value={ getMaxTransactionFeeTotal || draftHexMaximumTransactionFee } - hideLabel={!useNativeCurrencyAsPrimaryCurrency} />
diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js index ca85dd9abae9..3e45b4c87722 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js @@ -35,9 +35,7 @@ const render = async ({ contextProps } = {}) => { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, gasFeeEstimatesByChainId: { diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js index 30f839b3f78c..9c91ca476ebb 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js @@ -9,10 +9,8 @@ import { } from '../../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, - conversionRateSelector, getCurrentChainId, getCurrentCurrency, - getPreferences, } from '../../../../selectors'; import { formatCurrency } from '../../../../helpers/utils/confirm-tx.util'; import { @@ -38,11 +36,8 @@ const SignatureRequestHeader = ({ txData }) => { const providerConfig = useSelector(getProviderConfig); const networkName = getNetworkNameFromProviderType(providerConfig.type); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const conversionRateFromSelector = useSelector(conversionRateSelector); - const conversionRate = useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateFromSelector; + + const conversionRate = null; // setting conversion rate to null by default to display balance in native const currentNetwork = networkName === '' diff --git a/ui/pages/confirmations/components/signature-request/signature-request.test.js b/ui/pages/confirmations/components/signature-request/signature-request.test.js index 0d50f906e5ca..9851cdbef454 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.test.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.test.js @@ -45,9 +45,7 @@ const mockStore = { rpcUrl: 'http://localhost:8545', ticker: 'ETH', }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, accounts: { '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx index 15bfab8a2428..7c4fdc6e0d22 100644 --- a/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx +++ b/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx @@ -29,7 +29,7 @@ const storeMock = configureStore({ ...mockState.metamask, preferences: { ...mockState.metamask.preferences, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), useTokenDetection: true, diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index cbe80f86fe8b..ebd57c35a141 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -86,7 +86,6 @@ export default class ConfirmApproveContent extends Component { setUserAcknowledgedGasMissing: PropTypes.func, renderSimulationFailureWarning: PropTypes.bool, useCurrencyRateCheck: PropTypes.bool, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, }; state = { @@ -159,7 +158,6 @@ export default class ConfirmApproveContent extends Component { userAcknowledgedGasMissing, renderSimulationFailureWarning, useCurrencyRateCheck, - useNativeCurrencyAsPrimaryCurrency, } = this.props; if ( !hasLayer1GasFee && @@ -183,7 +181,6 @@ export default class ConfirmApproveContent extends Component { } noBold diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 2abec9ef4c13..ba1c7c5a568c 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -11,9 +11,7 @@ const renderComponent = (props) => { const store = configureMockStore([])({ metamask: { ...mockNetworkState({ chainId: '0x0' }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, currencyRates: {}, }, }); diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index 0828c236a38f..a5dcaeb6202d 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -27,7 +27,6 @@ import { getRpcPrefsForCurrentProvider, checkNetworkAndAccountSupports1559, getUseCurrencyRateCheck, - getPreferences, } from '../../../selectors'; import { useApproveTransaction } from '../hooks/useApproveTransaction'; import { useSimulationFailureWarning } from '../hooks/useSimulationFailureWarning'; @@ -84,7 +83,6 @@ export default function ConfirmApprove({ isAddressLedgerByFromAddress(userAddress), ); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const [customPermissionAmount, setCustomPermissionAmount] = useState(''); const [submitWarning, setSubmitWarning] = useState(''); const [isContract, setIsContract] = useState(false); @@ -298,9 +296,6 @@ export default function ConfirmApprove({ hasLayer1GasFee={layer1GasFee !== undefined} supportsEIP1559={supportsEIP1559} useCurrencyRateCheck={useCurrencyRateCheck} - useNativeCurrencyAsPrimaryCurrency={ - useNativeCurrencyAsPrimaryCurrency - } /> {showCustomizeGasPopover && !supportsEIP1559 && (
0.000021 + + ETH +
@@ -431,13 +436,18 @@ exports[`Confirm Transaction Base should match snapshot 1`] = `
0.000021 + + ETH +
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 31149997a39c..96fd5315e317 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 @@ -146,7 +146,6 @@ export default class ConfirmTransactionBase extends Component { secondaryTotalTextOverride: PropTypes.string, gasIsLoading: PropTypes.bool, primaryTotalTextOverrideMaxAmount: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, maxFeePerGas: PropTypes.string, maxPriorityFeePerGas: PropTypes.string, baseFeePerGas: PropTypes.string, @@ -399,7 +398,6 @@ export default class ConfirmTransactionBase extends Component { nextNonce, getNextNonce, txData, - useNativeCurrencyAsPrimaryCurrency, primaryTotalTextOverrideMaxAmount, showLedgerSteps, nativeCurrency, @@ -459,7 +457,6 @@ export default class ConfirmTransactionBase extends Component { type={PRIMARY} key="total-max-amount" value={getTotalAmount(useMaxFee)} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); } @@ -468,9 +465,8 @@ export default class ConfirmTransactionBase extends Component { const primaryTotal = useMaxFee ? primaryTotalTextOverrideMaxAmount : primaryTotalTextOverride; - const totalMaxAmount = useNativeCurrencyAsPrimaryCurrency - ? primaryTotal - : secondaryTotalTextOverride; + + const totalMaxAmount = primaryTotal; return isBoldTextAndNotOverridden ? ( {totalMaxAmount} @@ -500,14 +496,12 @@ export default class ConfirmTransactionBase extends Component { color: TextColor.textDefault, variant: TextVariant.bodyMdBold, }} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel />
); } - return useNativeCurrencyAsPrimaryCurrency - ? secondaryTotalTextOverride - : primaryTotalTextOverride; + return secondaryTotalTextOverride; }; const nextNonceValue = 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 ed012d07e5ce..5d92a1af9c56 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 @@ -45,7 +45,6 @@ import { getIsEthGasPriceFetched, getShouldShowFiat, checkNetworkAndAccountSupports1559, - getPreferences, doesAddressRequireLedgerHidConnection, getTokenList, getEnsResolutionByAddress, @@ -266,7 +265,6 @@ const mapStateToProps = (state, ownProps) => { customNonceValue = getCustomNonceValue(state); const isEthGasPriceFetched = getIsEthGasPriceFetched(state); const noGasPrice = !supportsEIP1559 && getNoGasPriceFetched(state); - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); const gasFeeIsCustom = fullTxData.userFeeLevel === CUSTOM_GAS_ESTIMATE || txParamsAreDappSuggested(fullTxData); @@ -347,7 +345,6 @@ const mapStateToProps = (state, ownProps) => { noGasPrice, supportsEIP1559, gasIsLoading: isGasEstimatesLoading || gasLoadingAnimationIsShowing, - useNativeCurrencyAsPrimaryCurrency, maxFeePerGas: gasEstimationObject.maxFeePerGas, maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas, baseFeePerGas: gasEstimationObject.baseFeePerGas, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index f8a7b40430fb..bea6aef1d84d 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -108,9 +108,7 @@ const baseStore = { chainId: CHAIN_IDS.GOERLI, }), tokens: [], - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, currentCurrency: 'USD', currencyRates: {}, featureFlags: { diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js index 8af6bcf3ba40..908f600564f8 100644 --- a/ui/pages/confirmations/hooks/test-utils.js +++ b/ui/pages/confirmations/hooks/test-utils.js @@ -10,10 +10,10 @@ import { import { getCurrentCurrency, getShouldShowFiat, - getPreferences, txDataSelector, getCurrentKeyring, getTokenExchangeRates, + getPreferences, } from '../../../selectors'; import { @@ -118,7 +118,7 @@ export const generateUseSelectorRouter = } if (selector === getPreferences) { return { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }; } if ( diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 5fbad8445cd6..33a011c2966a 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -48,6 +48,7 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics'; import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; +// This function is no longer used in codebase, to be deleted. export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); const dispatch = useDispatch(); @@ -61,8 +62,7 @@ export default function GasDisplay({ gasError }) { const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { showFiatInTestnets, useNativeCurrencyAsPrimaryCurrency } = - useSelector(getPreferences); + const { showFiatInTestnets } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); const nativeCurrency = useSelector(getNativeCurrency); const { chainId } = providerConfig; @@ -132,7 +132,6 @@ export default function GasDisplay({ gasError }) { type={PRIMARY} key="total-detail-value" value={hexTransactionTotal} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); @@ -144,10 +143,9 @@ export default function GasDisplay({ gasError }) { draftTransaction.amount.value, hexMaximumTransactionFee, )} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); - } else if (useNativeCurrencyAsPrimaryCurrency) { + } else { detailTotal = primaryTotalTextOverrideMaxAmount; maxAmount = primaryTotalTextOverrideMaxAmount; } @@ -177,7 +175,7 @@ export default function GasDisplay({ gasError }) { type={SECONDARY} key="total-detail-text" value={hexTransactionTotal} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel /> ) diff --git a/ui/pages/home/index.scss b/ui/pages/home/index.scss index 03b4cd5d7cf9..5a85a3eb5d3c 100644 --- a/ui/pages/home/index.scss +++ b/ui/pages/home/index.scss @@ -20,6 +20,7 @@ min-width: 0; display: flex; flex-direction: column; + padding-top: 8px; } &__connect-status-text { diff --git a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx index cdefb3986d1f..d7a474ad3b24 100644 --- a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx +++ b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx @@ -17,9 +17,7 @@ jest.mock('../../../store/institutional/institution-background', () => ({ describe('Confirm Add Custodian Token', () => { const mockStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, institutionalFeatures: { connectRequests: [ { @@ -50,9 +48,7 @@ describe('Confirm Add Custodian Token', () => { it('tries to connect to custodian with empty token', async () => { const customMockedStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, institutionalFeatures: { connectRequests: [ { diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx index 5719fb38015f..5044d6085812 100644 --- a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx @@ -9,9 +9,7 @@ describe('Confirm Add Custodian Token', () => { const mockStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, }, history: { push: '/', diff --git a/ui/pages/institutional/custody/custody.test.tsx b/ui/pages/institutional/custody/custody.test.tsx index 383e615492da..577e599397ba 100644 --- a/ui/pages/institutional/custody/custody.test.tsx +++ b/ui/pages/institutional/custody/custody.test.tsx @@ -99,9 +99,7 @@ describe('CustodyPage', function () { }, ], }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, appState: { isLoading: false, }, diff --git a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap index fcc11ec8336b..6318abd37570 100644 --- a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap +++ b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap @@ -501,7 +501,7 @@ exports[`AdvancedTab Component should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
{ return ( - + ); }; @@ -73,7 +79,11 @@ export default function SettingsSearch({ onClick={() => handleSearch('')} style={{ cursor: 'pointer' }} > - + )} @@ -93,6 +103,7 @@ export default function SettingsSearch({ autoComplete="off" startAdornment={renderStartAdornment()} endAdornment={renderEndAdornment()} + theme="bordered" /> ); } diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js index b998cde80515..191bbbc78685 100644 --- a/ui/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/pages/settings/settings-tab/settings-tab.component.js @@ -62,12 +62,10 @@ export default class SettingsTab extends PureComponent { currentLocale: PropTypes.string, useBlockie: PropTypes.bool, currentCurrency: PropTypes.string, - nativeCurrency: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, - setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, + showNativeTokenAsMainBalance: PropTypes.bool, + setShowNativeTokenAsMainBalancePreference: PropTypes.func, hideZeroBalanceTokens: PropTypes.bool, setHideZeroBalanceTokens: PropTypes.func, - lastFetchedConversionDate: PropTypes.number, selectedAddress: PropTypes.string, tokenList: PropTypes.object, theme: PropTypes.string, @@ -94,8 +92,7 @@ export default class SettingsTab extends PureComponent { renderCurrentConversion() { const { t } = this.context; - const { currentCurrency, setCurrentCurrency, lastFetchedConversionDate } = - this.props; + const { currentCurrency, setCurrentCurrency } = this.props; return (
- {t('currencyConversion')} - - {lastFetchedConversionDate - ? t('updatedWithDate', [ - new Date(lastFetchedConversionDate * 1000).toString(), - ]) - : t('noConversionDateAvailable')} - + + {t('currencyConversion')} +
@@ -131,6 +127,7 @@ export default class SettingsTab extends PureComponent { }, }); }} + className="settings-page__content-item__dropdown" />
@@ -141,10 +138,6 @@ export default class SettingsTab extends PureComponent { renderCurrentLocale() { const { t } = this.context; const { updateCurrentLocale, currentLocale } = this.props; - const currentLocaleMeta = locales.find( - (locale) => locale.code === currentLocale, - ); - const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''; return (
- + {t('currentLanguage')} - - - {currentLocaleName} - +
@@ -191,15 +185,20 @@ export default class SettingsTab extends PureComponent { id="toggle-zero-balance" >
- {t('hideZeroBalanceTokens')} + + {t('hideZeroBalanceTokens')} +
setHideZeroBalanceTokens(!value)} - offLabel={t('off')} - onLabel={t('on')} + data-testid="toggle-zero-balance-button" />
@@ -229,14 +228,19 @@ export default class SettingsTab extends PureComponent {
{t('accountIdenticon')} - + {t('jazzAndBlockies')} - +
+
+
+
+ +
+
+`; diff --git a/ui/components/ui/survey-toast/index.ts b/ui/components/ui/survey-toast/index.ts new file mode 100644 index 000000000000..8926c3e7dfd9 --- /dev/null +++ b/ui/components/ui/survey-toast/index.ts @@ -0,0 +1 @@ +export { SurveyToast } from './survey-toast'; diff --git a/ui/components/ui/survey-toast/survey-toast.test.tsx b/ui/components/ui/survey-toast/survey-toast.test.tsx new file mode 100644 index 000000000000..c4b90ec27a5e --- /dev/null +++ b/ui/components/ui/survey-toast/survey-toast.test.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { act } from 'react-dom/test-utils'; +import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { SurveyToast } from './survey-toast'; + +jest.mock('../../../../shared/lib/fetch-with-cache', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const mockFetchWithCache = fetchWithCache as jest.Mock; +const mockTrackEvent = jest.fn(); +const mockStore = configureStore([thunk]); + +const surveyData = { + valid: { + url: 'https://example.com', + description: 'Test Survey', + cta: 'Take Survey', + id: 3, + }, + stale: { + url: 'https://example.com', + description: 'Test Survey', + cta: 'Take Survey', + id: 1, + }, +}; + +const createStore = (options = { metametricsEnabled: true }) => + mockStore({ + user: { basicFunctionality: true }, + metamask: { + lastViewedUserSurvey: 2, + useExternalServices: true, + participateInMetaMetrics: options.metametricsEnabled, + metaMetricsId: '0x123', + internalAccounts: { + selectedAccount: '0x123', + accounts: { '0x123': { address: '0x123' } }, + }, + }, + }); + +const renderComponent = (options = { metametricsEnabled: true }) => + renderWithProvider( + + + , + createStore(options), + ); + +describe('SurveyToast', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + + global.platform = { + openTab: jest.fn(), + closeCurrentWindow: jest.fn(), + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it('should match snapshot', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + let container; + await act(async () => { + const result = renderComponent(); + container = result.container; + }); + + expect(container).toMatchSnapshot(); + }); + + it('renders nothing if no survey is available', () => { + mockFetchWithCache.mockResolvedValue({ surveys: [] }); + renderComponent(); + expect(screen.queryByTestId('survey-toast')).toBeNull(); + }); + + it('renders nothing if the survey is stale', () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.stale }); + renderComponent(); + expect(screen.queryByTestId('survey-toast')).toBeNull(); + }); + + it('renders the survey toast when a valid survey is available', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + await act(async () => { + renderComponent(); + }); + + await waitFor(() => { + expect(screen.getByTestId('survey-toast')).toBeInTheDocument(); + expect( + screen.getByText(surveyData.valid.description), + ).toBeInTheDocument(); + expect(screen.getByText(surveyData.valid.cta)).toBeInTheDocument(); + }); + }); + + it('handles action click correctly when metametrics is enabled', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('survey-toast')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText(surveyData.valid.cta)); + + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: surveyData.valid.url, + }); + expect(mockTrackEvent).toHaveBeenCalledWith({ + event: MetaMetricsEventName.SurveyToast, + category: MetaMetricsEventCategory.Feedback, + properties: { + response: 'accept', + survey: surveyData.valid.id, + }, + }); + }); + + it('should not show the toast if metametrics is disabled', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + renderComponent({ + metametricsEnabled: false, + }); + + await waitFor(() => { + expect(screen.queryByTestId('survey-toast')).toBeNull(); + }); + }); +}); diff --git a/ui/components/ui/survey-toast/survey-toast.tsx b/ui/components/ui/survey-toast/survey-toast.tsx new file mode 100644 index 000000000000..ff485b2b73e3 --- /dev/null +++ b/ui/components/ui/survey-toast/survey-toast.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useState, useContext, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; +import { DAY } from '../../../../shared/constants/time'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { + getSelectedInternalAccount, + getLastViewedUserSurvey, + getUseExternalServices, + getMetaMetricsId, + getParticipateInMetaMetrics, +} from '../../../selectors'; +import { ACCOUNTS_API_BASE_URL } from '../../../../shared/constants/accounts'; +import { setLastViewedUserSurvey } from '../../../store/actions'; +import { Toast } from '../../multichain'; + +type Survey = { + url: string; + description: string; + cta: string; + id: number; +}; + +export function SurveyToast() { + const [survey, setSurvey] = useState(null); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + const lastViewedUserSurvey = useSelector(getLastViewedUserSurvey); + const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics); + const basicFunctionality = useSelector(getUseExternalServices); + const internalAccount = useSelector(getSelectedInternalAccount); + const metaMetricsId = useSelector(getMetaMetricsId); + + const surveyUrl = useMemo( + () => `${ACCOUNTS_API_BASE_URL}/v1/users/${metaMetricsId}/surveys`, + [metaMetricsId], + ); + + useEffect(() => { + if (!basicFunctionality || !metaMetricsId || !participateInMetaMetrics) { + return undefined; + } + + const controller = new AbortController(); + + const fetchSurvey = async () => { + try { + const response = await fetchWithCache({ + url: surveyUrl, + fetchOptions: { + method: 'GET', + headers: { + 'x-metamask-clientproduct': 'metamask-extension', + }, + signal: controller.signal, + }, + functionName: 'fetchSurveys', + cacheOptions: { cacheRefreshTime: process.env.IN_TEST ? 0 : DAY * 7 }, + }); + + const _survey: Survey = response?.surveys; + + if ( + !_survey || + Object.keys(_survey).length === 0 || + _survey.id <= lastViewedUserSurvey + ) { + return; + } + + setSurvey(_survey); + } catch (error: unknown) { + if (error instanceof Error && error.name !== 'AbortError') { + console.error('Failed to fetch survey:', metaMetricsId, error); + } + } + }; + + fetchSurvey(); + + return () => { + controller.abort(); + }; + }, [ + internalAccount?.address, + lastViewedUserSurvey, + basicFunctionality, + metaMetricsId, + dispatch, + ]); + + function handleActionClick() { + if (!survey) { + return; + } + global.platform.openTab({ + url: survey.url, + }); + dispatch(setLastViewedUserSurvey(survey.id)); + trackAction('accept'); + } + + function handleClose() { + if (!survey) { + return; + } + dispatch(setLastViewedUserSurvey(survey.id)); + trackAction('deny'); + } + + function trackAction(response: 'accept' | 'deny') { + if (!participateInMetaMetrics || !survey) { + return; + } + + trackEvent({ + event: MetaMetricsEventName.SurveyToast, + category: MetaMetricsEventCategory.Feedback, + properties: { + response, + survey: survey.id, + }, + }); + } + + if (!survey || survey.id <= lastViewedUserSurvey) { + return null; + } + + return ( + + ); +} diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 82361cb6b690..1fdbad27ed67 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -38,6 +38,7 @@ import { ToastContainer, Toast, } from '../../components/multichain'; +import { SurveyToast } from '../../components/ui/survey-toast'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; @@ -676,6 +677,7 @@ export default class Routes extends Component { return ( + {showConnectAccountToast && !this.state.hideConnectAccountToast && isEvmAccount ? ( diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index e0c54e932703..fac2f9f52c31 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1969,6 +1969,10 @@ export function getShowPrivacyPolicyToast(state) { ); } +export function getLastViewedUserSurvey(state) { + return state.metamask.lastViewedUserSurvey; +} + export function getShowOutdatedBrowserWarning(state) { const { outdatedBrowserWarningLastShown } = state.metamask; if (!outdatedBrowserWarningLastShown) { diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 615035b58add..e64d366a7c74 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4257,6 +4257,12 @@ export function setNewPrivacyPolicyToastClickedOrClosed() { }; } +export function setLastViewedUserSurvey(id: number) { + return async () => { + await submitRequestToBackground('setLastViewedUserSurvey', [id]); + }; +} + export function setOnboardingDate() { return async () => { await submitRequestToBackground('setOnboardingDate'); From 537b3fe32b8af594359e6c9180b9889288173e99 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 3 Oct 2024 15:04:59 +0100 Subject: [PATCH 057/226] fix: Max approval and array value spending cap bugs (#27573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes two bugs. The first happens when the user approves a token for the max amount. This value is decoded by sourcify and is tipically expressed in scientific notation. However, this number has more than 15 significant digits. The fix is to coerce the number to a string (also accepted by bignumber js), forcing it to be expressed with 15 significant digits only. Screenshot 2024-10-02 at 17 26 55 The second bug happens because sometimes the decoded data from sourcify expresses a value of a param as an array of elements. An exception was added to the param lookup, so that we don't try to use an array value as the spending cap to be displayed. Screenshot 2024-10-02 at 17 13 34 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27573?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27535 ## **Manual testing steps** ### First bug 1. Go to Uniswap 2. Select a token that hasn't been approved yet, and click to approve 3. The app shouldn't crash ### Second bug 1. Go to https://ethereumfilm.xyz/ethereum-stories 2. Click mint on one of the posters or mini-movies 3. The app shouldn't crash ## **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. --- .../confirm/info/approve/hooks/use-approve-token-simulation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts index e05823738b5c..19f26c9c9300 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts @@ -33,13 +33,14 @@ export const useApproveTokenSimulation = ( (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) + return new BigNumber(value.data[0].params[paramIndex].value.toString()) .dividedBy(new BigNumber(10).pow(Number(decimals))) .toNumber(); }, [value, decimals]); From c805f759b15fe1fe84f48e544720825a016009cc Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:38:44 +0200 Subject: [PATCH 058/226] chore: fix deps audit (#27620) 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/27620?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. --------- Co-authored-by: MetaMask Bot --- lavamoat/browserify/beta/policy.json | 5 +- lavamoat/browserify/flask/policy.json | 5 +- lavamoat/browserify/main/policy.json | 5 +- lavamoat/browserify/mmi/policy.json | 5 +- yarn.lock | 117 ++++++++++++-------------- 5 files changed, 67 insertions(+), 70 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 380e38cd7a63..a2d6685880fb 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2946,7 +2946,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3043,7 +3043,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 380e38cd7a63..a2d6685880fb 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2946,7 +2946,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3043,7 +3043,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 380e38cd7a63..a2d6685880fb 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2946,7 +2946,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3043,7 +3043,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 576041b08cfb..28828b13e737 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -3038,7 +3038,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3135,7 +3135,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, diff --git a/yarn.lock b/yarn.lock index cdde2e6fce0c..50915b343811 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7896,64 +7896,64 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/browser-utils@npm:8.19.0" +"@sentry-internal/browser-utils@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/browser-utils@npm:8.33.1" dependencies: - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/d6df6cb6edc6b2ddb7362daee39770a51b255d343b3dcb80dc98f77dc43a7cc66f29076e14d1a0ac162a51a4f620b876493a04c23a530f57170009364b6464ea + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/aed6ec58a2dea3613011c24c1e1f14899eaba721d4523ca7da281cbf70e1d48e5ab2bd50da17de76e8cc8052b983840d937e167ea980c6a07e4d32f0e374903c languageName: node linkType: hard -"@sentry-internal/feedback@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/feedback@npm:8.19.0" +"@sentry-internal/feedback@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/feedback@npm:8.33.1" dependencies: - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/e10cf1f63d49a41072aaa1b7b007241a273bd4bfa6d2c628e50d621c8cde836e6743bdefbf9ba7e96684b6dd18ad49e17841f4420fc33757e7c119ec88b4ac15 + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/2cb3f4c4b71f8cdf8bcab9251216b15e0caaae257bbce49fffcf053716fab60d61793898c221457e518b109e6319faf8190c2d0e57fcea8b91f28e5815f4e643 languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/replay-canvas@npm:8.19.0" +"@sentry-internal/replay-canvas@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/replay-canvas@npm:8.33.1" dependencies: - "@sentry-internal/replay": "npm:8.19.0" - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/1f379c141884b448c56fcd663b8acc0ff1c12d50a2b9db37f9552eb2bc8c99a970114f80e58c8c4fcd61f933f9a15f58dc6cbe6f4297bb574d6772be8f41c5bf + "@sentry-internal/replay": "npm:8.33.1" + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/75432f627a73bad2e09ad2a7b7200c1ea4fe9d9e797458615850689dd7b017f38c876f4435ea548da9ae7653f55be90d58fc115897febacc53b69e6593867afb languageName: node linkType: hard -"@sentry-internal/replay@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/replay@npm:8.19.0" +"@sentry-internal/replay@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/replay@npm:8.33.1" dependencies: - "@sentry-internal/browser-utils": "npm:8.19.0" - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/dc9bef6997d1f40fb0402f52c9d14f72cf050ec140fda27e00057c59ddd1a6144e78e40aeb5e0223dd48651bf02f809db26cf6e866dd5c8ec5c6bbbf76c6f1aa + "@sentry-internal/browser-utils": "npm:8.33.1" + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/05cdb361ccde5039c7353877a95eb15e4d630d5edbb874cd55ac190ee8256a1456e1c6cae37636df55bff10fcde6ff1232d8ca290467d43393bb18d9e4efe99f languageName: node linkType: hard "@sentry/browser@npm:^8.19.0": - version: 8.19.0 - resolution: "@sentry/browser@npm:8.19.0" + version: 8.33.1 + resolution: "@sentry/browser@npm:8.33.1" dependencies: - "@sentry-internal/browser-utils": "npm:8.19.0" - "@sentry-internal/feedback": "npm:8.19.0" - "@sentry-internal/replay": "npm:8.19.0" - "@sentry-internal/replay-canvas": "npm:8.19.0" - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/2412e938454bd5cc505bbbe7092a17bf5fde4b222ecfedaf3d54fb963a6c875c78661921d8f6e998498c85a9a52e616db75fd706867f76d38bf3f95714775aa6 + "@sentry-internal/browser-utils": "npm:8.33.1" + "@sentry-internal/feedback": "npm:8.33.1" + "@sentry-internal/replay": "npm:8.33.1" + "@sentry-internal/replay-canvas": "npm:8.33.1" + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/085717b19c89184fad0c9e17dee679401ff87616678f952d91afff574ebcc56114845c216bbbd7b81c93d54c2a42b3db4232af1c707843424cdd6800a99030a5 languageName: node linkType: hard @@ -7972,36 +7972,29 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry/core@npm:8.19.0" +"@sentry/core@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry/core@npm:8.33.1" dependencies: - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/708ef5abd81a9ab5288a4b258411e78591a7fec4854fc582c34f087fce62f5cd74e1086fbbc27a9f55da77d113dde137fbf9649f5b7df3d1a22886850702adbd + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/dbd781777f5dc003e21680919d37e308a64320776c54a5712163f72d4c0c4d5d25d7f07b83123e517c333fcdefb92ac5a0f15cb4dbbc79f3cc7309038cb0fcbb languageName: node linkType: hard -"@sentry/types@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry/types@npm:8.19.0" - checksum: 10/8812f7394c6c031197abc04d80e5b5b3693742dc065b877c535a9ceb538aabd60ee27fc2b13824e2b8fc264819868109bbd4de3642fd1c7bf30d304fb0c21aa9 +"@sentry/types@npm:8.33.1, @sentry/types@npm:^8.19.0": + version: 8.33.1 + resolution: "@sentry/types@npm:8.33.1" + checksum: 10/bcd7f80e84a23cb810fa5819dc85f45bd62d52b01b1f64a1b31297df21e9d1f4de8f7ea91835c5d6a7010d7dbfc8b09cd708d057d345a6ff685b7f12db41ae57 languageName: node linkType: hard -"@sentry/types@npm:^8.19.0": - version: 8.20.0 - resolution: "@sentry/types@npm:8.20.0" - checksum: 10/c7d7ed17975f0fc0b4bf5aece58084953c2a76e8f417923a476fe1fd42a2c9339c548d701edbc4b938c9252cf680d3eff4c6c2a986bc7ac62649aebf656c5b64 - languageName: node - linkType: hard - -"@sentry/utils@npm:8.19.0, @sentry/utils@npm:^8.19.0": - version: 8.19.0 - resolution: "@sentry/utils@npm:8.19.0" +"@sentry/utils@npm:8.33.1, @sentry/utils@npm:^8.19.0": + version: 8.33.1 + resolution: "@sentry/utils@npm:8.33.1" dependencies: - "@sentry/types": "npm:8.19.0" - checksum: 10/abd507e5b37c7753534865f74a1a622fdbe2d71cfa61fd009703f4c9c90634fb6d26e3b2f8e09904631d4692e3735de451ed914c505c31700a6f5504a61e649e + "@sentry/types": "npm:8.33.1" + checksum: 10/79426deba11c043f0410b4b5d635367147d7e41bb90526168f180ae05598768348de39a82f89a92a4f0365f5ece5f62950ba6eab0b7300faefea7a9bb0889df3 languageName: node linkType: hard From 9f68d101ac3f2fe84f63793182409e0da1ddaa95 Mon Sep 17 00:00:00 2001 From: Harika <153644847+hjetpoluru@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:15:48 -0400 Subject: [PATCH 059/226] fix: "Dapp viewed Event @no-mmi is sent when refreshing da..." flaky test (#27381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In this test, there are two different requests for the "Dapp Viewed" event: one with the property is_first_visit: true and the other with is_first_visit: false. However, the current mock setup does not differentiate between these two requests. To ensure that both "Dapp Viewed" event requests are properly handled, we need to create two separate mocks has been created. Special thanks to @seaona for her thorough analysis and understanding of this tricky flaky test. Her insights and proposed solution were instrumental in resolving this issue. All credit for identifying and addressing this problem goes to her. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27381?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/24655 https://github.com/MetaMask/metamask-extension/issues/24651 https://github.com/MetaMask/metamask-extension/issues/26899 ## **Manual testing steps** Run the dapp viewed spec locally or in codespace using below commands against chrome browser: yarn yarn build:test:webpack ENABLE_MV3=false yarn test:e2e:single test/e2e/tests/metrics/dapp-viewed.spec.js --browser=chrome ## **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). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/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/metrics/dapp-viewed.spec.js | 62 ++++++++++++++++------ 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/test/e2e/tests/metrics/dapp-viewed.spec.js b/test/e2e/tests/metrics/dapp-viewed.spec.js index b9b4b08ca73e..78214685777e 100644 --- a/test/e2e/tests/metrics/dapp-viewed.spec.js +++ b/test/e2e/tests/metrics/dapp-viewed.spec.js @@ -14,11 +14,40 @@ const { MetaMetricsEventName, } = require('../../../../shared/constants/metametrics'); -async function mockedDappViewedEndpoint(mockServer) { +async function mockedDappViewedEndpointFirstVisit(mockServer) { return await mockServer .forPost('https://api.segment.io/v1/batch') .withJsonBodyIncluding({ - batch: [{ type: 'track', event: MetaMetricsEventName.DappViewed }], + batch: [ + { + type: 'track', + event: MetaMetricsEventName.DappViewed, + properties: { + is_first_visit: true, + }, + }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }); +} + +async function mockedDappViewedEndpointReVisit(mockServer) { + return await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { + type: 'track', + event: MetaMetricsEventName.DappViewed, + properties: { + is_first_visit: false, + }, + }, + ], }) .thenCallback(() => { return { @@ -67,7 +96,7 @@ describe('Dapp viewed Event @no-mmi', function () { const validFakeMetricsId = 'fake-metrics-fd20'; it('is not sent when metametrics ID is not valid', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( @@ -93,7 +122,7 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when navigating to dapp with no account connected', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( @@ -125,8 +154,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when opening the dapp in a new tab with one account connected', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -163,8 +192,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when refreshing dapp with one account connected', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -189,10 +218,9 @@ describe('Dapp viewed Event @no-mmi', function () { // refresh dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.refresh(); - const events = await getEventPayloads(driver, mockedEndpoints); - // events are original dapp viewed, new dapp viewed when refresh, and permission approved + // events are original dapp viewed, navigate to dapp, new dapp viewed when refresh, new dapp viewed when navigate and permission approved const dappViewedEventProperties = events[1].properties; assert.equal(dappViewedEventProperties.is_first_visit, false); assert.equal(dappViewedEventProperties.number_of_accounts, 1); @@ -204,10 +232,10 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when navigating to a connected dapp', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -247,7 +275,7 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when connecting dapp with two accounts', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( { @@ -299,8 +327,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when reconnect to a dapp that has been connected before', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), ]; } From ab50595ff6865458dc924f2d5cdd738c6e6f68fe Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:19:14 +0100 Subject: [PATCH 060/226] fix: add amount row for contract deployment (#27594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds an "Amount" row to display the token value for contract deployments when either: - The transaction parameters include a value greater than 0, or - The simulation fails. This change ensures that users can see the token amount being sent during contract deployments, even when the "Estimate balance changes" feature is disabled. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27594?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27525 ## **Manual testing steps** 1. Go to Remix 2. Trigger a contract deployment with some ETH 3. See no balance appears in the amount 4. Disable Transaction redesign 5. Trigger a contract deployment with some ETH 6. See balance appears in the amount Example Contract ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Params { constructor() payable {} receive() external payable { } } ``` ## **Screenshots/Recordings** ### **Before** [amount-contract.webm](https://github.com/user-attachments/assets/131f36f6-73f6-465e-b731-bb10554d5f76) ### **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. --- .../confirmations/contract-interaction.ts | 18 ++++++++++-- .../transaction-details.test.tsx | 22 ++++++++++++++ .../transaction-details.tsx | 29 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 507a27a48dc3..49a6e1aad1ab 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -1,4 +1,6 @@ import { + SimulationData, + TransactionMeta, TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; @@ -22,12 +24,14 @@ export const genUnapprovedContractInteractionConfirmation = ({ address = CONTRACT_INTERACTION_SENDER_ADDRESS, txData = DEPOSIT_METHOD_DATA, chainId = CHAIN_ID, + simulationData, }: { address?: Hex; txData?: Hex; chainId?: string; -} = {}): Confirmation => - ({ + simulationData?: SimulationData; +} = {}): Confirmation => { + const confirmation: Confirmation = { actionId: String(400855682), chainId, dappSuggestedGasFees: { @@ -160,4 +164,12 @@ export const genUnapprovedContractInteractionConfirmation = ({ userEditedGasLimit: false, userFeeLevel: 'medium', verifiedOnBlockchain: false, - } as SignatureRequestType); + } as SignatureRequestType; + + // Overwrite simulation data if provided + if (simulationData) { + (confirmation as TransactionMeta).simulationData = simulationData; + } + + return confirmation; +}; diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx index 34da84540e9b..1263acf08397 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx @@ -1,11 +1,15 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { SimulationErrorCode } from '@metamask/transaction-controller'; import { getMockConfirmState, + getMockConfirmStateForTransaction, getMockContractInteractionConfirmState, } from '../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import { CHAIN_IDS } from '../../../../../../../../shared/constants/network'; +import { genUnapprovedContractInteractionConfirmation } from '../../../../../../../../test/data/confirmations/contract-interaction'; import { TransactionDetails } from './transaction-details'; jest.mock( @@ -39,4 +43,22 @@ describe('', () => { ); expect(container).toMatchSnapshot(); }); + + it('renders component for transaction details with amount', () => { + const simulationDataMock = { + error: { code: SimulationErrorCode.Disabled }, + tokenBalanceChanges: [], + }; + const contractInteraction = genUnapprovedContractInteractionConfirmation({ + simulationData: simulationDataMock, + chainId: CHAIN_IDS.GOERLI, + }); + const state = getMockConfirmStateForTransaction(contractInteraction); + const mockStore = configureMockStore(middleware)(state); + const { getByTestId } = renderWithConfirmContextProvider( + , + mockStore, + ); + expect(getByTestId('transaction-details-amount-row')).toBeInTheDocument(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx index e53387af325a..706729a8cc0a 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx @@ -16,6 +16,10 @@ import { selectPaymasterAddress } from '../../../../../../../selectors/account-a import { selectConfirmationAdvancedDetailsOpen } from '../../../../../selectors/preferences'; import { useConfirmContext } from '../../../../../context/confirm'; import { useFourByte } from '../../hooks/useFourByte'; +import { ConfirmInfoRowCurrency } from '../../../../../../../components/app/confirm/info/row/currency'; +import { PRIMARY } from '../../../../../../../helpers/constants/common'; +import { useUserPreferencedCurrency } from '../../../../../../../hooks/useUserPreferencedCurrency'; +import { HEX_ZERO } from '../constants'; export const OriginRow = () => { const t = useI18nContext(); @@ -83,6 +87,30 @@ export const MethodDataRow = () => { ); }; +const AmountRow = () => { + const t = useI18nContext(); + const { currentConfirmation } = useConfirmContext(); + const { currency } = useUserPreferencedCurrency(PRIMARY); + + const value = currentConfirmation?.txParams?.value; + const simulationData = currentConfirmation?.simulationData; + + if (!value || value === HEX_ZERO || !simulationData?.error) { + return null; + } + + return ( + + + + + + ); +}; + const PaymasterRow = () => { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); @@ -124,6 +152,7 @@ export const TransactionDetails = () => { {showAdvancedDetails && } + ); From a9667f8b9ccc86d3dcb2b2c24a2b16c5460ad6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Tavares?= Date: Fri, 4 Oct 2024 14:25:32 +0100 Subject: [PATCH 061/226] fix: revert jest collect coverage patterns (#27583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Reverts the coverage patterns for unit tests. When we started to collect coverage from the types dir, we started to get the following error from babel parser: ``` ERROR: /home/runner/work/metamask-extension/metamask-extension/types/global.d.ts: 'export declare' must be followed by an ambient declaration. ``` Removing the types dir from the coverage paths would be enough to fix it, but it was suggested by the Platform team (@itsyoboieltr) to revert completely the changes added on this [PR](https://github.com/MetaMask/metamask-extension/pull/27282/files). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27583?quickstart=1) ## **Related issues** Fixes: 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** - [ ] 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: legobeat <109787230+legobeat@users.noreply.github.com> --- jest.config.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/jest.config.js b/jest.config.js index be304e027ace..dbfb0522cff7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,10 @@ module.exports = { collectCoverageFrom: [ - '/app/**/*.(js|ts|tsx)', - '/development/**/*.(js|ts|tsx)', - '/offscreen/**/*.(js|ts|tsx)', + '/app/scripts/**/*.(js|ts|tsx)', '/shared/**/*.(js|ts|tsx)', - '/test/**/*.(js|ts|tsx)', - '/types/**/*.(js|ts|tsx)', '/ui/**/*.(js|ts|tsx)', + '/development/build/transforms/**/*.js', + '/test/unit-global/**/*.test.(js|ts|tsx)', ], coverageDirectory: './coverage/unit', coveragePathIgnorePatterns: ['.stories.*', '.snap'], From 3bdb7ebcce4e02bc02e6d9418a6900ffcd38d901 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 4 Oct 2024 15:29:24 +0100 Subject: [PATCH 062/226] fix: disable transaction data decode if deployment (#27586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Disable transaction data decoding when deploying a smart contract. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27586?quickstart=1) ## **Related issues** Fixes: #27524 ## **Manual testing steps** See issue. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../hooks/useDecodedTransactionData.test.ts | 17 +++++++++++ .../info/hooks/useDecodedTransactionData.ts | 5 ++-- .../confirm/info/hooks/useFourByte.test.ts | 29 +++++++++++++++++-- .../confirm/info/hooks/useFourByte.ts | 19 ++++++++++-- .../transaction-data.test.tsx | 1 + 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts index 35f2f42c4792..32a711abf754 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts @@ -59,6 +59,23 @@ describe('useDecodedTransactionData', () => { }, ); + it('returns undefined if no transaction to', async () => { + const result = await runHook( + getMockConfirmStateForTransaction({ + id: '123', + chainId: CHAIN_ID_MOCK, + type: TransactionType.contractInteraction, + status: TransactionStatus.unapproved, + txParams: { + data: TRANSACTION_DATA_UNISWAP, + to: undefined, + } as TransactionParams, + }), + ); + + expect(result).toStrictEqual({ pending: false, value: undefined }); + }); + it('returns the decoded data', async () => { decodeTransactionDataMock.mockResolvedValue(TRANSACTION_DECODE_SOURCIFY); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts index b2d69df413d4..6934f893378d 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts @@ -18,9 +18,10 @@ export function useDecodedTransactionData(): AsyncResult< 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)) { + if (!hasTransactionData(transactionData) || !transactionTo) { return undefined; } @@ -29,5 +30,5 @@ export function useDecodedTransactionData(): AsyncResult< chainId, contractAddress, }); - }, [transactionData, chainId, contractAddress]); + }, [transactionData, transactionTo, chainId, contractAddress]); } diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts index 5d4a023c82bb..9f1a848a96cd 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts @@ -34,7 +34,7 @@ describe('useFourByte', () => { expect(result.current.params).toEqual([]); }); - it('returns empty object if resolution is turned off', () => { + it('returns null if resolution disabled', () => { const currentConfirmation = genUnapprovedContractInteractionConfirmation({ address: CONTRACT_INTERACTION_SENDER_ADDRESS, txData: depositHexData, @@ -57,7 +57,7 @@ describe('useFourByte', () => { expect(result.current).toBeNull(); }); - it("returns undefined if it's not known even if resolution is enabled", () => { + it('returns null if not known even if resolution enabled', () => { const currentConfirmation = genUnapprovedContractInteractionConfirmation({ address: CONTRACT_INTERACTION_SENDER_ADDRESS, txData: depositHexData, @@ -77,4 +77,29 @@ describe('useFourByte', () => { expect(result.current).toBeNull(); }); + + it('returns null if no transaction to', () => { + const currentConfirmation = genUnapprovedContractInteractionConfirmation({ + address: CONTRACT_INTERACTION_SENDER_ADDRESS, + txData: depositHexData, + }) as TransactionMeta; + + currentConfirmation.txParams.to = undefined; + + const { result } = renderHookWithProvider( + () => useFourByte(currentConfirmation), + { + ...mockState, + metamask: { + ...mockState.metamask, + use4ByteResolution: true, + knownMethodData: { + [depositHexData]: { name: 'Deposit', params: [] }, + }, + }, + }, + ); + + expect(result.current).toBeNull(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts index 7e2b81b443bd..7fece13fb417 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts @@ -1,28 +1,41 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import { useDispatch, useSelector } from 'react-redux'; import { useEffect } from 'react'; +import { Hex } from '@metamask/utils'; import { getKnownMethodData, use4ByteResolutionSelector, } from '../../../../../../selectors'; import { getContractMethodData } from '../../../../../../store/actions'; +import { hasTransactionData } from '../../../../../../../shared/modules/transaction.utils'; export const useFourByte = (currentConfirmation: TransactionMeta) => { const dispatch = useDispatch(); const isFourByteEnabled = useSelector(use4ByteResolutionSelector); - const transactionData = currentConfirmation?.txParams?.data; + const transactionTo = currentConfirmation?.txParams?.to; + const transactionData = currentConfirmation?.txParams?.data as + | Hex + | undefined; useEffect(() => { - if (!isFourByteEnabled || !transactionData) { + if ( + !isFourByteEnabled || + !hasTransactionData(transactionData) || + !transactionTo + ) { return; } dispatch(getContractMethodData(transactionData)); - }, [isFourByteEnabled, transactionData, dispatch]); + }, [isFourByteEnabled, transactionData, transactionTo, dispatch]); const methodData = useSelector((state) => getKnownMethodData(state, transactionData), ); + if (!transactionTo) { + return null; + } + return methodData; }; diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.test.tsx index 5a793508c744..33037e75850b 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.test.tsx @@ -31,6 +31,7 @@ async function renderTransactionData(transactionData: string) { type: TransactionType.contractInteraction, status: TransactionStatus.unapproved, txParams: { + to: '0x1234', data: transactionData, }, } as Confirmation); From 5790f85f8107a84eb198a66b5e4038946a818ac2 Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:53:50 +0200 Subject: [PATCH 063/226] feat: Migrate AccountTrackerController to BaseController v2 (#27258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate AccountTrackerController to BaseController v2 PS: Should be merged after the conversion to typescript is merged https://github.com/MetaMask/metamask-extension/pull/27231 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27258?quickstart=1) ## **Related issues** Fixes: [#25929](https://github.com/MetaMask/metamask-extension/issues/25929) ## **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. --- .../account-tracker-controller.test.ts} | 230 ++++++------ .../account-tracker-controller.ts} | 353 ++++++++++++------ .../controllers/mmi-controller.test.ts | 10 +- app/scripts/controllers/mmi-controller.ts | 8 +- app/scripts/metamask-controller.js | 66 ++-- app/scripts/metamask-controller.test.js | 44 ++- .../files-to-convert.json | 1 - shared/constants/mmi-controller.ts | 4 +- 8 files changed, 427 insertions(+), 289 deletions(-) rename app/scripts/{lib/account-tracker.test.ts => controllers/account-tracker-controller.test.ts} (85%) rename app/scripts/{lib/account-tracker.ts => controllers/account-tracker-controller.ts} (68%) diff --git a/app/scripts/lib/account-tracker.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts similarity index 85% rename from app/scripts/lib/account-tracker.test.ts rename to app/scripts/controllers/account-tracker-controller.test.ts index 7cc0dcba14c7..dbabb927fa71 100644 --- a/app/scripts/lib/account-tracker.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -1,19 +1,19 @@ import EventEmitter from 'events'; import { ControllerMessenger } from '@metamask/base-controller'; import { InternalAccount } from '@metamask/keyring-api'; -import { Hex } from '@metamask/utils'; import { BlockTracker, Provider } from '@metamask/network-controller'; import { flushPromises } from '../../../test/lib/timer-helpers'; -import PreferencesController from '../controllers/preferences-controller'; -import OnboardingController from '../controllers/onboarding'; import { createTestProviderTools } from '../../../test/stub/provider'; -import AccountTracker, { - AccountTrackerOptions, +import PreferencesController from './preferences-controller'; +import type { + AccountTrackerControllerOptions, AllowedActions, AllowedEvents, - getDefaultAccountTrackerState, -} from './account-tracker'; +} from './account-tracker-controller'; +import AccountTrackerController, { + getDefaultAccountTrackerControllerState, +} from './account-tracker-controller'; const noop = () => true; const currentNetworkId = '5'; @@ -68,18 +68,18 @@ type WithControllerOptions = { useMultiAccountBalanceChecker?: boolean; getNetworkClientById?: jest.Mock; getSelectedAccount?: jest.Mock; -} & Partial; +} & Partial; type WithControllerCallback = ({ controller, blockTrackerFromHookStub, blockTrackerStub, - triggerOnAccountRemoved, + triggerAccountRemoved, }: { - controller: AccountTracker; + controller: AccountTrackerController; blockTrackerFromHookStub: MockBlockTracker; blockTrackerStub: MockBlockTracker; - triggerOnAccountRemoved: (address: string) => void; + triggerAccountRemoved: (address: string) => void; }) => ReturnValue; type WithControllerArgs = @@ -132,23 +132,37 @@ function withController( chainId: '0x1', }); - const blockTrackerFromHookStub = buildMockBlockTracker(); + const getNetworkStateStub = jest.fn().mockReturnValue({ + selectedNetworkClientId: 'selectedNetworkClientId', + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + getNetworkStateStub, + ); + const blockTrackerFromHookStub = buildMockBlockTracker(); const getNetworkClientByIdStub = jest.fn().mockReturnValue({ configuration: { - chainId: '0x1', + chainId: currentChainId, }, blockTracker: blockTrackerFromHookStub, provider: providerFromHook, }); - controllerMessenger.registerActionHandler( 'NetworkController:getNetworkClientById', getNetworkClientById || getNetworkClientByIdStub, ); - const controller = new AccountTracker({ - initState: getDefaultAccountTrackerState(), + const getOnboardingControllerState = jest.fn().mockReturnValue({ + completedOnboarding, + }); + controllerMessenger.registerActionHandler( + 'OnboardingController:getState', + getOnboardingControllerState, + ); + + const controller = new AccountTrackerController({ + state: getDefaultAccountTrackerControllerState(), provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), @@ -159,13 +173,20 @@ function withController( }), }, } as PreferencesController, - onboardingController: { - state: { - completedOnboarding, - }, - } as OnboardingController, - controllerMessenger, - getCurrentChainId: () => currentChainId, + messenger: controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), ...accountTrackerOptions, }); @@ -173,13 +194,13 @@ function withController( controller, blockTrackerFromHookStub, blockTrackerStub, - triggerOnAccountRemoved: (address: string) => { + triggerAccountRemoved: (address: string) => { controllerMessenger.publish('KeyringController:accountRemoved', address); }, }); } -describe('Account Tracker', () => { +describe('AccountTrackerController', () => { describe('start', () => { it('restarts the subscription to the block tracker and update accounts', async () => { withController(({ controller, blockTrackerStub }) => { @@ -456,9 +477,7 @@ describe('Account Tracker', () => { expect(updateAccountsSpy).toHaveBeenCalledWith(undefined); - const newState = controller.store.getState(); - - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts: {}, accountsByChainId: {}, currentBlockGasLimit: GAS_LIMIT, @@ -509,9 +528,7 @@ describe('Account Tracker', () => { expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - const newState = controller.store.getState(); - - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts: {}, accountsByChainId: {}, currentBlockGasLimit: '', @@ -567,8 +584,7 @@ describe('Account Tracker', () => { async ({ controller }) => { await controller.updateAccounts(); - const state = controller.store.getState(); - expect(state).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts: {}, currentBlockGasLimit: '', accountsByChainId: {}, @@ -579,7 +595,6 @@ describe('Account Tracker', () => { }); describe('chain does not have single call balance address', () => { - const getCurrentChainIdStub: () => Hex = () => '0x999'; // chain without single call balance address const mockAccountsWithSelectedAddress = { ...mockAccounts, [SELECTED_ADDRESS]: { @@ -600,11 +615,9 @@ describe('Account Tracker', () => { { completedOnboarding: true, useMultiAccountBalanceChecker: true, - getCurrentChainId: getCurrentChainIdStub, + state: mockInitialState, }, async ({ controller }) => { - controller.store.updateState(mockInitialState); - await controller.updateAccounts(); const accounts = { @@ -622,8 +635,7 @@ describe('Account Tracker', () => { }, }; - const newState = controller.store.getState(); - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { '0x999': accounts, @@ -642,11 +654,9 @@ describe('Account Tracker', () => { { completedOnboarding: true, useMultiAccountBalanceChecker: false, - getCurrentChainId: getCurrentChainIdStub, + state: mockInitialState, }, async ({ controller }) => { - controller.store.updateState(mockInitialState); - await controller.updateAccounts(); const accounts = { @@ -661,8 +671,7 @@ describe('Account Tracker', () => { }, }; - const newState = controller.store.getState(); - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { '0x999': accounts, @@ -686,20 +695,18 @@ describe('Account Tracker', () => { getNetworkIdentifier: jest .fn() .mockReturnValue('http://not-localhost:8545'), - getCurrentChainId: () => '0x1', // chain with single call balance address getSelectedAccount: jest.fn().mockReturnValue({ id: 'accountId', address: VALID_ADDRESS, } as InternalAccount), - }, - async ({ controller }) => { - controller.store.updateState({ + state: { accounts: { ...mockAccounts }, accountsByChainId: { '0x1': { ...mockAccounts }, }, - }); - + }, + }, + async ({ controller }) => { await controller.updateAccounts('mainnet'); const accounts = { @@ -713,8 +720,7 @@ describe('Account Tracker', () => { }, }; - const newState = controller.store.getState(); - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { '0x1': accounts, @@ -731,75 +737,77 @@ describe('Account Tracker', () => { describe('onAccountRemoved', () => { it('should remove an account from state', () => { - withController(({ controller, triggerOnAccountRemoved }) => { - controller.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, + withController( + { + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, }, }, - }); - - triggerOnAccountRemoved(VALID_ADDRESS); - - const newState = controller.store.getState(); - - const accounts = { - [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], - }; - - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - [currentChainId]: accounts, - '0x1': accounts, - '0x2': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); + }, + ({ controller, triggerAccountRemoved }) => { + triggerAccountRemoved(VALID_ADDRESS); + + const accounts = { + [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], + }; + + expect(controller.state).toStrictEqual({ + accounts, + accountsByChainId: { + [currentChainId]: accounts, + '0x1': accounts, + '0x2': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); }); }); describe('clearAccounts', () => { it('should reset state', () => { - withController(({ controller }) => { - controller.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, + withController( + { + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, }, }, - }); - - controller.clearAccounts(); - - const newState = controller.store.getState(); + }, + ({ controller }) => { + controller.clearAccounts(); - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: { - [currentChainId]: {}, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); + expect(controller.state).toStrictEqual({ + accounts: {}, + accountsByChainId: { + [currentChainId]: {}, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); }); }); }); diff --git a/app/scripts/lib/account-tracker.ts b/app/scripts/controllers/account-tracker-controller.ts similarity index 68% rename from app/scripts/lib/account-tracker.ts rename to app/scripts/controllers/account-tracker-controller.ts index 8ca119ccf83f..e2c78ea3f3f9 100644 --- a/app/scripts/lib/account-tracker.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -10,7 +10,6 @@ import EthQuery from '@metamask/eth-query'; import { v4 as random } from 'uuid'; -import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import pify from 'pify'; import { Web3Provider } from '@ethersproject/providers'; @@ -22,10 +21,16 @@ import { NetworkClientConfiguration, NetworkClientId, NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, Provider, } from '@metamask/network-controller'; import { hasProperty, Hex } from '@metamask/utils'; -import { ControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedEvmAccountChangeEvent, @@ -33,51 +38,139 @@ import { import { KeyringControllerAccountRemovedEvent } from '@metamask/keyring-controller'; import { InternalAccount } from '@metamask/keyring-api'; -import OnboardingController, { - OnboardingControllerStateChangeEvent, -} from '../controllers/onboarding'; -import PreferencesController from '../controllers/preferences-controller'; import { LOCALHOST_RPC_URL } from '../../../shared/constants/network'; import { SINGLE_CALL_BALANCES_ADDRESSES } from '../constants/contracts'; -import { previousValueComparator } from './util'; +import { previousValueComparator } from '../lib/util'; +import type { + OnboardingControllerGetStateAction, + OnboardingControllerStateChangeEvent, +} from './onboarding'; +import PreferencesController from './preferences-controller'; + +// Unique name for the controller +const controllerName = 'AccountTrackerController'; type Account = { address: string; balance: string | null; }; -export type AccountTrackerState = { +/** + * The state of the {@link AccountTrackerController} + * + * @property accounts - The accounts currently stored in this AccountTrackerController + * @property accountsByChainId - The accounts currently stored in this AccountTrackerController keyed by chain id + * @property currentBlockGasLimit - A hex string indicating the gas limit of the current block + * @property currentBlockGasLimitByChainId - A hex string indicating the gas limit of the current block keyed by chain id + */ +export type AccountTrackerControllerState = { accounts: Record>; currentBlockGasLimit: string; - accountsByChainId: Record; + accountsByChainId: Record; currentBlockGasLimitByChainId: Record; }; -export const getDefaultAccountTrackerState = (): AccountTrackerState => ({ - accounts: {}, - currentBlockGasLimit: '', - accountsByChainId: {}, - currentBlockGasLimitByChainId: {}, -}); +/** + * {@link AccountTrackerController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + accounts: { + persist: true, + anonymous: false, + }, + currentBlockGasLimit: { + persist: true, + anonymous: true, + }, + accountsByChainId: { + persist: true, + anonymous: false, + }, + currentBlockGasLimitByChainId: { + persist: true, + anonymous: true, + }, +}; + +/** + * Function to get default state of the {@link AccountTrackerController}. + */ +export const getDefaultAccountTrackerControllerState = + (): AccountTrackerControllerState => ({ + accounts: {}, + currentBlockGasLimit: '', + accountsByChainId: {}, + currentBlockGasLimitByChainId: {}, + }); + +/** + * Returns the state of the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AccountTrackerControllerState +>; +/** + * Actions exposed by the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerActions = + AccountTrackerControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AccountTrackerController} changes. + */ +export type AccountTrackerControllerStateChangeEvent = + ControllerStateChangeEvent< + typeof controllerName, + AccountTrackerControllerState + >; + +/** + * Events emitted by {@link AccountTrackerController}. + */ +export type AccountTrackerControllerEvents = + AccountTrackerControllerStateChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ export type AllowedActions = + | OnboardingControllerGetStateAction | AccountsControllerGetSelectedAccountAction + | NetworkControllerGetStateAction | NetworkControllerGetNetworkClientByIdAction; +/** + * Events that this controller is allowed to subscribe. + */ export type AllowedEvents = | AccountsControllerSelectedEvmAccountChangeEvent | KeyringControllerAccountRemovedEvent | OnboardingControllerStateChangeEvent; -export type AccountTrackerOptions = { - initState: Partial; +/** + * Messenger type for the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AccountTrackerControllerActions | AllowedActions, + AccountTrackerControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +export type AccountTrackerControllerOptions = { + state: Partial; + messenger: AccountTrackerControllerMessenger; provider: Provider; blockTracker: BlockTracker; - getCurrentChainId: () => Hex; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; preferencesController: PreferencesController; - onboardingController: OnboardingController; - controllerMessenger: ControllerMessenger; }; /** @@ -86,22 +179,12 @@ export type AccountTrackerOptions = { * * It also tracks transaction hashes, and checks their inclusion status on each new block. * - * AccountTracker - * - * @property store The stored object containing all accounts to track, as well as the current block's gas limit. - * @property store.accounts The accounts currently stored in this AccountTracker - * @property store.accountsByChainId The accounts currently stored in this AccountTracker keyed by chain id - * @property store.currentBlockGasLimit A hex string indicating the gas limit of the current block - * @property store.currentBlockGasLimitByChainId A hex string indicating the gas limit of the current block keyed by chain id */ -export default class AccountTracker { - /** - * Observable store containing controller data. - */ - store: ObservableStore; - - resetState: () => void; - +export default class AccountTrackerController extends BaseController< + typeof controllerName, + AccountTrackerControllerState, + AccountTrackerControllerMessenger +> { #pollingTokenSets = new Map>(); #listeners: Record Promise> = @@ -113,52 +196,48 @@ export default class AccountTracker { #currentBlockNumberByChainId: Record = {}; - #getCurrentChainId: AccountTrackerOptions['getCurrentChainId']; - - #getNetworkIdentifier: AccountTrackerOptions['getNetworkIdentifier']; + #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesController: AccountTrackerOptions['preferencesController']; - - #onboardingController: AccountTrackerOptions['onboardingController']; - - #controllerMessenger: AccountTrackerOptions['controllerMessenger']; + #preferencesController: AccountTrackerControllerOptions['preferencesController']; #selectedAccount: InternalAccount; /** - * @param opts - Options for initializing the controller - * @param opts.provider - An EIP-1193 provider instance that uses the current global network - * @param opts.blockTracker - A block tracker, which emits events for each new block - * @param opts.getCurrentChainId - A function that returns the `chainId` for the current global network - * @param opts.getNetworkIdentifier - A function that returns the current network or passed nework configuration + * @param options - Options for initializing the controller + * @param options.state - Initial controller state. + * @param options.messenger - Messenger used to communicate with BaseV2 controller. + * @param options.provider - An EIP-1193 provider instance that uses the current global network + * @param options.blockTracker - A block tracker, which emits events for each new block + * @param options.getNetworkIdentifier - A function that returns the current network or passed network configuration + * @param options.preferencesController - The preferences controller */ - constructor(opts: AccountTrackerOptions) { - const initState = getDefaultAccountTrackerState(); - this.store = new ObservableStore({ - ...initState, - ...opts.initState, + constructor(options: AccountTrackerControllerOptions) { + super({ + name: controllerName, + metadata: controllerMetadata, + state: { + ...getDefaultAccountTrackerControllerState(), + ...options.state, + }, + messenger: options.messenger, }); - this.resetState = () => { - this.store.updateState(initState); - }; + this.#provider = options.provider; + this.#blockTracker = options.blockTracker; - this.#provider = opts.provider; - this.#blockTracker = opts.blockTracker; - - this.#getCurrentChainId = opts.getCurrentChainId; - this.#getNetworkIdentifier = opts.getNetworkIdentifier; - this.#preferencesController = opts.preferencesController; - this.#onboardingController = opts.onboardingController; - this.#controllerMessenger = opts.controllerMessenger; + this.#getNetworkIdentifier = options.getNetworkIdentifier; + this.#preferencesController = options.preferencesController; // subscribe to account removal - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'KeyringController:accountRemoved', (address) => this.removeAccounts([address]), ); - this.#controllerMessenger.subscribe( + const onboardingState = this.messagingSystem.call( + 'OnboardingController:getState', + ); + this.messagingSystem.subscribe( 'OnboardingController:stateChange', previousValueComparator((prevState, currState) => { const { completedOnboarding: prevCompletedOnboarding } = prevState; @@ -167,14 +246,14 @@ export default class AccountTracker { this.updateAccountsAllActiveNetworks(); } return true; - }, this.#onboardingController.state), + }, onboardingState), ); - this.#selectedAccount = this.#controllerMessenger.call( + this.#selectedAccount = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'AccountsController:selectedEvmAccountChange', (newAccount) => { const { useMultiAccountBalanceChecker } = @@ -191,6 +270,21 @@ export default class AccountTracker { ); } + resetState(): void { + const { + accounts, + accountsByChainId, + currentBlockGasLimit, + currentBlockGasLimitByChainId, + } = getDefaultAccountTrackerControllerState(); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + state.currentBlockGasLimit = currentBlockGasLimit; + state.currentBlockGasLimitByChainId = currentBlockGasLimitByChainId; + }); + } + /** * Starts polling with global selected network */ @@ -220,6 +314,22 @@ export default class AccountTracker { this.#blockTracker.removeListener('latest', this.#updateForBlock); } + /** + * Gets the current chain ID. + */ + #getCurrentChainId(): Hex { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + return chainId; + } + /** * Resolves a networkClientId to a network client config * or globally selected network config if not provided @@ -235,7 +345,7 @@ export default class AccountTracker { } { if (networkClientId) { const { configuration, provider, blockTracker } = - this.#controllerMessenger.call( + this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, ); @@ -355,13 +465,15 @@ export default class AccountTracker { * * @param chainId - The chain ID */ - #getAccountsForChainId(chainId: Hex): AccountTrackerState['accounts'] { - const { accounts, accountsByChainId } = this.store.getState(); + #getAccountsForChainId( + chainId: Hex, + ): AccountTrackerControllerState['accounts'] { + const { accounts, accountsByChainId } = this.state; if (accountsByChainId[chainId]) { return cloneDeep(accountsByChainId[chainId]); } - const newAccounts: AccountTrackerState['accounts'] = {}; + const newAccounts: AccountTrackerControllerState['accounts'] = {}; Object.keys(accounts).forEach((address) => { newAccounts[address] = {}; }); @@ -370,16 +482,16 @@ export default class AccountTracker { /** * Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this - * AccountTracker. + * AccountTrackerController. * - * Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each + * Once this AccountTrackerController accounts are up to date with those referenced by the passed addresses, each * of these accounts are given an updated balance via EthQuery. * - * @param addresses - The array of hex addresses for accounts with which this AccountTracker's accounts should be + * @param addresses - The array of hex addresses for accounts with which this AccountTrackerController accounts should be * in sync */ syncWithAddresses(addresses: string[]): void { - const { accounts } = this.store.getState(); + const { accounts } = this.state; const locals = Object.keys(accounts); const accountsToAdd: string[] = []; @@ -408,7 +520,7 @@ export default class AccountTracker { */ addAccounts(addresses: string[]): void { const { accounts: _accounts, accountsByChainId: _accountsByChainId } = - this.store.getState(); + this.state; const accounts = cloneDeep(_accounts); const accountsByChainId = cloneDeep(_accountsByChainId); @@ -422,7 +534,10 @@ export default class AccountTracker { }); }); // save accounts state - this.store.updateState({ accounts, accountsByChainId }); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + }); // fetch balances for the accounts if there is block number ready if (this.#currentBlockNumberByChainId[this.#getCurrentChainId()]) { @@ -443,7 +558,7 @@ export default class AccountTracker { */ removeAccounts(addresses: string[]): void { const { accounts: _accounts, accountsByChainId: _accountsByChainId } = - this.store.getState(); + this.state; const accounts = cloneDeep(_accounts); const accountsByChainId = cloneDeep(_accountsByChainId); @@ -457,23 +572,26 @@ export default class AccountTracker { }); }); // save accounts state - this.store.updateState({ accounts, accountsByChainId }); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + }); } /** * Removes all addresses and associated balances */ clearAccounts(): void { - this.store.updateState({ - accounts: {}, - accountsByChainId: { + this.update((state) => { + state.accounts = {}; + state.accountsByChainId = { [this.#getCurrentChainId()]: {}, - }, + }; }); } /** - * Given a block, updates this AccountTracker's currentBlockGasLimit and currentBlockGasLimitByChainId and then updates + * Given a block, updates this AccountTrackerController currentBlockGasLimit and currentBlockGasLimitByChainId and then updates * each local account's balance via EthQuery * * @private @@ -485,7 +603,7 @@ export default class AccountTracker { }; /** - * Given a block, updates this AccountTracker's currentBlockGasLimitByChainId, and then updates each local account's balance + * Given a block, updates this AccountTrackerController currentBlockGasLimitByChainId, and then updates each local account's balance * via EthQuery * * @private @@ -510,15 +628,11 @@ export default class AccountTracker { return; } const currentBlockGasLimit = currentBlock.gasLimit; - const { currentBlockGasLimitByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.#getCurrentChainId() && { - currentBlockGasLimit, - }), - currentBlockGasLimitByChainId: { - ...currentBlockGasLimitByChainId, - [chainId]: currentBlockGasLimit, - }, + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.currentBlockGasLimit = currentBlockGasLimit; + } + state.currentBlockGasLimitByChainId[chainId] = currentBlockGasLimit; }); try { @@ -549,7 +663,9 @@ export default class AccountTracker { * @param networkClientId - optional network client ID to use instead of the globally selected network. */ async updateAccounts(networkClientId?: NetworkClientId): Promise { - const { completedOnboarding } = this.#onboardingController.state; + const { completedOnboarding } = this.messagingSystem.call( + 'OnboardingController:getState', + ); if (!completedOnboarding) { return; } @@ -561,11 +677,11 @@ export default class AccountTracker { let addresses = []; if (useMultiAccountBalanceChecker) { - const { accounts } = this.store.getState(); + const { accounts } = this.state; addresses = Object.keys(accounts); } else { - const selectedAddress = this.#controllerMessenger.call( + const selectedAddress = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ).address; @@ -573,14 +689,11 @@ export default class AccountTracker { } const rpcUrl = 'http://127.0.0.1:8545'; - const singleCallBalancesAddress = - SINGLE_CALL_BALANCES_ADDRESSES[ - chainId as keyof typeof SINGLE_CALL_BALANCES_ADDRESSES - ]; if ( identifier === LOCALHOST_RPC_URL || identifier === rpcUrl || - !singleCallBalancesAddress + !((id): id is keyof typeof SINGLE_CALL_BALANCES_ADDRESSES => + id in SINGLE_CALL_BALANCES_ADDRESSES)(chainId) ) { await Promise.all( addresses.map((address) => @@ -590,7 +703,7 @@ export default class AccountTracker { } else { await this.#updateAccountsViaBalanceChecker( addresses, - singleCallBalancesAddress, + SINGLE_CALL_BALANCES_ADDRESSES[chainId], provider, chainId, ); @@ -657,15 +770,11 @@ export default class AccountTracker { newAccounts[address] = result; - const { accountsByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.#getCurrentChainId() && { - accounts: newAccounts, - }), - accountsByChainId: { - ...accountsByChainId, - [chainId]: newAccounts, - }, + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.accounts = newAccounts; + } + state.accountsByChainId[chainId] = newAccounts; }); } @@ -695,7 +804,7 @@ export default class AccountTracker { const balances = await ethContract.balances(addresses, ethBalance); const accounts = this.#getAccountsForChainId(chainId); - const newAccounts: AccountTrackerState['accounts'] = {}; + const newAccounts: AccountTrackerControllerState['accounts'] = {}; Object.keys(accounts).forEach((address) => { if (!addresses.includes(address)) { newAccounts[address] = { address, balance: null }; @@ -706,15 +815,11 @@ export default class AccountTracker { newAccounts[address] = { address, balance }; }); - const { accountsByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.#getCurrentChainId() && { - accounts: newAccounts, - }), - accountsByChainId: { - ...accountsByChainId, - [chainId]: newAccounts, - }, + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.accounts = newAccounts; + } + state.accountsByChainId[chainId] = newAccounts; }); } catch (error) { log.warn( diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 3a9e6cddba6a..348ccd40916b 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -99,7 +99,7 @@ describe('MMIController', function () { 'NetworkController:infuraIsUnblocked', ], }), - state: mockNetworkState({chainId: CHAIN_IDS.SEPOLIA}), + state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), infuraProjectId: 'mock-infura-project-id', }); @@ -272,7 +272,7 @@ describe('MMIController', function () { mmiController.getState = jest.fn(); mmiController.captureException = jest.fn(); - mmiController.accountTracker = { syncWithAddresses: jest.fn() }; + mmiController.accountTrackerController = { syncWithAddresses: jest.fn() }; jest.spyOn(metaMetricsController.store, 'getState').mockReturnValue({ metaMetricsId: mockMetaMetricsId, @@ -385,7 +385,7 @@ describe('MMIController', function () { mmiController.keyringController.addNewAccountForKeyring = jest.fn(); mmiController.custodyController.setAccountDetails = jest.fn(); - mmiController.accountTracker.syncWithAddresses = jest.fn(); + mmiController.accountTrackerController.syncWithAddresses = jest.fn(); mmiController.storeCustodianSupportedChains = jest.fn(); mmiController.custodyController.storeCustodyStatusMap = jest.fn(); @@ -400,7 +400,9 @@ describe('MMIController', function () { expect( mmiController.custodyController.setAccountDetails, ).toHaveBeenCalled(); - expect(mmiController.accountTracker.syncWithAddresses).toHaveBeenCalled(); + expect( + mmiController.accountTrackerController.syncWithAddresses, + ).toHaveBeenCalled(); expect(mmiController.storeCustodianSupportedChains).toHaveBeenCalled(); expect( mmiController.custodyController.storeCustodyStatusMap, diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 0c43684d7f58..d0e905d673d8 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -39,12 +39,12 @@ import { Signature, ConnectionRequest, } from '../../../shared/constants/mmi-controller'; -import AccountTracker from '../lib/account-tracker'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; +import AccountTrackerController from './account-tracker-controller'; import PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; @@ -86,7 +86,7 @@ export default class MMIController extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any private getPendingNonce: (address: string) => Promise; - private accountTracker: AccountTracker; + private accountTrackerController: AccountTrackerController; private metaMetricsController: MetaMetricsController; @@ -148,7 +148,7 @@ export default class MMIController extends EventEmitter { this.custodyController = opts.custodyController; this.getState = opts.getState; this.getPendingNonce = opts.getPendingNonce; - this.accountTracker = opts.accountTracker; + this.accountTrackerController = opts.accountTrackerController; this.metaMetricsController = opts.metaMetricsController; this.networkController = opts.networkController; this.permissionController = opts.permissionController; @@ -504,7 +504,7 @@ export default class MMIController extends EventEmitter { } }); - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); for (const address of newAccounts) { try { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a5445e16875a..3d6d16df4b95 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -274,7 +274,7 @@ import MMIController from './controllers/mmi-controller'; import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory'; ///: END:ONLY_INCLUDE_IF import ComposableObservableStore from './lib/ComposableObservableStore'; -import AccountTracker from './lib/account-tracker'; +import AccountTrackerController from './controllers/account-tracker-controller'; import createDupeReqFilterStream from './lib/createDupeReqFilterStream'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; import { @@ -1222,7 +1222,7 @@ export default class MetamaskController extends EventEmitter { const internalAccountCount = internalAccounts.length; const accountTrackerCount = Object.keys( - this.accountTracker.store.getState().accounts || {}, + this.accountTrackerController.state.accounts || {}, ).length; captureException( @@ -1655,11 +1655,24 @@ export default class MetamaskController extends EventEmitter { }); // account tracker watches balances, nonces, and any code at their address - this.accountTracker = new AccountTracker({ + this.accountTrackerController = new AccountTrackerController({ + state: { accounts: {} }, + messenger: this.controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), provider: this.provider, blockTracker: this.blockTracker, - getCurrentChainId: () => - getCurrentChainId({ metamask: this.networkController.state }), getNetworkIdentifier: (providerConfig) => { const { type, rpcUrl } = providerConfig ?? @@ -1669,17 +1682,6 @@ export default class MetamaskController extends EventEmitter { return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, preferencesController: this.preferencesController, - onboardingController: this.onboardingController, - controllerMessenger: this.controllerMessenger.getRestricted({ - name: 'AccountTracker', - allowedActions: ['AccountsController:getSelectedAccount'], - allowedEvents: [ - 'AccountsController:selectedEvmAccountChange', - 'OnboardingController:stateChange', - 'KeyringController:accountRemoved', - ], - }), - initState: { accounts: {} }, }); // start and stop polling for balances based on activeControllerConnections @@ -1998,7 +2000,7 @@ export default class MetamaskController extends EventEmitter { custodyController: this.custodyController, getState: this.getState.bind(this), getPendingNonce: this.getPendingNonce.bind(this), - accountTracker: this.accountTracker, + accountTrackerController: this.accountTrackerController, metaMetricsController: this.metaMetricsController, networkController: this.networkController, permissionController: this.permissionController, @@ -2207,11 +2209,11 @@ export default class MetamaskController extends EventEmitter { this._onUserOperationTransactionUpdated.bind(this), ); - // ensure accountTracker updates balances after network change + // ensure AccountTrackerController updates balances after network change networkControllerMessenger.subscribe( 'NetworkController:networkDidChange', () => { - this.accountTracker.updateAccounts(); + this.accountTrackerController.updateAccounts(); }, ); @@ -2323,7 +2325,7 @@ export default class MetamaskController extends EventEmitter { * On chrome profile re-start, they will be re-initialized. */ const resetOnRestartStore = { - AccountTracker: this.accountTracker.store, + AccountTracker: this.accountTrackerController, TokenRatesController: this.tokenRatesController, DecryptMessageController: this.decryptMessageController, EncryptionPublicKeyController: this.encryptionPublicKeyController, @@ -2448,7 +2450,9 @@ export default class MetamaskController extends EventEmitter { // if this is the first time, clear the state of by calling these methods const resetMethods = [ - this.accountTracker.resetState, + this.accountTrackerController.resetState.bind( + this.accountTrackerController, + ), this.decryptMessageController.resetState.bind( this.decryptMessageController, ), @@ -2548,7 +2552,7 @@ export default class MetamaskController extends EventEmitter { } triggerNetworkrequests() { - this.accountTracker.start(); + this.accountTrackerController.start(); this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); @@ -2567,7 +2571,7 @@ export default class MetamaskController extends EventEmitter { } stopNetworkRequests() { - this.accountTracker.stop(); + this.accountTrackerController.stop(); this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); @@ -4268,8 +4272,8 @@ export default class MetamaskController extends EventEmitter { // Clear notification state this.notificationController.clear(); - // clear accounts in accountTracker - this.accountTracker.clearAccounts(); + // clear accounts in AccountTrackerController + this.accountTrackerController.clearAccounts(); this.txController.clearUnapprovedTransactions(); @@ -4366,14 +4370,14 @@ export default class MetamaskController extends EventEmitter { } /** - * Get an account balance from the AccountTracker or request it directly from the network. + * Get an account balance from the AccountTrackerController or request it directly from the network. * * @param {string} address - The account address * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network */ getBalance(address, ethQuery) { return new Promise((resolve, reject) => { - const cached = this.accountTracker.store.getState().accounts[address]; + const cached = this.accountTrackerController.state.accounts[address]; if (cached && cached.balance) { resolve(cached.balance); @@ -4431,9 +4435,9 @@ export default class MetamaskController extends EventEmitter { // Automatic login via config password await this.submitPassword(password); - // Updating accounts in this.accountTracker before starting UI syncing ensure that + // Updating accounts in this.accountTrackerController before starting UI syncing ensure that // state has account balance before it is synced with UI - await this.accountTracker.updateAccountsAllActiveNetworks(); + await this.accountTrackerController.updateAccountsAllActiveNetworks(); } finally { this._startUISync(); } @@ -4610,7 +4614,7 @@ export default class MetamaskController extends EventEmitter { oldAccounts.concat(accounts.map((a) => a.address.toLowerCase())), ), ]; - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); return accounts; } @@ -6157,7 +6161,7 @@ export default class MetamaskController extends EventEmitter { return; } - this.accountTracker.syncWithAddresses(addresses); + this.accountTrackerController.syncWithAddresses(addresses); } /** diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 4121160a45af..d1da34c48e0e 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -738,19 +738,23 @@ describe('MetaMaskController', () => { }); describe('#getBalance', () => { - it('should return the balance known by accountTracker', async () => { + it('should return the balance known by accountTrackerController', async () => { const accounts = {}; const balance = '0x14ced5122ce0a000'; accounts[TEST_ADDRESS] = { balance }; - metamaskController.accountTracker.store.putState({ accounts }); + jest + .spyOn(metamaskController.accountTrackerController, 'state', 'get') + .mockReturnValue({ + accounts, + }); const gotten = await metamaskController.getBalance(TEST_ADDRESS); expect(balance).toStrictEqual(gotten); }); - it('should ask the network for a balance when not known by accountTracker', async () => { + it('should ask the network for a balance when not known by accountTrackerController', async () => { const accounts = {}; const balance = '0x14ced5122ce0a000'; const ethQuery = new EthQuery(); @@ -758,7 +762,11 @@ describe('MetaMaskController', () => { callback(undefined, balance); }); - metamaskController.accountTracker.store.putState({ accounts }); + jest + .spyOn(metamaskController.accountTrackerController, 'state', 'get') + .mockReturnValue({ + accounts, + }); const gotten = await metamaskController.getBalance( TEST_ADDRESS, @@ -1687,21 +1695,27 @@ describe('MetaMaskController', () => { it('should do nothing if there are no keyrings in state', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); await metamaskController._onKeyringControllerUpdate({ keyrings: [] }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).not.toHaveBeenCalled(); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('should sync addresses if there are keyrings in state', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1714,14 +1728,17 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('should NOT update selected address if already unlocked', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1735,14 +1752,17 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('filter out non-EVM addresses prior to calling syncWithAddresses', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1759,7 +1779,7 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index d5063250db16..ea3015d4c1ba 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -63,7 +63,6 @@ "app/scripts/inpage.js", "app/scripts/lib/ComposableObservableStore.js", "app/scripts/lib/ComposableObservableStore.test.js", - "app/scripts/lib/account-tracker.js", "app/scripts/lib/cleanErrorStack.js", "app/scripts/lib/cleanErrorStack.test.js", "app/scripts/lib/createLoggerMiddleware.js", diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 50cc26ef5541..e61d7ed807cd 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -12,7 +12,7 @@ import PreferencesController from '../../app/scripts/controllers/preferences-con import { AppStateController } from '../../app/scripts/controllers/app-state'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import AccountTracker from '../../app/scripts/lib/account-tracker'; +import AccountTrackerController from '../../app/scripts/controllers/account-tracker-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import MetaMetricsController from '../../app/scripts/controllers/metametrics'; @@ -35,7 +35,7 @@ export type MMIControllerOptions = { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getPendingNonce: (address: string) => Promise; - accountTracker: AccountTracker; + accountTrackerController: AccountTrackerController; metaMetricsController: MetaMetricsController; networkController: NetworkController; // TODO: Replace `any` with type From b10ffa6bef36ddf4db63768e138e68b386c7f953 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Fri, 4 Oct 2024 17:14:20 +0200 Subject: [PATCH 064/226] fix: fix reading address from market data (#27604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes error when tokensMarketData sometimes resolves with a small delay which will result in an app error; [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27604?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Switch networks back and forth and you should not see the app crash ## **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/wallet-overview/aggregated-percentage-overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx index e69ff1ed514d..94555d3bc0cd 100644 --- a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx @@ -51,7 +51,7 @@ export const AggregatedPercentageOverview = () => { // This is a regular ERC20 token // find the relevant pricePercentChange1d in tokensMarketData // Find the corresponding market data for the token by filtering the values of the tokensMarketData object - const found = tokensMarketData[toChecksumAddress(item.address)]; + const found = tokensMarketData?.[toChecksumAddress(item.address)]; const tokenFiat1dAgo = getCalculatedTokenAmount1dAgo( item.fiatBalance, From f2192e9be1838f0ae129d5ff92c473611a479b7c Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:45:59 -0700 Subject: [PATCH 065/226] chore: set bridge dest network, tokens and top assets (#26213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** #### Bridge changes * Implements a `selectDestNetwork` bridge controller action, which sets a state value for the destination network for bridged funds * On dest network selection, the controller fetches the bridgeable tokens for the network and also the top assets list #### Swaps changes * Exports the `TOKEN_VALIDATORS` constant in order to reuse it for bridge token list validation * Splits the `fetchTopAssets` util into 2 methods: `fetchTopAssetsList` validates topAssets and returns a list, and `fetchTopAssets` reduces the validated assets list into a mapping [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26213?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/METABRIDGE-866 ## **Manual testing steps** N/A. This doesn't change user functionality for swaps or bridging, just setting up getters/setters ## **Screenshots/Recordings** New values added to state: ``` { metamask: { bridgeState: { destTokens: { [tokenAddress.toLowerCase()]: { ...tokenDetails } }, destTopAssets: [ // list of tokens sorted by popularity ], } } } ``` - [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/scripts/constants/sentry-state.ts | 2 + .../bridge/bridge-controller.test.ts | 44 +++ .../controllers/bridge/bridge-controller.ts | 37 +- app/scripts/controllers/bridge/constants.ts | 2 + app/scripts/controllers/bridge/types.ts | 9 +- app/scripts/metamask-controller.js | 10 +- test/e2e/default-fixture.js | 2 + test/e2e/fixture-builder.js | 2 + ...rs-after-init-opt-in-background-state.json | 4 +- .../errors-after-init-opt-in-ui-state.json | 4 +- ...s-before-init-opt-in-background-state.json | 4 +- .../errors-before-init-opt-in-ui-state.json | 4 +- test/jest/mock-store.js | 14 +- ui/ducks/bridge/actions.ts | 35 +- ui/ducks/bridge/bridge.test.ts | 33 +- ui/ducks/bridge/bridge.ts | 10 +- ui/ducks/bridge/selectors.test.ts | 342 +++++++++++------- ui/ducks/bridge/selectors.ts | 66 +++- ui/pages/bridge/bridge.util.test.ts | 64 +++- ui/pages/bridge/bridge.util.ts | 73 +++- ui/pages/swaps/swaps.util.test.js | 20 + ui/pages/swaps/swaps.util.ts | 27 +- 22 files changed, 614 insertions(+), 194 deletions(-) diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 9763d152eb39..831dc6c539fb 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -102,6 +102,8 @@ export const SENTRY_BACKGROUND_STATE = { destNetworkAllowlist: [], srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], }, }, CronjobController: { diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 9c9036b87f7b..221a1e1a2a00 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -1,6 +1,7 @@ import nock from 'nock'; import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; @@ -32,6 +33,28 @@ describe('BridgeController', function () { 'src-network-allowlist': [10, 534352], 'dest-network-allowlist': [137, 42161], }); + nock(BRIDGE_API_BASE_URL) + .get('/getTokens?chainId=10') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1291478912', + symbol: 'DEF', + decimals: 16, + }, + ]); + nock(SWAPS_API_V2_BASE_URL) + .get('/networks/10/topAssets') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); }); it('constructor should setup correctly', function () { @@ -51,4 +74,25 @@ describe('BridgeController', function () { expectedFeatureFlagsResponse, ); }); + + it('selectDestNetwork should set the bridge dest tokens and top assets', async function () { + await bridgeController.selectDestNetwork('0xa'); + expect(bridgeController.state.bridgeState.destTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([ + { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, + ]); + }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 6ca076c2e060..1bc673af43f8 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -1,7 +1,14 @@ import { BaseController, StateMetadata } from '@metamask/base-controller'; +import { Hex } from '@metamask/utils'; +import { + fetchBridgeFeatureFlags, + fetchBridgeTokens, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../../ui/pages/bridge/bridge.util'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { fetchBridgeFeatureFlags } from '../../../../ui/pages/bridge/bridge.util'; +import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -32,6 +39,10 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, + this.selectDestNetwork.bind(this), + ); } resetState = () => { @@ -49,4 +60,28 @@ export default class BridgeController extends BaseController< _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; }); }; + + selectDestNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'destTopAssets'); + await this.#setTokens(chainId, 'destTokens'); + }; + + #setTopAssets = async ( + chainId: Hex, + stateKey: 'srcTopAssets' | 'destTopAssets', + ) => { + const { bridgeState } = this.state; + const topAssets = await fetchTopAssetsList(chainId); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, [stateKey]: topAssets }; + }); + }; + + #setTokens = async (chainId: Hex, stateKey: 'srcTokens' | 'destTokens') => { + const { bridgeState } = this.state; + const tokens = await fetchBridgeTokens(chainId); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, [stateKey]: tokens }; + }); + }; } diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index f2932120f98d..e21071d71c4d 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -8,4 +8,6 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }, + destTokens: {}, + destTopAssets: [], }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index aa92a6597c69..ddc4668b3e53 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -3,6 +3,7 @@ import { RestrictedControllerMessenger, } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { SwapsTokenObject } from '../../../../shared/constants/swaps'; import BridgeController from './bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './constants'; @@ -20,8 +21,13 @@ export type BridgeFeatureFlags = { export type BridgeControllerState = { bridgeFeatureFlags: BridgeFeatureFlags; + destTokens: Record; + destTopAssets: { address: string }[]; }; +export enum BridgeUserAction { + SELECT_DEST_NETWORK = 'selectDestNetwork', +} export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', } @@ -33,7 +39,8 @@ type BridgeControllerAction = { // Maps to BridgeController function names type BridgeControllerActions = - BridgeControllerAction; + | BridgeControllerAction + | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3d6d16df4b95..584ae7e91ad2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -347,7 +347,10 @@ import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; import { decodeTransactionData } from './lib/transaction/decode/util'; -import { BridgeBackgroundAction } from './controllers/bridge/types'; +import { + BridgeUserAction, + BridgeBackgroundAction, +} from './controllers/bridge/types'; import BridgeController from './controllers/bridge/bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; import { @@ -3888,6 +3891,11 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.SET_FEATURE_FLAGS}`, ), + [BridgeUserAction.SELECT_DEST_NETWORK]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_DEST_NETWORK}`, + ), // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 4605e0bb0295..d56141572d81 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -127,6 +127,8 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { srcNetworkAllowlist: ['0x1', '0xa', '0xe708'], destNetworkAllowlist: ['0x1', '0xa', '0xe708'], }, + destTokens: {}, + destTopAssets: [], }, }, CurrencyController: { diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index edce958fab11..a8c80e972346 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -400,6 +400,8 @@ class FixtureBuilder { extensionSupport: false, srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], }, }; return this; 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 ad0e4014805a..bbd833c87656 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 @@ -65,7 +65,9 @@ "extensionSupport": "boolean", "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {} } }, "CronjobController": { "jobs": "object" }, 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 a566fe4ece2f..9812df603e92 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 @@ -252,7 +252,9 @@ "extensionSupport": "boolean", "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {} }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index f21b237a1c46..14f0c27c5d80 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -153,7 +153,9 @@ "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {} } }, "SubjectMetadataController": { "subjectMetadata": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 833584fd8c6d..c899811aad0f 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -162,7 +162,9 @@ "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {} } }, "TransactionController": { "transactions": "object" }, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 736b9c4eb325..a18f2e0b6944 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -705,16 +705,23 @@ export const createSwapsMockStore = () => { export const createBridgeMockStore = ( featureFlagOverrides = {}, bridgeSliceOverrides = {}, + bridgeStateOverrides = {}, + metamaskStateOverrides = {}, ) => { const swapsStore = createSwapsMockStore(); return { ...swapsStore, bridge: { - toChain: null, + toChainId: null, ...bridgeSliceOverrides, }, metamask: { ...swapsStore.metamask, + ...mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.LINEA_MAINNET }, + ), + ...metamaskStateOverrides, bridgeState: { ...(swapsStore.metamask.bridgeState ?? {}), bridgeFeatureFlags: { @@ -723,11 +730,8 @@ export const createBridgeMockStore = ( destNetworkAllowlist: [], ...featureFlagOverrides, }, + ...bridgeStateOverrides, }, - ...mockNetworkState( - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.LINEA_MAINNET }, - ), }, }; }; diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 24cd9728625f..47912db8fd17 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -1,18 +1,29 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { BridgeBackgroundAction } from '../../../app/scripts/controllers/bridge/types'; +import { Hex } from '@metamask/utils'; +import { + BridgeBackgroundAction, + BridgeUserAction, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../app/scripts/controllers/bridge/types'; + import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; import { bridgeSlice } from './bridge'; -const { setToChain, setFromToken, setToToken, setFromTokenInputValue } = - bridgeSlice.actions; +const { + setToChainId: setToChainId_, + setFromToken, + setToToken, + setFromTokenInputValue, +} = bridgeSlice.actions; -export { setToChain, setFromToken, setToToken, setFromTokenInputValue }; +export { setFromToken, setToToken, setFromTokenInputValue }; const callBridgeControllerMethod = ( - bridgeAction: BridgeBackgroundAction, + bridgeAction: BridgeUserAction | BridgeBackgroundAction, args?: T[], ) => { return async (dispatch: MetaMaskReduxDispatch) => { @@ -21,8 +32,6 @@ const callBridgeControllerMethod = ( }; }; -// User actions - // Background actions export const setBridgeFeatureFlags = () => { return async (dispatch: MetaMaskReduxDispatch) => { @@ -31,3 +40,15 @@ export const setBridgeFeatureFlags = () => { ); }; }; + +// User actions +export const setToChain = (chainId: Hex) => { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(setToChainId_(chainId)); + dispatch( + callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [ + chainId, + ]), + ); + }; +}; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index 0bfdb47b35eb..a9eddde18081 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -3,9 +3,12 @@ import thunk from 'redux-thunk'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { setBackgroundConnection } from '../../store/background-connection'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { BridgeBackgroundAction } from '../../../app/scripts/controllers/bridge/types'; +import { + BridgeBackgroundAction, + BridgeUserAction, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../app/scripts/controllers/bridge/types'; import bridgeReducer from './bridge'; import { setBridgeFeatureFlags, @@ -26,14 +29,28 @@ describe('Ducks - Bridge', () => { }); describe('setToChain', () => { - it('calls the "bridge/setToChain" action', () => { + it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => { const state = store.getState().bridge; - const actionPayload = CHAIN_IDS.BSC; - store.dispatch(setToChain(actionPayload)); + const actionPayload = CHAIN_IDS.OPTIMISM; + + const mockSelectDestNetwork = jest.fn().mockReturnValue({}); + setBackgroundConnection({ + [BridgeUserAction.SELECT_DEST_NETWORK]: mockSelectDestNetwork, + } as never); + + store.dispatch(setToChain(actionPayload as never) as never); + + // Check redux state const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setToChain'); + expect(actions[0].type).toBe('bridge/setToChainId'); const newState = bridgeReducer(state, actions[0]); - expect(newState.toChain).toBe(actionPayload); + expect(newState.toChainId).toBe(actionPayload); + // Check background state + expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); + expect(mockSelectDestNetwork).toHaveBeenCalledWith( + '0xa', + expect.anything(), + ); }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index a35534381000..f2469d1025f3 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -1,19 +1,19 @@ import { createSlice } from '@reduxjs/toolkit'; +import { Hex } from '@metamask/utils'; import { swapsSlice } from '../swaps/swaps'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { SwapsEthToken } from '../../selectors'; -import { MultichainProviderConfig } from '../../../shared/constants/multichain/networks'; export type BridgeState = { - toChain: MultichainProviderConfig | null; + toChainId: Hex | null; fromToken: SwapsTokenObject | SwapsEthToken | null; toToken: SwapsTokenObject | SwapsEthToken | null; fromTokenInputValue: string | null; }; const initialState: BridgeState = { - toChain: null, + toChainId: null, fromToken: null, toToken: null, fromTokenInputValue: null, @@ -24,8 +24,8 @@ const bridgeSlice = createSlice({ initialState: { ...initialState }, reducers: { ...swapsSlice.reducer, - setToChain: (state, action) => { - state.toChain = action.payload; + setToChainId: (state, action) => { + state.toChainId = action.payload; }, setFromToken: (state, action) => { state.fromToken = action.payload; diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 9a7c818fb20a..98c5264dd97d 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -1,7 +1,10 @@ import { createBridgeMockStore } from '../../../test/jest/mock-store'; -import { CHAIN_IDS, FEATURED_RPCS } from '../../../shared/constants/network'; +import { + BUILT_IN_NETWORKS, + CHAIN_IDS, + FEATURED_RPCS, +} from '../../../shared/constants/network'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; -import { getProviderConfig } from '../metamask/metamask'; import { mockNetworkState } from '../../../test/stub/networks'; import { getAllBridgeableNetworks, @@ -14,102 +17,104 @@ import { getToChain, getToChains, getToToken, + getToTokens, + getToTopAssets, } from './selectors'; describe('Bridge selectors', () => { describe('getFromChain', () => { it('returns the fromChain from the state', () => { - const state = { - metamask: { ...mockNetworkState({ chainId: '0x1' }) }, - }; + const state = createBridgeMockStore( + { srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] }, + { toChainId: '0xe708' }, + {}, + { ...mockNetworkState(FEATURED_RPCS[0]) }, + ); + const result = getFromChain(state as never); - expect(result).toStrictEqual(getProviderConfig(state)); + expect(result).toStrictEqual({ + blockExplorerUrls: ['https://localhost/blockExplorer/0xa4b1'], + chainId: '0xa4b1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Arbitrum One', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: expect.anything(), + type: 'custom', + url: 'https://localhost/rpc/0xa4b1', + }, + ], + }); }); }); describe('getToChain', () => { it('returns the toChain from the state', () => { - const state = { - bridge: { - toChain: { chainId: '0x1' } as unknown, - }, - }; + const state = createBridgeMockStore( + { destNetworkAllowlist: ['0xe708'] }, + { toChainId: '0xe708' }, + ); const result = getToChain(state as never); - expect(result).toStrictEqual({ chainId: '0x1' }); + expect(result).toStrictEqual({ + blockExplorerUrls: ['https://localhost/blockExplorer/0xe708'], + chainId: '0xe708', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Linea Mainnet', + rpcEndpoints: [ + { + networkClientId: expect.anything(), + type: 'custom', + url: 'https://localhost/rpc/0xe708', + }, + ], + nativeCurrency: 'ETH', + }); }); }); describe('getAllBridgeableNetworks', () => { it('returns list of ALLOWED_BRIDGE_CHAIN_IDS networks', () => { - const state = createBridgeMockStore(); + const state = createBridgeMockStore( + {}, + {}, + {}, + mockNetworkState(...FEATURED_RPCS), + ); const result = getAllBridgeableNetworks(state as never); - expect(result).toHaveLength(9); + expect(result).toHaveLength(7); expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), + expect.objectContaining({ chainId: FEATURED_RPCS[0].chainId }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), + expect.objectContaining({ chainId: FEATURED_RPCS[1].chainId }), ); - expect(result.slice(2)).toStrictEqual(FEATURED_RPCS); + FEATURED_RPCS.forEach((rpcDefinition, idx) => { + expect(result[idx]).toStrictEqual( + expect.objectContaining({ + ...rpcDefinition, + blockExplorerUrls: [ + `https://localhost/blockExplorer/${rpcDefinition.chainId}`, + ], + name: expect.anything(), + rpcEndpoints: [ + { + networkClientId: expect.anything(), + type: 'custom', + url: `https://localhost/rpc/${rpcDefinition.chainId}`, + }, + ], + }), + ); + }); result.forEach(({ chainId }) => { expect(ALLOWED_BRIDGE_CHAIN_IDS).toContain(chainId); }); - ALLOWED_BRIDGE_CHAIN_IDS.forEach((allowedChainId) => { - expect( - result.findIndex(({ chainId }) => chainId === allowedChainId), - ).toBeGreaterThan(-1); - }); - }); - - it('uses config from allNetworks if network is in both FEATURED_RPCS and allNetworks', () => { - const addedFeaturedNetwork = { - ...FEATURED_RPCS[FEATURED_RPCS.length - 1], - }; - - const state = { - ...createBridgeMockStore(), - metamask: { - networkConfigurations: [addedFeaturedNetwork], - ...mockNetworkState( - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.LINEA_MAINNET }, - { - ...FEATURED_RPCS[FEATURED_RPCS.length - 1], - id: 'testid', - blockExplorerUrl: 'https://basescan.org', - rpcUrl: 'https://mainnet.base.org', - }, - ), - }, - }; - const result = getAllBridgeableNetworks(state as never); - - expect(result).toHaveLength(9); - expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), - ); - expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), - ); - expect(result[2]).toStrictEqual({ - blockExplorerUrls: addedFeaturedNetwork.blockExplorerUrls, - chainId: addedFeaturedNetwork.chainId, - defaultBlockExplorerUrlIndex: - addedFeaturedNetwork.defaultBlockExplorerUrlIndex, - defaultRpcEndpointIndex: addedFeaturedNetwork.defaultRpcEndpointIndex, - name: addedFeaturedNetwork.name, - nativeCurrency: addedFeaturedNetwork.nativeCurrency, - rpcEndpoints: [ - { - networkClientId: 'testid', - ...addedFeaturedNetwork.rpcEndpoints[0], - }, - ], - }); - expect(result.slice(3)).toStrictEqual(FEATURED_RPCS.slice(0, -1)); }); it('returns network if included in ALLOWED_BRIDGE_CHAIN_IDS', () => { @@ -119,45 +124,46 @@ describe('Bridge selectors', () => { ...mockNetworkState( { chainId: CHAIN_IDS.MAINNET }, { chainId: CHAIN_IDS.LINEA_MAINNET }, + { chainId: CHAIN_IDS.MOONBEAM }, ), }, }; const result = getAllBridgeableNetworks(state as never); - expect(result).toHaveLength(9); + expect(result).toHaveLength(2); expect(result[0]).toStrictEqual( expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), ); expect(result[1]).toStrictEqual( expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), ); - expect(result.slice(2)).toStrictEqual(FEATURED_RPCS); + expect( + result.find(({ chainId }) => chainId === CHAIN_IDS.MOONBEAM), + ).toStrictEqual(undefined); }); }); describe('getFromChains', () => { - it('excludes selected toChain and disabled chains from options', () => { + it('excludes disabled chains from options', () => { const state = createBridgeMockStore( { srcNetworkAllowlist: [ CHAIN_IDS.MAINNET, + CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.OPTIMISM, CHAIN_IDS.POLYGON, ], }, - { toChain: { chainId: CHAIN_IDS.MAINNET } }, + { toChainId: CHAIN_IDS.LINEA_MAINNET }, ); const result = getFromChains(state as never); - expect(result).toHaveLength(3); + expect(result).toHaveLength(2); expect(result[0]).toStrictEqual( expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), - ); - expect(result[2]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), + expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), ); }); @@ -171,24 +177,32 @@ describe('Bridge selectors', () => { describe('getToChains', () => { it('excludes selected providerConfig and disabled chains from options', () => { - const state = createBridgeMockStore({ - destNetworkAllowlist: [ - CHAIN_IDS.MAINNET, - CHAIN_IDS.OPTIMISM, - CHAIN_IDS.POLYGON, - ], - }); + const state = createBridgeMockStore( + { + destNetworkAllowlist: [ + CHAIN_IDS.ARBITRUM, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.POLYGON, + ], + }, + {}, + {}, + mockNetworkState(...FEATURED_RPCS, { + chainId: CHAIN_IDS.LINEA_MAINNET, + }), + ); const result = getToChains(state as never); expect(result).toHaveLength(3); expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), + expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), + expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), ); expect(result[2]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), + expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), ); }); @@ -202,44 +216,50 @@ describe('Bridge selectors', () => { describe('getIsBridgeTx', () => { it('returns false if bridge is not enabled', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: false } }, + const state = createBridgeMockStore( + { + extensionSupport: false, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x38'], }, - bridge: { toChain: { chainId: '0x38' } as unknown }, - }; + { toChainId: '0x38' }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, + ); const result = getIsBridgeTx(state as never); expect(result).toBe(false); }); - it('returns false if toChain is null', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, + it('returns false if toChainId is null', () => { + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x1'], }, - bridge: { toChain: null }, - }; + { toChainId: null }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, + ); const result = getIsBridgeTx(state as never); expect(result).toBe(false); }); - it('returns false if fromChain and toChain have the same chainId', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, + it('returns false if fromChain and toChainId have the same chainId', () => { + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x1'], }, - bridge: { toChain: { chainId: '0x1' } }, - }; + { toChainId: '0x1' }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, + ); const result = getIsBridgeTx(state as never); @@ -247,29 +267,39 @@ describe('Bridge selectors', () => { }); it('returns false if useExternalServices is not enabled', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: false, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x38'], }, - bridge: { toChain: { chainId: '0x38' } }, - }; + { toChainId: '0x38' }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: false }, + ); const result = getIsBridgeTx(state as never); expect(result).toBe(false); }); - it('returns true if bridge is enabled and fromChain and toChain have different chainIds', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), + it('returns true if bridge is enabled and fromChain and toChainId have different chainIds', () => { + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x38'], + }, + { toChainId: '0x38' }, + {}, + { + ...mockNetworkState( + ...Object.values(BUILT_IN_NETWORKS), + ...FEATURED_RPCS, + ), useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, }, - bridge: { toChain: { chainId: '0x38' } }, - }; + ); const result = getIsBridgeTx(state as never); @@ -366,4 +396,64 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual('0'); }); }); + + describe('getToTokens', () => { + it('returns dest tokens from controller state when toChainId is defined', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getToTokens(state as never); + + expect(result).toStrictEqual({ + '0x00': { address: '0x00', symbol: 'TEST' }, + }); + }); + + it('returns empty dest tokens from controller state when toChainId is undefined', () => { + const state = createBridgeMockStore( + {}, + {}, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getToTokens(state as never); + + expect(result).toStrictEqual({}); + }); + }); + + describe('getToTopAssets', () => { + it('returns dest top assets from controller state when toChainId is defined', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + destTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getToTopAssets(state as never); + + expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); + }); + + it('returns empty dest top assets from controller state when toChainId is undefined', () => { + const state = createBridgeMockStore( + {}, + {}, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + destTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getToTopAssets(state as never); + + expect(result).toStrictEqual([]); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index b688b2096e51..dd8dfa7a8999 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -1,4 +1,7 @@ -import { NetworkState } from '@metamask/network-controller'; +import { + NetworkConfiguration, + NetworkState, +} from '@metamask/network-controller'; import { uniqBy } from 'lodash'; import { getNetworkConfigurationsByChainId, @@ -13,7 +16,6 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../app/scripts/controllers/bridge/types'; -import { FEATURED_RPCS } from '../../../shared/constants/network'; import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; @@ -26,14 +28,12 @@ type BridgeAppState = { bridge: BridgeState; }; -export const getFromChain = (state: BridgeAppState) => getProviderConfig(state); -export const getToChain = (state: BridgeAppState) => state.bridge.toChain; - +// only includes networks user has added export const getAllBridgeableNetworks = createDeepEqualSelector( getNetworkConfigurationsByChainId, (networkConfigurationsByChainId) => { return uniqBy( - [...Object.values(networkConfigurationsByChainId), ...FEATURED_RPCS], + Object.values(networkConfigurationsByChainId), 'chainId', ).filter(({ chainId }) => ALLOWED_BRIDGE_CHAIN_IDS.includes( @@ -42,6 +42,7 @@ export const getAllBridgeableNetworks = createDeepEqualSelector( ); }, ); + export const getFromChains = createDeepEqualSelector( getAllBridgeableNetworks, (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, @@ -52,17 +53,53 @@ export const getFromChains = createDeepEqualSelector( ), ), ); + +export const getFromChain = createDeepEqualSelector( + getNetworkConfigurationsByChainId, + getProviderConfig, + ( + networkConfigurationsByChainId, + providerConfig, + ): NetworkConfiguration | undefined => + providerConfig?.chainId + ? networkConfigurationsByChainId[providerConfig.chainId] + : undefined, +); + export const getToChains = createDeepEqualSelector( + getFromChain, getAllBridgeableNetworks, (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, - (allBridgeableNetworks, bridgeFeatureFlags) => - allBridgeableNetworks.filter(({ chainId }) => - bridgeFeatureFlags[BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST].includes( - chainId, - ), + ( + fromChain, + allBridgeableNetworks, + bridgeFeatureFlags, + ): NetworkConfiguration[] => + allBridgeableNetworks.filter( + ({ chainId }) => + fromChain?.chainId && + chainId !== fromChain.chainId && + bridgeFeatureFlags[ + BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST + ].includes(chainId), ), ); +export const getToChain = createDeepEqualSelector( + getToChains, + (state: BridgeAppState) => state.bridge.toChainId, + (toChains, toChainId): NetworkConfiguration | undefined => + toChains.find(({ chainId }) => chainId === toChainId), +); + +export const getToTopAssets = (state: BridgeAppState) => { + return state.bridge.toChainId ? state.metamask.bridgeState.destTopAssets : []; +}; + +export const getToTokens = (state: BridgeAppState) => { + return state.bridge.toChainId ? state.metamask.bridgeState.destTokens : {}; +}; + export const getFromToken = ( state: BridgeAppState, ): SwapsTokenObject | SwapsEthToken => { @@ -88,8 +125,7 @@ export const getIsBridgeTx = createDeepEqualSelector( getToChain, (state: BridgeAppState) => getIsBridgeEnabled(state), (fromChain, toChain, isBridgeEnabled: boolean) => - isBridgeEnabled && - toChain !== null && - fromChain !== undefined && - fromChain.chainId !== toChain.chainId, + isBridgeEnabled && toChain && fromChain?.chainId + ? fromChain.chainId !== toChain.chainId + : false, ); diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts index da2637b1f5f4..07c35ae57749 100644 --- a/ui/pages/bridge/bridge.util.test.ts +++ b/ui/pages/bridge/bridge.util.test.ts @@ -1,6 +1,6 @@ import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { CHAIN_IDS } from '../../../shared/constants/network'; -import { fetchBridgeFeatureFlags } from './bridge.util'; +import { fetchBridgeFeatureFlags, fetchBridgeTokens } from './bridge.util'; jest.mock('../../../shared/lib/fetch-with-cache'); @@ -79,4 +79,66 @@ describe('Bridge utils', () => { await expect(fetchBridgeFeatureFlags()).rejects.toThrowError(mockError); }); }); + + describe('fetchBridgeTokens', () => { + it('should fetch bridge tokens successfully', async () => { + const mockResponse = [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + symbol: 'DEF', + }, + { + address: '0x124', + symbol: 'JKL', + decimals: 16, + }, + ]; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeTokens('0xa'); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeTokens', + }); + + expect(result).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 16, + symbol: 'ABC', + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (fetchWithCache as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError); + }); + }); }); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index 0f72b75a0787..915a933e7c02 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -1,4 +1,4 @@ -import { add0x } from '@metamask/utils'; +import { Hex, add0x } from '@metamask/utils'; import { BridgeFeatureFlagsKey, BridgeFeatureFlags, @@ -12,7 +12,19 @@ import { import { MINUTE } from '../../../shared/constants/time'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { validateData } from '../../../shared/lib/swaps-utils'; -import { decimalToHex } from '../../../shared/modules/conversion.utils'; +import { + decimalToHex, + hexToDecimal, +} from '../../../shared/modules/conversion.utils'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, +} from '../../../shared/constants/swaps'; +import { TOKEN_VALIDATORS } from '../swaps/swaps.util'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../shared/modules/swaps.utils'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; @@ -31,17 +43,17 @@ export type FeatureFlagResponse = { }; // End of copied types -type Validator = { - property: keyof T; +type Validator = { + property: keyof ExpectedResponse | string; type: string; - validator: (value: unknown) => boolean; + validator: (value: DataToValidate) => boolean; }; -const validateResponse = ( - validators: Validator[], +const validateResponse = ( + validators: Validator[], data: unknown, urlUsed: string, -): data is T => { +): data is ExpectedResponse => { return validateData(validators, data, urlUsed); }; @@ -55,7 +67,7 @@ export async function fetchBridgeFeatureFlags(): Promise { }); if ( - validateResponse( + validateResponse( [ { property: BridgeFlag.EXTENSION_SUPPORT, @@ -104,3 +116,46 @@ export async function fetchBridgeFeatureFlags(): Promise { [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }; } + +// Returns a list of enabled (unblocked) tokens +export async function fetchBridgeTokens( + chainId: Hex, +): Promise> { + // TODO make token api v2 call + const url = `${BRIDGE_API_BASE_URL}/getTokens?chainId=${hexToDecimal( + chainId, + )}`; + const tokens = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeTokens', + }); + + const nativeToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + const transformedTokens: Record = {}; + if (nativeToken) { + transformedTokens[nativeToken.address] = nativeToken; + } + + tokens.forEach((token: SwapsTokenObject) => { + if ( + validateResponse( + TOKEN_VALIDATORS, + token, + url, + ) && + !( + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) + ) + ) { + transformedTokens[token.address] = token; + } + }); + return transformedTokens; +} diff --git a/ui/pages/swaps/swaps.util.test.js b/ui/pages/swaps/swaps.util.test.js index d7ecace642ae..4b277ab56345 100644 --- a/ui/pages/swaps/swaps.util.test.js +++ b/ui/pages/swaps/swaps.util.test.js @@ -35,6 +35,7 @@ import { showRemainingTimeInMinAndSec, getFeeForSmartTransaction, formatSwapsValueForDisplay, + fetchTopAssetsList, } from './swaps.util'; jest.mock('../../../shared/lib/storage-helpers', () => ({ @@ -85,6 +86,25 @@ describe('Swaps Util', () => { }); }); + describe('fetchTopAssetsList', () => { + beforeEach(() => { + nock('https://swap.api.cx.metamask.io') + .persist() + .get('/networks/1/topAssets') + .reply(200, TOP_ASSETS); + }); + + it('should fetch top assets', async () => { + const result = await fetchTopAssetsList(CHAIN_IDS.MAINNET); + expect(result).toStrictEqual(TOP_ASSETS); + }); + + it('should fetch top assets on prod', async () => { + const result = await fetchTopAssetsList(CHAIN_IDS.MAINNET); + expect(result).toStrictEqual(TOP_ASSETS); + }); + }); + describe('fetchTopAssets', () => { beforeEach(() => { nock('https://swap.api.cx.metamask.io') diff --git a/ui/pages/swaps/swaps.util.ts b/ui/pages/swaps/swaps.util.ts index ce06d4ef37f8..21de45b5f349 100644 --- a/ui/pages/swaps/swaps.util.ts +++ b/ui/pages/swaps/swaps.util.ts @@ -56,7 +56,7 @@ type Validator = { validator: (a: string) => boolean; }; -const TOKEN_VALIDATORS: Validator[] = [ +export const TOKEN_VALIDATORS: Validator[] = [ { property: 'address', type: 'string', @@ -199,9 +199,9 @@ export async function fetchAggregatorMetadata(chainId: any): Promise { return filteredAggregators; } -// TODO: Replace `any` with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function fetchTopAssets(chainId: any): Promise { +export async function fetchTopAssetsList( + chainId: string, +): Promise<{ address: string }[]> { const topAssetsUrl = getBaseApi('topAssets', chainId); const response = (await fetchWithCache({ @@ -210,14 +210,19 @@ export async function fetchTopAssets(chainId: any): Promise { fetchOptions: { method: 'GET', headers: clientIdHeader }, cacheOptions: { cacheRefreshTime: CACHE_REFRESH_FIVE_MINUTES }, })) || []; + const topAssetsList = response.filter((asset: { address: string }) => + validateData(TOP_ASSET_VALIDATORS, asset, topAssetsUrl), + ); + return topAssetsList; +} + +export async function fetchTopAssets( + chainId: string, +): Promise> { + const response = await fetchTopAssetsList(chainId); const topAssetsMap = response.reduce( - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_topAssetsMap: any, asset: { address: string }, index: number) => { - if (validateData(TOP_ASSET_VALIDATORS, asset, topAssetsUrl)) { - return { ..._topAssetsMap, [asset.address]: { index: String(index) } }; - } - return _topAssetsMap; + (_topAssetsMap, asset: { address: string }, index: number) => { + return { ..._topAssetsMap, [asset.address]: { index: String(index) } }; }, {}, ); From a451a4045eb72b7448ac42e2c558052c88bee3f1 Mon Sep 17 00:00:00 2001 From: Bowen Sanders Date: Fri, 4 Oct 2024 12:10:36 -0700 Subject: [PATCH 066/226] test: [Snaps E2E] add delay to installed snaps test to reduce flaking (#27521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This adds an additional delay to the 500ms delay to allow CI to actually scroll to the correct place before clicking an element. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27521?quickstart=1) ## **Related issues** Fixes: #26804 ## **Manual testing steps** 1. Does CI Pass every time? ## **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/snaps/test-snap-installed.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/snaps/test-snap-installed.spec.js b/test/e2e/snaps/test-snap-installed.spec.js index e9697fff806e..5c7a3394966f 100644 --- a/test/e2e/snaps/test-snap-installed.spec.js +++ b/test/e2e/snaps/test-snap-installed.spec.js @@ -35,7 +35,7 @@ describe('Test Snap Installed', function () { const confirmButton = await driver.findElement('#connectdialogs'); await driver.scrollToElement(confirmButton); - await driver.delay(500); + await driver.delay(1000); await driver.clickElement('#connectdialogs'); // switch to metamask extension and click connect From ec698f8a1b1ac016e9a2b8c2c2084a66f0dcc3f3 Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:29:23 -0700 Subject: [PATCH 067/226] chore: set bridge src network, tokens and top assets (#26214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** * Implements a `selectSrcNetwork` bridge controller action, which sets a state value for the cross-chain swaps source network * On src network change, the controller fetches the bridgeable tokens for the network and also the top assets list * Adds a call to the `useBridging` hook within the bridge route, which sets the src network, tokens and topAssets when the bridge experience is loaded [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26214?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/METABRIDGE-866 ## **Manual testing steps** N/A, changes only affect state and are not visible ## **Screenshots/Recordings** New values added to state: ``` { metamask: { bridgeState: { srcTokens: { [tokenAddress.toLowerCase()]: { ...tokenDetails } }, srcTopAssets: [ // list of tokens sorted by popularity ], } } } ``` ## **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/constants/sentry-state.ts | 2 ++ .../bridge/bridge-controller.test.ts | 24 +++++++++++++ .../controllers/bridge/bridge-controller.ts | 9 +++++ app/scripts/controllers/bridge/constants.ts | 2 ++ app/scripts/controllers/bridge/types.ts | 4 +++ app/scripts/metamask-controller.js | 4 +++ test/e2e/default-fixture.js | 2 ++ test/e2e/fixture-builder.js | 2 ++ test/e2e/tests/bridge/bridge-test-utils.ts | 12 ++++++- ...rs-after-init-opt-in-background-state.json | 4 ++- .../errors-after-init-opt-in-ui-state.json | 4 ++- ...s-before-init-opt-in-background-state.json | 4 ++- .../errors-before-init-opt-in-ui-state.json | 4 ++- ui/ducks/bridge/actions.ts | 10 ++++++ ui/ducks/bridge/bridge.test.ts | 18 ++++++++++ ui/ducks/bridge/selectors.test.ts | 35 +++++++++++++++++++ ui/ducks/bridge/selectors.ts | 8 +++++ ui/hooks/bridge/useBridging.test.ts | 12 ++++++- ui/hooks/bridge/useBridging.ts | 24 +++++++++---- ui/pages/bridge/index.test.tsx | 2 ++ ui/pages/bridge/index.tsx | 4 +++ 21 files changed, 177 insertions(+), 13 deletions(-) diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 831dc6c539fb..76fb2386f1f6 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -104,6 +104,8 @@ export const SENTRY_BACKGROUND_STATE = { }, destTokens: {}, destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CronjobController: { diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 221a1e1a2a00..25b6eae98c33 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -95,4 +95,28 @@ describe('BridgeController', function () { { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, ]); }); + + it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { + await bridgeController.selectSrcNetwork('0xa'); + expect(bridgeController.state.bridgeState.srcTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.srcTopAssets).toStrictEqual([ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); + }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 1bc673af43f8..841d735ac52c 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -39,6 +39,10 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectSrcNetwork`, + this.selectSrcNetwork.bind(this), + ); this.messagingSystem.registerActionHandler( `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, this.selectDestNetwork.bind(this), @@ -61,6 +65,11 @@ export default class BridgeController extends BaseController< }); }; + selectSrcNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'srcTopAssets'); + await this.#setTokens(chainId, 'srcTokens'); + }; + selectDestNetwork = async (chainId: Hex) => { await this.#setTopAssets(chainId, 'destTopAssets'); await this.#setTokens(chainId, 'destTokens'); diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index e21071d71c4d..58c7d015b7bb 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -8,6 +8,8 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }, + srcTokens: {}, + srcTopAssets: [], destTokens: {}, destTopAssets: [], }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index ddc4668b3e53..2fb36e1e983e 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -21,11 +21,14 @@ export type BridgeFeatureFlags = { export type BridgeControllerState = { bridgeFeatureFlags: BridgeFeatureFlags; + srcTokens: Record; + srcTopAssets: { address: string }[]; destTokens: Record; destTopAssets: { address: string }[]; }; export enum BridgeUserAction { + SELECT_SRC_NETWORK = 'selectSrcNetwork', SELECT_DEST_NETWORK = 'selectDestNetwork', } export enum BridgeBackgroundAction { @@ -40,6 +43,7 @@ type BridgeControllerAction = { // Maps to BridgeController function names type BridgeControllerActions = | BridgeControllerAction + | BridgeControllerAction | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 584ae7e91ad2..45083c881e1f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3891,6 +3891,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.SET_FEATURE_FLAGS}`, ), + [BridgeUserAction.SELECT_SRC_NETWORK]: this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_SRC_NETWORK}`, + ), [BridgeUserAction.SELECT_DEST_NETWORK]: this.controllerMessenger.call.bind( this.controllerMessenger, diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index d56141572d81..83b8b29a5e83 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -129,6 +129,8 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { }, destTokens: {}, destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CurrencyController: { diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index a8c80e972346..415af23071e7 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -402,6 +402,8 @@ class FixtureBuilder { }, destTokens: {}, destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }; return this; diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 596c7623208d..40bb8c6bd97f 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -120,7 +120,17 @@ const mockServer = }; }), ); - return Promise.all(featureFlagMocks); + const portfolioMock = async () => + await mockServer_ + .forGet('https://portfolio.metamask.io/bridge') + .always() + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }); + return Promise.all([...featureFlagMocks, portfolioMock]); }; export const getBridgeFixtures = ( 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 bbd833c87656..11f74e9c7511 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 @@ -67,7 +67,9 @@ "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "CronjobController": { "jobs": "object" }, 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 9812df603e92..a6f5de2d24b6 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 @@ -254,7 +254,9 @@ "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 14f0c27c5d80..f40b2687316b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -155,7 +155,9 @@ } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "SubjectMetadataController": { "subjectMetadata": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index c899811aad0f..3c692fa59405 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -164,7 +164,9 @@ } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "TransactionController": { "transactions": "object" }, diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 47912db8fd17..5bfbda1e23cf 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -42,6 +42,16 @@ export const setBridgeFeatureFlags = () => { }; // User actions +export const setFromChain = (chainId: Hex) => { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch( + callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [ + chainId, + ]), + ); + }; +}; + export const setToChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { dispatch(setToChainId_(chainId)); diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index a9eddde18081..b8d2e09eb0ea 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -16,6 +16,7 @@ import { setFromTokenInputValue, setToChain, setToToken, + setFromChain, } from './actions'; const middleware = [thunk]; @@ -100,4 +101,21 @@ describe('Ducks - Bridge', () => { expect(mockSetBridgeFeatureFlags).toHaveBeenCalledTimes(1); }); }); + + describe('setFromChain', () => { + it('calls the selectSrcNetwork background action', async () => { + const mockSelectSrcNetwork = jest.fn().mockReturnValue({}); + setBackgroundConnection({ + [BridgeUserAction.SELECT_SRC_NETWORK]: mockSelectSrcNetwork, + } as never); + + await store.dispatch(setFromChain(CHAIN_IDS.MAINNET) as never); + + expect(mockSelectSrcNetwork).toHaveBeenCalledTimes(1); + expect(mockSelectSrcNetwork).toHaveBeenCalledWith( + '0x1', + expect.anything(), + ); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 98c5264dd97d..cf27790aa943 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -12,6 +12,8 @@ import { getFromChain, getFromChains, getFromToken, + getFromTokens, + getFromTopAssets, getIsBridgeTx, getToAmount, getToChain, @@ -456,4 +458,37 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual([]); }); }); + + describe('getFromTokens', () => { + it('returns src tokens from controller state', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getFromTokens(state as never); + + expect(result).toStrictEqual({ + '0x00': { address: '0x00', symbol: 'TEST' }, + }); + }); + }); + + describe('getFromTopAssets', () => { + it('returns src top assets from controller state', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getFromTopAssets(state as never); + + expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index dd8dfa7a8999..8cd56928fc66 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -92,6 +92,14 @@ export const getToChain = createDeepEqualSelector( toChains.find(({ chainId }) => chainId === toChainId), ); +export const getFromTokens = (state: BridgeAppState) => { + return state.metamask.bridgeState.srcTokens ?? {}; +}; + +export const getFromTopAssets = (state: BridgeAppState) => { + return state.metamask.bridgeState.srcTopAssets ?? []; +}; + export const getToTopAssets = (state: BridgeAppState) => { return state.bridge.toChainId ? state.metamask.bridgeState.destTopAssets : []; }; diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index 9e2f205c28dc..df8bbb940f4e 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -15,9 +15,16 @@ jest.mock('react-router-dom', () => ({ }), })); +const mockDispatch = jest.fn().mockReturnValue(() => jest.fn()); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), - useDispatch: jest.fn().mockReturnValue(() => jest.fn()), + useDispatch: () => mockDispatch, +})); + +const mockSetFromChain = jest.fn(); +jest.mock('../../ducks/bridge/actions', () => ({ + ...jest.requireActual('../../ducks/bridge/actions'), + setFromChain: () => mockSetFromChain(), })); const MOCK_METAMETRICS_ID = '0xtestMetaMetricsId'; @@ -94,6 +101,8 @@ describe('useBridging', () => { }, }); + expect(mockDispatch.mock.calls).toHaveLength(1); + expect(nock(BRIDGE_API_BASE_URL).isDone()).toBe(true); result.current.openBridgeExperience(location, token, urlSuffix); @@ -165,6 +174,7 @@ describe('useBridging', () => { result.current.openBridgeExperience(location, token, urlSuffix); + expect(mockDispatch.mock.calls).toHaveLength(3); 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 d11aaeb821a9..ce4b8c48b89c 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -1,9 +1,11 @@ import { useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { - getCurrentChainId, + setBridgeFeatureFlags, + setFromChain, +} from '../../ducks/bridge/actions'; +import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getCurrentKeyring, getDataCollectionForMarketing, @@ -31,6 +33,7 @@ import { isHardwareKeyring } from '../../helpers/utils/hardware'; import { getPortfolioUrl } from '../../helpers/utils/portfolio'; import { setSwapsFromToken } from '../../ducks/swaps/swaps'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { getProviderConfig } from '../../ducks/metamask/metamask'; ///: END:ONLY_INCLUDE_IF const useBridging = () => { @@ -41,7 +44,7 @@ const useBridging = () => { const metaMetricsId = useSelector(getMetaMetricsId); const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); - const chainId = useSelector(getCurrentChainId); + const providerConfig = useSelector(getProviderConfig); const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = isHardwareKeyring(keyring.type); @@ -52,13 +55,20 @@ const useBridging = () => { dispatch(setBridgeFeatureFlags()); }, [dispatch, setBridgeFeatureFlags]); + useEffect(() => { + isBridgeChain && + isBridgeSupported && + providerConfig && + dispatch(setFromChain(providerConfig.chainId)); + }, []); + const openBridgeExperience = useCallback( ( location: string, token: SwapsTokenObject | SwapsEthToken, portfolioUrlSuffix?: string, ) => { - if (!isBridgeChain) { + if (!isBridgeChain || !providerConfig) { return; } @@ -70,7 +80,7 @@ const useBridging = () => { token_symbol: token.symbol, location, text: 'Bridge', - chain_id: chainId, + chain_id: providerConfig.chainId, }, }); dispatch( @@ -105,7 +115,7 @@ const useBridging = () => { location, text: 'Bridge', url: portfolioUrl, - chain_id: chainId, + chain_id: providerConfig.chainId, token_symbol: token.symbol, }, }); @@ -114,7 +124,6 @@ const useBridging = () => { [ isBridgeSupported, isBridgeChain, - chainId, setSwapsFromToken, dispatch, usingHardwareWallet, @@ -123,6 +132,7 @@ const useBridging = () => { trackEvent, isMetaMetricsEnabled, isMarketingEnabled, + providerConfig, ], ); diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 4352ff359742..a73cfa370681 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -22,6 +22,8 @@ setBackgroundConnection({ getNetworkConfigurationByNetworkClientId: jest .fn() .mockResolvedValue({ chainId: '0x1' }), + setBridgeFeatureFlags: jest.fn(), + selectSrcNetwork: jest.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index 780a19bd71f4..e4b5c0b930d4 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -28,10 +28,14 @@ import { BlockSize, } from '../../helpers/constants/design-system'; import { getIsBridgeEnabled } from '../../selectors'; +import useBridging from '../../hooks/bridge/useBridging'; import { PrepareBridgePage } from './prepare/prepare-bridge-page'; const CrossChainSwap = () => { const t = useContext(I18nContext); + + useBridging(); + const history = useHistory(); const dispatch = useDispatch(); From e354ad5b8ad3c17461f90ba2192c8f9ec19b2cb6 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 7 Oct 2024 09:46:16 +0200 Subject: [PATCH 068/226] chore: update accounts related packages (#27284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updating packages to use versions coming from the new [accounts monorepo](https://github.com/MetaMask/accounts). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27284?quickstart=1) ## **Related issues** Related to: - https://github.com/MetaMask/accounts/pull/39 - https://github.com/MetaMask/accounts/pull/50 - https://github.com/MetaMask/accounts/pull/54 - https://github.com/MetaMask/core/pull/4713 - https://github.com/MetaMask/core/pull/4734 - https://github.com/MetaMask/snap-simple-keyring/pull/156 - https://github.com/MetaMask/snap-watch-only/pull/52 - https://github.com/MetaMask/snap-bitcoin-wallet/pull/255 - https://github.com/MetaMask/snap-account-abstraction-keyring/pull/142 ## **Manual testing steps** Test parts of the extension that closely related to accounts management + HW wallets support. ## **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 | 16 +- test/data/mock-state.json | 4 +- test/e2e/constants.ts | 4 +- ...rs-after-init-opt-in-background-state.json | 4 +- .../errors-after-init-opt-in-ui-state.json | 2 + .../create-snap-redirect.test.tsx.snap | 2 +- yarn.lock | 145 ++++++------------ 7 files changed, 68 insertions(+), 109 deletions(-) diff --git a/package.json b/package.json index 6f378c0ee6a7..657c8110f09a 100644 --- a/package.json +++ b/package.json @@ -297,14 +297,14 @@ "@metamask-institutional/transaction-update": "^0.2.5", "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", - "@metamask/account-watcher": "^4.1.0", - "@metamask/accounts-controller": "^18.2.1", + "@metamask/account-watcher": "^4.1.1", + "@metamask/accounts-controller": "^18.2.2", "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", "@metamask/assets-controllers": "^37.0.0", "@metamask/base-controller": "^7.0.0", - "@metamask/bitcoin-wallet-snap": "^0.6.0", + "@metamask/bitcoin-wallet-snap": "^0.6.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", @@ -316,17 +316,17 @@ "@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.3", + "@metamask/eth-snap-keyring": "^4.3.6", "@metamask/eth-token-tracker": "^8.0.0", - "@metamask/eth-trezor-keyring": "^3.1.0", + "@metamask/eth-trezor-keyring": "^3.1.3", "@metamask/etherscan-link": "^3.0.0", "@metamask/ethjs": "^0.6.0", "@metamask/ethjs-contract": "^0.4.1", "@metamask/ethjs-query": "^0.7.1", "@metamask/gas-fee-controller": "^18.0.0", "@metamask/jazzicon": "^2.0.0", - "@metamask/keyring-api": "^8.1.0", - "@metamask/keyring-controller": "^17.2.1", + "@metamask/keyring-api": "^8.1.3", + "@metamask/keyring-controller": "^17.2.2", "@metamask/logging-controller": "^6.0.0", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^10.1.0", @@ -359,7 +359,7 @@ "@metamask/snaps-rpc-methods": "^11.1.1", "@metamask/snaps-sdk": "^6.5.1", "@metamask/snaps-utils": "^8.1.1", - "@metamask/transaction-controller": "^37.0.0", + "@metamask/transaction-controller": "^37.1.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.1.0", "@ngraveio/bc-ur": "^1.1.12", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index c2d18bcb76dc..32a61c573500 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -616,8 +616,8 @@ "developer": "Metamask", "website": "https://www.consensys.io/", "auditUrls": ["auditUrl1", "auditUrl2"], - "version": "1.0.0", - "lastUpdated": "April 20, 2023" + "version": "1.1.6", + "lastUpdated": "September 26, 2024" } }, "notifications": { diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index e7fef587f533..7e92a28cf463 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -20,7 +20,7 @@ export const BUNDLER_URL = 'http://localhost:3000/rpc'; /* URL of the 4337 account snap site. */ export const ERC_4337_ACCOUNT_SNAP_URL = - 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.1/'; + 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.2/'; /* Salt used to generate the 4337 account. */ export const ERC_4337_ACCOUNT_SALT = '0x1'; @@ -31,7 +31,7 @@ export const SIMPLE_ACCOUNT_FACTORY = /* URL of the Snap Simple Keyring site. */ export const TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL = - 'https://metamask.github.io/snap-simple-keyring/1.1.2/'; + 'https://metamask.github.io/snap-simple-keyring/1.1.6/'; /* Address of the VerifyingPaymaster smart contract deployed to Ganache. */ export const VERIFYING_PAYMASTER = '0xbdbDEc38ed168331b1F7004cc9e5392A2272C1D7'; 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 11f74e9c7511..e3efb4a9a728 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 @@ -211,6 +211,7 @@ "smartTransactionsOptInStatus": false, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, + "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", @@ -304,7 +305,8 @@ "TxController": { "methodData": "object", "transactions": "object", - "lastFetchedBlockNumbers": "object" + "lastFetchedBlockNumbers": "object", + "submitHistory": "object" }, "UserOperationController": { "userOperations": "object" }, "UserStorageController": { 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 a6f5de2d24b6..af8b816456fe 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 @@ -33,6 +33,7 @@ "smartTransactionsOptInStatus": false, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, + "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", @@ -179,6 +180,7 @@ "logs": "object", "methodData": "object", "lastFetchedBlockNumbers": "object", + "submitHistory": "object", "fiatCurrency": "usd", "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, "cryptocurrencies": ["btc"], diff --git a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap index a971dd30faba..e6bb4ba7579c 100644 --- a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap +++ b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap @@ -107,7 +107,7 @@ exports[` renders the url and message when provided and i class="mm-box mm-text mm-text--body-md mm-box--padding-2 mm-box--color-primary-default" data-testid="snap-account-redirect-url-display-box" > - https://metamask.github.io/snap-simple-keyring/1.1.2/ + https://metamask.github.io/snap-simple-keyring/1.1.6/

- - - -`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "deadline_missed" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "pending" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "reverted" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "unknown" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the component with initial props 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
-
-
- -
-
-`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap new file mode 100644 index 000000000000..f3ff42c89116 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SmartTransactionStatusPage renders the "failed" STX status: smart-transaction-status-failed 1`] = ` +
+
+
+
+
+

+ Your transaction failed +

+
+

+ Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. +

+
+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "pending" STX status: smart-transaction-status-pending 1`] = ` +
+
+
+
+
+

+ Your transaction was submitted +

+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "success" STX status: smart-transaction-status-success 1`] = ` +
+
+
+
+
+

+ Your transaction is complete +

+
+ +
+
+
+ +
+
+`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss index 5e74ba9a8b3d..2227673029d8 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss +++ b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss @@ -1,10 +1,3 @@ - -@keyframes shift { - to { - background-position: 100% 0; - } -} - .smart-transaction-status-page { text-align: center; @@ -20,24 +13,6 @@ } } - &__loading-bar-container { - @media screen and (min-width: 768px) { - max-width: 260px; - } - - width: 100%; - height: 3px; - background: var(--color-background-alternative); - display: flex; - margin-top: 16px; - } - - &__loading-bar { - height: 3px; - background: var(--color-primary-default); - transition: width 0.5s linear; - } - &__footer { grid-area: footer; } @@ -45,35 +20,4 @@ &__countdown { width: 25px; } - - // Slightly overwrite the default SimulationDetails layout to look better on the Smart Transaction status page. - .simulation-details-layout { - margin-left: 0; - margin-right: 0; - width: 100%; - text-align: left; - } - - &__background-animation { - position: relative; - left: -88px; - background-repeat: repeat; - background-position: 0 0; - - &--top { - width: 1634px; - height: 54px; - background-size: 817px 54px; - background-image: url('/images/transaction-background-top.svg'); - animation: shift 19s linear infinite; - } - - &--bottom { - width: 1600px; - height: 62px; - background-size: 800px 62px; - background-image: url('/images/transaction-background-bottom.svg'); - animation: shift 22s linear infinite; - } - } } diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx new file mode 100644 index 000000000000..fa4166af1461 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +// Declare a variable to store the onComplete callback +let mockOnComplete: () => void; + +// Modify the existing jest.mock to capture the onComplete callback +jest.mock('../../../components/component-library/lottie-animation', () => ({ + LottieAnimation: ({ + path, + loop, + autoplay, + onComplete, + }: { + path: string; + loop: boolean; + autoplay: boolean; + onComplete: () => void; + }) => { + // Store the onComplete callback for later use in tests + mockOnComplete = onComplete; + return ( +
+ ); + }, +})); + +describe('SmartTransactionsStatusAnimation', () => { + it('renders correctly for PENDING status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for SUCCESS status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('confirmed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for REVERTED status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for UNKNOWN status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for other statuses', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('processing'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); + + it('transitions from submittingIntro to submittingLoop when onComplete is called', () => { + render( + , + ); + const lottieAnimation = screen.getByTestId('mock-lottie-animation'); + + // Initially, should render 'submitting-intro' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + + // Trigger the onComplete callback to simulate animation completion + expect(lottieAnimation.getAttribute('data-on-complete')).toBeDefined(); + act(() => { + mockOnComplete(); + }); + + // After onComplete is called, it should transition to 'submitting-loop' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-loop'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); +}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx new file mode 100644 index 000000000000..3dc739aefa1f --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx @@ -0,0 +1,80 @@ +import React, { useState, useCallback } from 'react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { Box } from '../../../components/component-library'; +import { Display } from '../../../helpers/constants/design-system'; +import { LottieAnimation } from '../../../components/component-library/lottie-animation'; + +const ANIMATIONS_FOLDER = 'images/animations/smart-transaction-status'; + +type AnimationInfo = { + path: string; + loop: boolean; +}; + +const Animations: Record = { + Failed: { + path: `${ANIMATIONS_FOLDER}/failed.lottie.json`, + loop: false, + }, + Confirmed: { + path: `${ANIMATIONS_FOLDER}/confirmed.lottie.json`, + loop: false, + }, + SubmittingIntro: { + path: `${ANIMATIONS_FOLDER}/submitting-intro.lottie.json`, + loop: false, + }, + SubmittingLoop: { + path: `${ANIMATIONS_FOLDER}/submitting-loop.lottie.json`, + loop: true, + }, + Processing: { + path: `${ANIMATIONS_FOLDER}/processing.lottie.json`, + loop: true, + }, +}; + +export const SmartTransactionStatusAnimation = ({ + status, +}: { + status: SmartTransactionStatuses; +}) => { + const [isIntro, setIsIntro] = useState(true); + + let animation: AnimationInfo; + + if (status === SmartTransactionStatuses.PENDING) { + animation = isIntro + ? Animations.SubmittingIntro + : Animations.SubmittingLoop; + } else { + switch (status) { + case SmartTransactionStatuses.SUCCESS: + animation = Animations.Confirmed; + break; + case SmartTransactionStatuses.REVERTED: + case SmartTransactionStatuses.UNKNOWN: + animation = Animations.Failed; + break; + default: + animation = Animations.Processing; + } + } + + const handleAnimationComplete = useCallback(() => { + if (status === SmartTransactionStatuses.PENDING && isIntro) { + setIsIntro(false); + } + }, [status, isIntro]); + + return ( + + + + ); +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx new file mode 100644 index 000000000000..12d356ce4cc4 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import SmartTransactionStatusPage from './smart-transaction-status-page'; +import { Meta, StoryObj } from '@storybook/react'; +import { SimulationData } from '@metamask/transaction-controller'; +import { mockNetworkState } from '../../../../test/stub/networks'; + +// Mock data +const CHAIN_ID_MOCK = '0x1'; + +const simulationData: SimulationData = { + nativeBalanceChange: { + previousBalance: '0x0', + newBalance: '0x0', + difference: '0x12345678912345678', + isDecrease: true, + }, + tokenBalanceChanges: [], +}; + +const TX_MOCK = { + id: 'txId', + simulationData, + chainId: CHAIN_ID_MOCK, +}; + +const storeMock = configureStore({ + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: false, + }, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + transactions: [TX_MOCK], + currentNetworkTxList: [TX_MOCK], + }, +}); + +const meta: Meta = { + title: 'Pages/SmartTransactions/SmartTransactionStatusPage', + component: SmartTransactionStatusPage, + decorators: [(story) => {story()}], +}; + +export default meta; +type Story = StoryObj; + +export const Pending: Story = { + args: { + requestState: { + smartTransaction: { + status: 'pending', + creationTime: Date.now(), + uuid: 'uuid', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Success: Story = { + args: { + requestState: { + smartTransaction: { + status: 'success', + creationTime: Date.now() - 60000, // 1 minute ago + uuid: 'uuid-success', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-success', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Failed: Story = { + args: { + requestState: { + smartTransaction: { + status: 'unknown', + creationTime: Date.now() - 180000, // 3 minutes ago + uuid: 'uuid-failed', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-failed', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx index 2eb29bfa4e4e..4492ed4e4844 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { SmartTransactionStatuses, @@ -8,9 +8,7 @@ import { import { Box, Text, - Icon, IconName, - IconSize, Button, ButtonVariant, ButtonSecondary, @@ -26,22 +24,18 @@ import { TextColor, FontWeight, IconColor, - TextAlign, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, getFullTxData } from '../../../selectors'; -import { getFeatureFlagsByChainId } from '../../../../shared/modules/selectors'; import { BaseUrl } from '../../../../shared/constants/urls'; -import { - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE, - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE, -} from '../../../../shared/constants/smartTransactions'; import { hideLoadingIndication } from '../../../store/actions'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { SimulationDetails } from '../../confirmations/components/simulation-details'; import { NOTIFICATION_WIDTH } from '../../../../shared/constants/notifications'; -type RequestState = { +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +export type RequestState = { smartTransaction?: SmartTransaction; isDapp: boolean; txId?: string; @@ -49,8 +43,8 @@ type RequestState = { export type SmartTransactionStatusPageProps = { requestState: RequestState; - onCloseExtension: () => void; - onViewActivity: () => void; + onCloseExtension?: () => void; + onViewActivity?: () => void; }; export const showRemainingTimeInMinAndSec = ( @@ -66,30 +60,18 @@ export const showRemainingTimeInMinAndSec = ( const getDisplayValues = ({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }: { t: ReturnType; - countdown: JSX.Element | undefined; isSmartTransactionPending: boolean; - isSmartTransactionTakingTooLong: boolean; isSmartTransactionSuccess: boolean; isSmartTransactionCancelled: boolean; }) => { - if (isSmartTransactionPending && isSmartTransactionTakingTooLong) { - return { - title: t('smartTransactionTakingTooLong'), - description: t('smartTransactionTakingTooLongDescription', [countdown]), - iconName: IconName.Clock, - iconColor: IconColor.primaryDefault, - }; - } else if (isSmartTransactionPending) { + if (isSmartTransactionPending) { return { title: t('smartTransactionPending'), - description: t('stxEstimatedCompletion', [countdown]), iconName: IconName.Clock, iconColor: IconColor.primaryDefault, }; @@ -102,7 +84,7 @@ const getDisplayValues = ({ } else if (isSmartTransactionCancelled) { return { title: t('smartTransactionCancelled'), - description: t('smartTransactionCancelledDescription', [countdown]), + description: t('smartTransactionCancelledDescription'), iconName: IconName.Danger, iconColor: IconColor.errorDefault, }; @@ -116,98 +98,6 @@ const getDisplayValues = ({ }; }; -const useRemainingTime = ({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, -}: { - isSmartTransactionPending: boolean; - smartTransaction?: SmartTransaction; - stxMaxDeadline: number; - stxEstimatedDeadline: number; -}) => { - const [timeLeftForPendingStxInSec, setTimeLeftForPendingStxInSec] = - useState(0); - const [isSmartTransactionTakingTooLong, setIsSmartTransactionTakingTooLong] = - useState(false); - const stxDeadline = isSmartTransactionTakingTooLong - ? stxMaxDeadline - : stxEstimatedDeadline; - - useEffect(() => { - if (!isSmartTransactionPending) { - return; - } - - const calculateRemainingTime = () => { - const secondsAfterStxSubmission = smartTransaction?.creationTime - ? Math.round((Date.now() - smartTransaction.creationTime) / 1000) - : 0; - - if (secondsAfterStxSubmission > stxDeadline) { - setTimeLeftForPendingStxInSec(0); - if (!isSmartTransactionTakingTooLong) { - setIsSmartTransactionTakingTooLong(true); - } - return; - } - - setTimeLeftForPendingStxInSec(stxDeadline - secondsAfterStxSubmission); - }; - - const intervalId = setInterval(calculateRemainingTime, 1000); - calculateRemainingTime(); - - // eslint-disable-next-line consistent-return - return () => clearInterval(intervalId); - }, [ - isSmartTransactionPending, - isSmartTransactionTakingTooLong, - smartTransaction?.creationTime, - stxDeadline, - ]); - - return { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - }; -}; - -const Deadline = ({ - isSmartTransactionPending, - stxDeadline, - timeLeftForPendingStxInSec, -}: { - isSmartTransactionPending: boolean; - stxDeadline: number; - timeLeftForPendingStxInSec: number; -}) => { - if (!isSmartTransactionPending) { - return null; - } - return ( - -
-
-
- - ); -}; - const Description = ({ description }: { description: string | undefined }) => { if (!description) { return null; @@ -388,29 +278,10 @@ const Title = ({ title }: { title: string }) => { ); }; -const SmartTransactionsStatusIcon = ({ - iconName, - iconColor, -}: { - iconName: IconName; - iconColor: IconColor; -}) => { - return ( - - - - ); -}; - export const SmartTransactionStatusPage = ({ requestState, - onCloseExtension, - onViewActivity, + onCloseExtension = () => null, + onViewActivity = () => null, }: SmartTransactionStatusPageProps) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -423,50 +294,15 @@ export const SmartTransactionStatusPage = ({ const isSmartTransactionCancelled = Boolean( smartTransaction?.status?.startsWith(SmartTransactionStatuses.CANCELLED), ); - const featureFlags: { - smartTransactions?: { - expectedDeadline?: number; - maxDeadline?: number; - }; - } | null = useSelector(getFeatureFlagsByChainId); - const stxEstimatedDeadline = - featureFlags?.smartTransactions?.expectedDeadline || - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE; - const stxMaxDeadline = - featureFlags?.smartTransactions?.maxDeadline || - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE; - const { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - } = useRemainingTime({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, - }); + const chainId: string = useSelector(getCurrentChainId); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: This same selector is used in the awaiting-swap component. const fullTxData = useSelector((state) => getFullTxData(state, txId)) || {}; - const countdown = isSmartTransactionPending ? ( - - {showRemainingTimeInMinAndSec(timeLeftForPendingStxInSec)} - - ) : undefined; - - const { title, description, iconName, iconColor } = getDisplayValues({ + const { title, description } = getDisplayValues({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }); @@ -515,20 +351,10 @@ export const SmartTransactionStatusPage = ({ paddingRight={6} width={BlockSize.Full} > - - - <Deadline - isSmartTransactionPending={isSmartTransactionPending} - stxDeadline={stxDeadline} - timeLeftForPendingStxInSec={timeLeftForPendingStxInSec} - /> <Description description={description} /> <PortfolioSmartTransactionStatusUrl portfolioSmartTransactionStatusUrl={ @@ -539,15 +365,13 @@ export const SmartTransactionStatusPage = ({ /> </Box> {canShowSimulationDetails && ( - <SimulationDetails - simulationData={fullTxData.simulationData} - transactionId={fullTxData.id} - /> + <Box width={BlockSize.Full}> + <SimulationDetails + simulationData={fullTxData.simulationData} + transactionId={fullTxData.id} + /> + </Box> )} - <Box - marginTop={3} - className="smart-transaction-status-page__background-animation smart-transaction-status-page__background-animation--bottom" - /> </Box> <SmartTransactionsStatusPageFooter isDapp={isDapp} diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js deleted file mode 100644 index d014c56373a4..000000000000 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; - -import { - renderWithProvider, - createSwapsMockStore, -} from '../../../../test/jest'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { SmartTransactionStatusPage } from '.'; - -const middleware = [thunk]; - -describe('SmartTransactionStatusPage', () => { - const requestState = { - smartTransaction: { - status: SmartTransactionStatuses.PENDING, - creationTime: Date.now(), - }, - }; - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Submitting your transaction')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "Sorry for the wait" pending status', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const newRequestState = { - ...requestState, - smartTransaction: { - ...requestState.smartTransaction, - creationTime: 1519211809934, - }, - }; - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={newRequestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('Sorry for the wait')).toBeInTheDocument(); - expect(queryByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction is complete')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "reverted" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.REVERTED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect( - getByText( - 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', - ), - ).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - requestState.smartTransaction = latestSmartTransaction; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect( - getByText( - `Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees.`, - ), - ).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "deadline_missed" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = - SmartTransactionStatuses.CANCELLED_DEADLINE_MISSED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "unknown" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.UNKNOWN; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "pending" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.PENDING; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(queryByText('View activity')).not.toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx new file mode 100644 index 000000000000..afd9b2872ce1 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { + SmartTransaction, + SmartTransactionStatuses, +} from '@metamask/smart-transactions-controller/dist/types'; + +import { fireEvent } from '@testing-library/react'; +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../test/jest'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { + SmartTransactionStatusPage, + RequestState, +} from './smart-transaction-status-page'; + +// Mock the SmartTransactionStatusAnimation component and capture props +jest.mock('./smart-transaction-status-animation', () => ({ + SmartTransactionStatusAnimation: ({ + status, + }: { + status: SmartTransactionStatuses; + }) => <div data-testid="mock-animation" data-status={status} />, +})); + +const middleware = [thunk]; +const mockStore = configureMockStore(middleware); + +const defaultRequestState: RequestState = { + smartTransaction: { + status: SmartTransactionStatuses.PENDING, + creationTime: Date.now(), + uuid: 'uuid', + chainId: CHAIN_IDS.MAINNET, + }, + isDapp: false, + txId: 'txId', +}; + +describe('SmartTransactionStatusPage', () => { + const statusTestCases = [ + { + status: SmartTransactionStatuses.PENDING, + isDapp: false, + expectedTexts: ['Your transaction was submitted', 'View activity'], + snapshotName: 'pending', + }, + { + status: SmartTransactionStatuses.SUCCESS, + isDapp: false, + expectedTexts: [ + 'Your transaction is complete', + 'View transaction', + 'View activity', + ], + snapshotName: 'success', + }, + { + status: SmartTransactionStatuses.REVERTED, + isDapp: false, + expectedTexts: [ + 'Your transaction failed', + 'View transaction', + 'View activity', + 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', + ], + snapshotName: 'failed', + }, + ]; + + statusTestCases.forEach(({ status, isDapp, expectedTexts, snapshotName }) => { + it(`renders the "${snapshotName}" STX status${ + isDapp ? ' for a dapp transaction' : '' + }`, () => { + const state = createSwapsMockStore(); + const latestSmartTransaction = + state.metamask.smartTransactionsState.smartTransactions[ + CHAIN_IDS.MAINNET + ][1]; + latestSmartTransaction.status = status; + const requestState: RequestState = { + smartTransaction: latestSmartTransaction as SmartTransaction, + isDapp, + txId: 'txId', + }; + + const { getByText, getByTestId, container } = renderWithProvider( + <SmartTransactionStatusPage requestState={requestState} />, + mockStore(state), + ); + + expectedTexts.forEach((text) => { + expect(getByText(text)).toBeInTheDocument(); + }); + + expect(getByTestId('mock-animation')).toBeInTheDocument(); + expect(getByTestId('mock-animation')).toHaveAttribute( + 'data-status', + status, + ); + expect(container).toMatchSnapshot( + `smart-transaction-status-${snapshotName}`, + ); + }); + }); + + describe('Action Buttons', () => { + it('calls onCloseExtension when Close extension button is clicked', () => { + const onCloseExtension = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: true }} + onCloseExtension={onCloseExtension} + />, + store, + ); + + const closeButton = getByText('Close extension'); + fireEvent.click(closeButton); + expect(onCloseExtension).toHaveBeenCalled(); + }); + + it('calls onViewActivity when View activity button is clicked', () => { + const onViewActivity = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: false }} + onViewActivity={onViewActivity} + />, + store, + ); + + const viewActivityButton = getByText('View activity'); + fireEvent.click(viewActivityButton); + expect(onViewActivity).toHaveBeenCalled(); + }); + }); +}); From 11ca25b78635455023ab23a2fc1e3544e6284cbc Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:51:37 -0400 Subject: [PATCH 080/226] feat: Adding delete metametrics data to security and privacy tab (#24571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> **This PR is dependant on #24503** ## **Description** - Added a new functional component as an entry to the Security & Privacy tab with the `Delete MetaMetrics Data` button. - A new Delete MetaMetrics Data model will open when you click the button. - Clicking the `Clear` button in the modal will create a data deletion regulation, update the state, and close the modal, deactivating the `Delete MetaMetrics Data` button. - The Erroring on the `Clear` button click opens a new error modal. **Scenarios to disable the DeleteMetaMetrics button:** 1. Metametrics ID not created / not available 2. Just performed a deletion independent on participate in metametrics toggle 3. Participate in metric opt-out & no data is recorded after deletion. 4. Status of current delete regulation as INITIALIZED, RUNNING, or FINISHED and (Participate in metric opt-out/no data recorded after deletion) <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24571?quickstart=1) ## **Related issues** Fixes #24406, #24407, https://github.com/MetaMask/MetaMask-planning/issues/2523 ## **Manual testing steps** Perquisite: Provide the following details in the `.metamaskrc` file: ``` ANALYTICS_DATA_DELETION_SOURCE_ID="wygFTooEUUtcckty9kaMc" ANALYTICS_DATA_DELETION_ENDPOINT="https://proxy.dev-api.cx.metamask.io/segment/v1" ``` 1. Make a build(`yarn`, `yarn dist`) against the code. 2. Load the extension in any browser. 3. Navigate to the "Security & privacy" in the Settings 4. Click on the "Delete MetaMetrics data" button which enables when the "Participate in MetaMetrics" is selected. 5. Validate the post request is made in the service worker with the id - `wygFTooEUUtcckty9kaMc`. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask 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: seaona <54408225+seaona@users.noreply.github.com> --- app/_locales/en/messages.json | 26 ++ privacy-snapshot.json | 2 + shared/constants/metametrics.ts | 2 + .../metrics/delete-metametrics-data.spec.ts | 246 ++++++++++++++++++ test/e2e/webdriver/driver.js | 2 + test/e2e/webdriver/types.ts | 5 + .../clear-metametrics-data.test.tsx | 59 +++++ .../clear-metametrics-data.tsx | 130 +++++++++ .../app/clear-metametrics-data/index.ts | 1 + .../data-deletion-error-modal.test.tsx | 51 ++++ .../data-deletion-error-modal.tsx | 99 +++++++ .../app/data-deletion-error-modal/index.ts | 1 + ui/ducks/app/app.test.js | 38 +++ ui/ducks/app/app.ts | 48 ++++ ui/helpers/constants/settings.js | 7 + ui/helpers/utils/settings-search.test.js | 2 +- .../__snapshots__/security-tab.test.js.snap | 53 ++++ .../delete-metametrics-data-button.test.tsx | 212 +++++++++++++++ .../delete-metametrics-data-button.tsx | 147 +++++++++++ .../delete-metametrics-data-button/index.ts | 1 + .../security-tab/security-tab.component.js | 13 +- .../security-tab/security-tab.container.js | 6 + .../security-tab/security-tab.test.js | 26 ++ ui/selectors/metametrics.js | 3 + ui/selectors/metametrics.test.js | 12 + ui/selectors/selectors.js | 20 ++ ui/selectors/selectors.test.js | 62 +++++ ui/store/actionConstants.ts | 8 + 28 files changed, 1278 insertions(+), 4 deletions(-) create mode 100644 test/e2e/tests/metrics/delete-metametrics-data.spec.ts create mode 100644 test/e2e/webdriver/types.ts create mode 100644 ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx create mode 100644 ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx create mode 100644 ui/components/app/clear-metametrics-data/index.ts create mode 100644 ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx create mode 100644 ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx create mode 100644 ui/components/app/data-deletion-error-modal/index.ts create mode 100644 ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx create mode 100644 ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx create mode 100644 ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 9a94edeb7edf..de17cf4ea877 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1569,6 +1569,29 @@ "deleteContact": { "message": "Delete contact" }, + "deleteMetaMetricsData": { + "message": "Delete MetaMetrics data" + }, + "deleteMetaMetricsDataDescription": { + "message": "This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "This request can't be completed right now due to an analytics system server issue, please try again later" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "We are unable to delete this data right now" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "We are about to remove all your MetaMetrics data. Are you sure?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Delete MetaMetrics data?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "You initiated this action on $1. This process can take up to 30 days. View the $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "If you delete this network, you will need to add it again to view your assets in this network" }, @@ -2873,6 +2896,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "The connection status button shows if the website you’re visiting is connected to your currently selected account." }, + "metaMetricsIdNotAvailableError": { + "message": "Since you've never opted into MetaMetrics, there's no data to delete here." + }, "metadataModalSourceTooltip": { "message": "$1 is hosted on npm and $2 is this Snap’s unique identifier.", "description": "$1 is the snap name and $2 is the snap NPM id." diff --git a/privacy-snapshot.json b/privacy-snapshot.json index b8920724a597..2516654f1803 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -33,6 +33,7 @@ "mainnet.infura.io", "metamask.eth", "metamask.github.io", + "metametrics.metamask.test", "min-api.cryptocompare.com", "nft.api.cx.metamask.io", "oidc.api.cx.metamask.io", @@ -42,6 +43,7 @@ "portfolio.metamask.io", "price.api.cx.metamask.io", "proxy.api.cx.metamask.io", + "proxy.dev-api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", "responsive-rpc.test", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 945af5416057..d0f1cfb87cbe 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -536,6 +536,7 @@ export enum MetaMetricsEventName { EncryptionPublicKeyApproved = 'Encryption Approved', EncryptionPublicKeyRejected = 'Encryption Rejected', EncryptionPublicKeyRequested = 'Encryption Requested', + ErrorOccured = 'Error occured', ExternalLinkClicked = 'External Link Clicked', KeyExportSelected = 'Key Export Selected', KeyExportRequested = 'Key Export Requested', @@ -552,6 +553,7 @@ export enum MetaMetricsEventName { MarkAllNotificationsRead = 'Notifications Marked All as Read', MetricsOptIn = 'Metrics Opt In', MetricsOptOut = 'Metrics Opt Out', + MetricsDataDeletionRequest = 'Delete MetaMetrics Data Request Submitted', NavAccountMenuOpened = 'Account Menu Opened', NavConnectedSitesOpened = 'Connected Sites Opened', NavMainMenuOpened = 'Main Menu Opened', diff --git a/test/e2e/tests/metrics/delete-metametrics-data.spec.ts b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts new file mode 100644 index 000000000000..308ff8508d0a --- /dev/null +++ b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts @@ -0,0 +1,246 @@ +import { strict as assert } from 'assert'; +import { MockedEndpoint, Mockttp } from 'mockttp'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + withFixtures, + getEventPayloads, + unlockWallet, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { TestSuiteArguments } from '../confirmations/transactions/shared'; +import { WebElementWithWaitForElementState } from '../../webdriver/types'; + +const selectors = { + accountOptionsMenuButton: '[data-testid="account-options-menu-button"]', + globalMenuSettingsButton: '[data-testid="global-menu-settings"]', + securityAndPrivacySettings: { text: 'Security & privacy', tag: 'div' }, + experimentalSettings: { text: 'Experimental', tag: 'div' }, + deletMetaMetricsSettings: '[data-testid="delete-metametrics-data-button"]', + deleteMetaMetricsDataButton: { + text: 'Delete MetaMetrics data', + tag: 'button', + }, + clearButton: { text: 'Clear', tag: 'button' }, + backButton: '[data-testid="settings-back-button"]', +}; + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Permissions Requested' and 'Permissions Received'. Do not use the constants + * from the metrics constants files, because if these change we want a strong + * indicator to our data team that the shape of data will change. + * + * @param mockServer + * @returns + */ +const mockSegment = async (mockServer: Mockttp) => { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { type: 'track', event: 'Delete MetaMetrics Data Request Submitted' }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://metametrics.metamask.test/regulations/sources/test') + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .withBodyIncluding( + JSON.stringify({ + regulationType: 'DELETE_ONLY', + subjectType: 'USER_ID', + subjectIds: ['fake-metrics-id'], + }), + ) + .thenCallback(() => ({ + statusCode: 200, + json: { data: { regulateId: 'fake-delete-regulation-id' } }, + })), + await mockServer + .forGet( + 'https://metametrics.metamask.test/regulations/fake-delete-regulation-id', + ) + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .thenCallback(() => ({ + statusCode: 200, + json: { + data: { + regulation: { + overallStatus: 'FINISHED', + }, + }, + }, + })), + ]; +}; +/** + * Scenarios: + * 1. Deletion while Metrics is Opted in. + * 2. Deletion while Metrics is Opted out. + * 3. Deletion when user never opted for metrics. + */ +describe('Delete MetaMetrics Data @no-mmi', function (this: Suite) { + it('while user has opted in for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 3); + assert.deepStrictEqual(events[0].properties, { + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = + await driver.findClickableElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButtonRefreshed.isEnabled(), + true, + 'Delete MetaMetrics data button is enabled', + ); + }, + ); + }); + it('while user has opted out for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 2); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButtonRefreshed as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + }, + ); + }); + it('when the user has never opted in for metrics', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + await driver.findElement(selectors.deletMetaMetricsSettings); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButton.isEnabled(), + false, + 'Delete MetaMetrics data button is disabled', + ); + }, + ); + }); +}); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 97c616d01f4b..a03a0d1cbd04 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -63,6 +63,8 @@ function wrapElementWithAPI(element, driver) { return await driver.wait(until.stalenessOf(element), timeout); case 'visible': return await driver.wait(until.elementIsVisible(element), timeout); + case 'disabled': + return await driver.wait(until.elementIsDisabled(element), timeout); default: throw new Error(`Provided state: '${state}' is not supported`); } diff --git a/test/e2e/webdriver/types.ts b/test/e2e/webdriver/types.ts new file mode 100644 index 000000000000..68cfa15dd600 --- /dev/null +++ b/test/e2e/webdriver/types.ts @@ -0,0 +1,5 @@ +import { WebElement, WebElementPromise } from 'selenium-webdriver'; + +export type WebElementWithWaitForElementState = WebElement & { + waitForElementState: (state: unknown, timeout?: unknown) => WebElementPromise; +}; diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx new file mode 100644 index 000000000000..5ce4ac7573dc --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import * as Actions from '../../../store/actions'; +import { DELETE_METAMETRICS_DATA_MODAL_CLOSE } from '../../../store/actionConstants'; +import ClearMetaMetricsData from './clear-metametrics-data'; + +const mockCloseDeleteMetaMetricsDataModal = jest.fn().mockImplementation(() => { + return { + type: DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +}); + +jest.mock('../../../store/actions', () => ({ + createMetaMetricsDataDeletionTask: jest.fn(), +})); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDeleteMetaMetricsDataModal: () => { + return mockCloseDeleteMetaMetricsDataModal(); + }, + }; +}); + +describe('ClearMetaMetricsData', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<ClearMetaMetricsData />, store); + + expect(getByText('Delete MetaMetrics data?')).toBeInTheDocument(); + expect( + getByText( + 'We are about to remove all your MetaMetrics data. Are you sure?', + ), + ).toBeInTheDocument(); + }); + + it('should call createMetaMetricsDataDeletionTask when Clear button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<ClearMetaMetricsData />, store); + expect(getByText('Clear')).toBeEnabled(); + fireEvent.click(getByText('Clear')); + expect(Actions.createMetaMetricsDataDeletionTask).toHaveBeenCalledTimes(1); + }); + + it('should call hideDeleteMetaMetricsDataModal when Cancel button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<ClearMetaMetricsData />, store); + expect(getByText('Cancel')).toBeEnabled(); + fireEvent.click(getByText('Cancel')); + expect(mockCloseDeleteMetaMetricsDataModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx new file mode 100644 index 000000000000..019c115eceac --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx @@ -0,0 +1,130 @@ +import React, { useContext } from 'react'; +import { useDispatch } from 'react-redux'; +import { + hideDeleteMetaMetricsDataModal, + openDataDeletionErrorModal, +} from '../../../ducks/app/app'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + Box, + Button, + ButtonSize, + ButtonVariant, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from '../../component-library'; +import { + AlignItems, + BlockSize, + Display, + FlexDirection, + JustifyContent, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { createMetaMetricsDataDeletionTask } from '../../../store/actions'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; + +export default function ClearMetaMetricsData() { + const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + + const closeModal = () => { + dispatch(hideDeleteMetaMetricsDataModal()); + }; + + const deleteMetaMetricsData = async () => { + try { + await createMetaMetricsDataDeletionTask(); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.MetricsDataDeletionRequest, + }, + { + excludeMetaMetricsId: true, + }, + ); + } catch (error: unknown) { + dispatch(openDataDeletionErrorModal()); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.ErrorOccured, + }, + { + excludeMetaMetricsId: true, + }, + ); + } finally { + dispatch(hideDeleteMetaMetricsDataModal()); + } + }; + + return ( + <Modal isOpen onClose={closeModal}> + <ModalOverlay /> + <ModalContent + modalDialogProps={{ + display: Display.Flex, + flexDirection: FlexDirection.Column, + }} + > + <ModalHeader onClose={closeModal}> + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + > + <Text variant={TextVariant.headingSm}> + {t('deleteMetaMetricsDataModalTitle')} + </Text> + </Box> + </ModalHeader> + <Box + marginLeft={4} + marginRight={4} + marginBottom={3} + display={Display.Flex} + flexDirection={FlexDirection.Column} + gap={4} + > + <Text variant={TextVariant.bodySmMedium}> + {t('deleteMetaMetricsDataModalDesc')} + </Text> + </Box> + <ModalFooter> + <Box display={Display.Flex} gap={4}> + <Button + size={ButtonSize.Lg} + width={BlockSize.Half} + variant={ButtonVariant.Secondary} + onClick={closeModal} + > + {t('cancel')} + </Button> + <Button + data-testid="clear-metametrics-data" + size={ButtonSize.Lg} + width={BlockSize.Half} + variant={ButtonVariant.Primary} + onClick={deleteMetaMetricsData} + danger + > + {t('clear')} + </Button> + </Box> + </ModalFooter> + </ModalContent> + </Modal> + ); +} diff --git a/ui/components/app/clear-metametrics-data/index.ts b/ui/components/app/clear-metametrics-data/index.ts new file mode 100644 index 000000000000..b29aee18d564 --- /dev/null +++ b/ui/components/app/clear-metametrics-data/index.ts @@ -0,0 +1 @@ +export { default } from './clear-metametrics-data'; diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx new file mode 100644 index 000000000000..cbb541f5648e --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { DATA_DELETION_ERROR_MODAL_CLOSE } from '../../../store/actionConstants'; + +import DataDeletionErrorModal from './data-deletion-error-modal'; + +const mockCloseDeleteMetaMetricsErrorModal = jest + .fn() + .mockImplementation(() => { + return { + type: DATA_DELETION_ERROR_MODAL_CLOSE, + }; + }); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDataDeletionErrorModal: () => { + return mockCloseDeleteMetaMetricsErrorModal(); + }, + }; +}); + +describe('DataDeletionErrorModal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<DataDeletionErrorModal />, store); + + expect( + getByText('We are unable to delete this data right now'), + ).toBeInTheDocument(); + expect( + getByText( + "This request can't be completed right now due to an analytics system server issue, please try again later", + ), + ).toBeInTheDocument(); + }); + + it('should call hideDeleteMetaMetricsDataModal when Ok button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<DataDeletionErrorModal />, store); + expect(getByText('Ok')).toBeEnabled(); + fireEvent.click(getByText('Ok')); + expect(mockCloseDeleteMetaMetricsErrorModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx new file mode 100644 index 000000000000..0b6be4fa782b --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + Display, + FlexDirection, + AlignItems, + JustifyContent, + TextVariant, + BlockSize, + IconColor, + TextAlign, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + ModalOverlay, + ModalContent, + ModalHeader, + Modal, + Box, + Text, + ModalFooter, + Button, + IconName, + ButtonVariant, + Icon, + IconSize, + ButtonSize, +} from '../../component-library'; +import { hideDataDeletionErrorModal } from '../../../ducks/app/app'; + +export default function DataDeletionErrorModal() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + function closeModal() { + dispatch(hideDataDeletionErrorModal()); + } + + return ( + <Modal onClose={closeModal} isOpen> + <ModalOverlay /> + <ModalContent + modalDialogProps={{ + display: Display.Flex, + flexDirection: FlexDirection.Column, + }} + > + <ModalHeader + paddingBottom={4} + paddingRight={6} + paddingLeft={6} + onClose={closeModal} + > + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + gap={4} + > + <Icon + size={IconSize.Xl} + name={IconName.Danger} + color={IconColor.warningDefault} + /> + <Text variant={TextVariant.headingSm} textAlign={TextAlign.Center}> + {t('deleteMetaMetricsDataErrorTitle')} + </Text> + </Box> + </ModalHeader> + + <Box + paddingLeft={6} + paddingRight={6} + display={Display.Flex} + gap={4} + flexDirection={FlexDirection.Column} + > + <Text variant={TextVariant.bodySm} textAlign={TextAlign.Justify}> + {t('deleteMetaMetricsDataErrorDesc')} + </Text> + </Box> + + <ModalFooter> + <Box display={Display.Flex} gap={4}> + <Button + size={ButtonSize.Lg} + width={BlockSize.Full} + variant={ButtonVariant.Primary} + onClick={closeModal} + > + {t('ok')} + </Button> + </Box> + </ModalFooter> + </ModalContent> + </Modal> + ); +} diff --git a/ui/components/app/data-deletion-error-modal/index.ts b/ui/components/app/data-deletion-error-modal/index.ts new file mode 100644 index 000000000000..383efd7029b5 --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/index.ts @@ -0,0 +1 @@ +export { default } from './data-deletion-error-modal'; diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 0d6441454f90..9a7a93ea958b 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -301,4 +301,42 @@ describe('App State', () => { }); expect(state.smartTransactionsError).toStrictEqual('Server Side Error'); }); + it('shows delete metametrics modal', () => { + const state = reduceApp(metamaskState, { + type: actions.DELETE_METAMETRICS_DATA_MODAL_OPEN, + }); + + expect(state.showDeleteMetaMetricsDataModal).toStrictEqual(true); + }); + it('hides delete metametrics modal', () => { + const deleteMetaMetricsDataModalState = { + showDeleteMetaMetricsDataModal: true, + }; + const oldState = { ...metamaskState, ...deleteMetaMetricsDataModalState }; + + const state = reduceApp(oldState, { + type: actions.DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }); + + expect(state.showDeleteMetaMetricsDataModal).toStrictEqual(false); + }); + it('shows delete metametrics error modal', () => { + const state = reduceApp(metamaskState, { + type: actions.DATA_DELETION_ERROR_MODAL_OPEN, + }); + + expect(state.showDataDeletionErrorModal).toStrictEqual(true); + }); + it('hides delete metametrics error modal', () => { + const deleteMetaMetricsErrorModalState = { + showDataDeletionErrorModal: true, + }; + const oldState = { ...metamaskState, ...deleteMetaMetricsErrorModalState }; + + const state = reduceApp(oldState, { + type: actions.DATA_DELETION_ERROR_MODAL_CLOSE, + }); + + expect(state.showDataDeletionErrorModal).toStrictEqual(false); + }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index 182ba426a3d7..e6a7855ce7a5 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -100,6 +100,8 @@ type AppState = { customTokenAmount: string; txId: string | null; accountDetailsAddress: string; + showDeleteMetaMetricsDataModal: boolean; + showDataDeletionErrorModal: boolean; snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; @@ -185,6 +187,8 @@ const initialState: AppState = { scrollToBottom: true, txId: null, accountDetailsAddress: '', + showDeleteMetaMetricsDataModal: false, + showDataDeletionErrorModal: false, snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, @@ -608,6 +612,26 @@ export default function reduceApp( isAddingNewNetwork: Boolean(action.payload?.isAddingNewNetwork), isMultiRpcOnboarding: Boolean(action.payload?.isMultiRpcOnboarding), }; + case actionConstants.DELETE_METAMETRICS_DATA_MODAL_OPEN: + return { + ...appState, + showDeleteMetaMetricsDataModal: true, + }; + case actionConstants.DELETE_METAMETRICS_DATA_MODAL_CLOSE: + return { + ...appState, + showDeleteMetaMetricsDataModal: false, + }; + case actionConstants.DATA_DELETION_ERROR_MODAL_OPEN: + return { + ...appState, + showDataDeletionErrorModal: true, + }; + case actionConstants.DATA_DELETION_ERROR_MODAL_CLOSE: + return { + ...appState, + showDataDeletionErrorModal: false, + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -717,3 +741,27 @@ export function getLedgerWebHidConnectedStatus( export function getLedgerTransportStatus(state: AppSliceState): string | null { return state.appState.ledgerTransportStatus; } + +export function openDeleteMetaMetricsDataModal(): Action { + return { + type: actionConstants.DELETE_METAMETRICS_DATA_MODAL_OPEN, + }; +} + +export function hideDeleteMetaMetricsDataModal(): Action { + return { + type: actionConstants.DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +} + +export function openDataDeletionErrorModal(): Action { + return { + type: actionConstants.DATA_DELETION_ERROR_MODAL_OPEN, + }; +} + +export function hideDataDeletionErrorModal(): Action { + return { + type: actionConstants.DATA_DELETION_ERROR_MODAL_CLOSE, + }; +} diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index 569999f8900e..89cca83f27cf 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -324,6 +324,13 @@ const SETTINGS_CONSTANTS = [ route: `${SECURITY_ROUTE}#dataCollectionForMarketing`, icon: 'fa fa-lock', }, + { + tabMessage: (t) => t('securityAndPrivacy'), + sectionMessage: (t) => t('deleteMetaMetricsData'), + descriptionMessage: (t) => t('deleteMetaMetricsDataDescription'), + route: `${SECURITY_ROUTE}#delete-metametrics-data`, + icon: 'fa fa-lock', + }, { tabMessage: (t) => t('alerts'), sectionMessage: (t) => t('alertSettingsUnconnectedAccount'), diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index bb1637ec2cef..c3d07073a7d3 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -174,7 +174,7 @@ describe('Settings Search Utils', () => { it('returns "Security & privacy" section count', () => { expect( getNumberOfSettingRoutesInTab(t, t('securityAndPrivacy')), - ).toStrictEqual(20); + ).toStrictEqual(21); }); it('returns "Alerts" section count', () => { diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index d18da9cc2eca..343a7f05ecb4 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -1561,6 +1561,59 @@ exports[`Security Tab should match snapshot 1`] = ` </label> </div> </div> + <div + class="mm-box settings-page__content-row mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-column" + data-testid="delete-metametrics-data-button" + > + <div + class="settings-page__content-item" + > + <span> + Delete MetaMetrics data + </span> + <div + class="settings-page__content-description" + > + <span> + + This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our + <a + href="https://consensys.io/privacy-policy/" + rel="noopener noreferrer" + target="_blank" + > + Privacy policy + </a> + . + + </span> + </div> + </div> + <div + class="settings-page__content-item-col" + > + <div + class="mm-box mm-box--display-inline-flex" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/info.svg');" + /> + <p + class="mm-box mm-text mm-text--body-xs mm-box--margin-bottom-2 mm-box--margin-left-1 mm-box--color-text-default" + > + Since you've never opted into MetaMetrics, there's no data to delete here. + </p> + </div> + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--disabled settings-page__button mm-button-primary mm-button-primary--disabled mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" + data-theme="light" + disabled="" + > + Delete MetaMetrics data + </button> + </div> + </div> </div> </div> </div> diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx new file mode 100644 index 000000000000..27132fb82f5c --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx @@ -0,0 +1,212 @@ +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../../store/store'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; + +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DeleteMetaMetricsDataButton from './delete-metametrics-data-button'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), + useDispatch: jest.fn(), +})); + +describe('DeleteMetaMetricsDataButton', () => { + const useSelectorMock = useSelector as jest.Mock; + const useDispatchMock = useDispatch as jest.Mock; + const mockDispatch = jest.fn(); + + beforeEach(() => { + useDispatchMock.mockReturnValue(mockDispatch); + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return undefined; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return ''; + } + + return undefined; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const store = configureStore({}); + const { getByTestId, getAllByText, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect(getByTestId('delete-metametrics-data-button')).toBeInTheDocument(); + expect(getAllByText('Delete MetaMetrics data')).toHaveLength(2); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when metrics is opted in and metametrics id is available ', async () => { + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when page mounts after a deletion task is performed and more data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + + // if user does not opt in to participate in metrics or for profile sync, metametricsId will not be created. + it('should disable the data deletion button when there is metametrics id not available', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return null; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + expect( + container.querySelector('.settings-page__content-item-col')?.textContent, + ).toMatchInlineSnapshot( + `"Since you've never opted into MetaMetrics, there's no data to delete here.Delete MetaMetrics data"`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + it('should open the modal on the button click', () => { + const store = configureStore({}); + const { getByRole } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + const deleteButton = getByRole('button', { + name: 'Delete MetaMetrics data', + }); + fireEvent.click(deleteButton); + expect(mockDispatch).toHaveBeenCalledWith(openDeleteMetaMetricsDataModal()); + }); +}); diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx new file mode 100644 index 000000000000..34b61697ed95 --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CONSENSYS_PRIVACY_LINK } from '../../../../../shared/lib/ui-utils'; +import ClearMetametricsData from '../../../../components/app/clear-metametrics-data'; +import { + Box, + ButtonPrimary, + Icon, + IconName, + IconSize, + PolymorphicComponentPropWithRef, + PolymorphicRef, + Text, +} from '../../../../components/component-library'; +import { + Display, + FlexDirection, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getShowDataDeletionErrorModal, + getShowDeleteMetaMetricsDataModal, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DataDeletionErrorModal from '../../../../components/app/data-deletion-error-modal'; +import { formatDate } from '../../../../helpers/utils/util'; +import { DeleteRegulationStatus } from '../../../../../shared/constants/metametrics'; + +type DeleteMetaMetricsDataButtonProps<C extends React.ElementType> = + PolymorphicComponentPropWithRef<C>; + +type DeleteMetaMetricsDataButtonComponent = < + C extends React.ElementType = 'div', +>( + props: DeleteMetaMetricsDataButtonProps<C>, +) => React.ReactElement | null; + +const DeleteMetaMetricsDataButton: DeleteMetaMetricsDataButtonComponent = + React.forwardRef( + <C extends React.ElementType = 'div'>( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { ...props }: DeleteMetaMetricsDataButtonProps<C>, + ref: PolymorphicRef<C>, + ) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const metaMetricsId = useSelector(getMetaMetricsId); + const metaMetricsDataDeletionStatus: DeleteRegulationStatus = useSelector( + getMetaMetricsDataDeletionStatus, + ); + const metaMetricsDataDeletionTimestamp = useSelector( + getMetaMetricsDataDeletionTimestamp, + ); + const formatedDate = formatDate( + metaMetricsDataDeletionTimestamp, + 'd/MM/y', + ); + + const showDeleteMetaMetricsDataModal = useSelector( + getShowDeleteMetaMetricsDataModal, + ); + const showDataDeletionErrorModal = useSelector( + getShowDataDeletionErrorModal, + ); + const latestMetricsEventTimestamp = useSelector( + getLatestMetricsEventTimestamp, + ); + + let dataDeletionButtonDisabled = Boolean(!metaMetricsId); + if (!dataDeletionButtonDisabled && metaMetricsDataDeletionStatus) { + dataDeletionButtonDisabled = + [ + DeleteRegulationStatus.Initialized, + DeleteRegulationStatus.Running, + DeleteRegulationStatus.Finished, + ].includes(metaMetricsDataDeletionStatus) && + metaMetricsDataDeletionTimestamp > latestMetricsEventTimestamp; + } + const privacyPolicyLink = ( + <a + href={CONSENSYS_PRIVACY_LINK} + target="_blank" + rel="noopener noreferrer" + key="metametrics-consensys-privacy-link" + > + {t('privacyMsg')} + </a> + ); + return ( + <> + <Box + ref={ref} + className="settings-page__content-row" + data-testid="delete-metametrics-data-button" + display={Display.Flex} + flexDirection={FlexDirection.Column} + gap={4} + > + <div className="settings-page__content-item"> + <span>{t('deleteMetaMetricsData')}</span> + <div className="settings-page__content-description"> + {dataDeletionButtonDisabled && Boolean(metaMetricsId) + ? t('deleteMetaMetricsDataRequestedDescription', [ + formatedDate, + privacyPolicyLink, + ]) + : t('deleteMetaMetricsDataDescription', [privacyPolicyLink])} + </div> + </div> + <div className="settings-page__content-item-col"> + {Boolean(!metaMetricsId) && ( + <Box display={Display.InlineFlex}> + <Icon name={IconName.Info} size={IconSize.Sm} /> + <Text + variant={TextVariant.bodyXs} + marginLeft={1} + marginBottom={2} + > + {t('metaMetricsIdNotAvailableError')} + </Text> + </Box> + )} + <ButtonPrimary + className="settings-page__button" + onClick={() => { + dispatch(openDeleteMetaMetricsDataModal()); + }} + disabled={dataDeletionButtonDisabled} + > + {t('deleteMetaMetricsData')} + </ButtonPrimary> + </div> + </Box> + {showDeleteMetaMetricsDataModal && <ClearMetametricsData />} + {showDataDeletionErrorModal && <DataDeletionErrorModal />} + </> + ); + }, + ); + +export default DeleteMetaMetricsDataButton; diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts new file mode 100644 index 000000000000..945f4d349ede --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts @@ -0,0 +1 @@ +export { default } from './delete-metametrics-data-button'; diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index f6da9fe2367f..1fae729d3f31 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -52,8 +52,10 @@ import { } from '../../../helpers/utils/settings-search'; import IncomingTransactionToggle from '../../../components/app/incoming-trasaction-toggle/incoming-transaction-toggle'; -import ProfileSyncToggle from './profile-sync-toggle'; +import { updateDataDeletionTaskStatus } from '../../../store/actions'; import MetametricsToggle from './metametrics-toggle'; +import ProfileSyncToggle from './profile-sync-toggle'; +import DeleteMetametricsDataButton from './delete-metametrics-data-button'; export default class SecurityTab extends PureComponent { static contextTypes = { @@ -102,6 +104,7 @@ export default class SecurityTab extends PureComponent { useExternalServices: PropTypes.bool, toggleExternalServices: PropTypes.func.isRequired, setSecurityAlertsEnabled: PropTypes.func, + metaMetricsDataDeletionId: PropTypes.string, }; state = { @@ -138,9 +141,12 @@ export default class SecurityTab extends PureComponent { } } - componentDidMount() { + async componentDidMount() { const { t } = this.context; handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs); + if (this.props.metaMetricsDataDeletionId) { + await updateDataDeletionTaskStatus(); + } } toggleSetting(value, eventName, eventAction, toggleMethod) { @@ -961,7 +967,7 @@ export default class SecurityTab extends PureComponent { return ( <Box - ref={this.settingsRefs[18]} + ref={this.settingsRefs[17]} className="settings-page__content-row" display={Display.Flex} flexDirection={FlexDirection.Row} @@ -1222,6 +1228,7 @@ export default class SecurityTab extends PureComponent { setDataCollectionForMarketing={setDataCollectionForMarketing} /> {this.renderDataCollectionForMarketing()} + <DeleteMetametricsDataButton ref={this.settingsRefs[20]} /> </div> </div> ); diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index 747e3738fe3f..224072ef2b10 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -20,10 +20,12 @@ import { setUseExternalNameSources, setUseTransactionSimulations, setSecurityAlertsEnabled, + updateDataDeletionTaskStatus, } from '../../../store/actions'; import { getIsSecurityAlertsEnabled, getNetworkConfigurationsByChainId, + getMetaMetricsDataDeletionId, getPetnamesEnabled, } from '../../../selectors'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; @@ -78,6 +80,7 @@ const mapStateToProps = (state) => { petnamesEnabled, securityAlertsEnabled: getIsSecurityAlertsEnabled(state), useTransactionSimulations: metamask.useTransactionSimulations, + metaMetricsDataDeletionId: getMetaMetricsDataDeletionId(state), }; }; @@ -116,6 +119,9 @@ const mapDispatchToProps = (dispatch) => { setUseTransactionSimulations: (value) => { return dispatch(setUseTransactionSimulations(value)); }, + updateDataDeletionTaskStatus: () => { + return updateDataDeletionTaskStatus(); + }, setSecurityAlertsEnabled: (value) => setSecurityAlertsEnabled(value), }; }; diff --git a/ui/pages/settings/security-tab/security-tab.test.js b/ui/pages/settings/security-tab/security-tab.test.js index 905fec684fd5..5e31cfb68c57 100644 --- a/ui/pages/settings/security-tab/security-tab.test.js +++ b/ui/pages/settings/security-tab/security-tab.test.js @@ -13,6 +13,8 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { getIsSecurityAlertsEnabled } from '../../../selectors'; import SecurityTab from './security-tab.container'; +const mockOpenDeleteMetaMetricsDataModal = jest.fn(); + const mockSetSecurityAlertsEnabled = jest .fn() .mockImplementation(() => () => undefined); @@ -36,6 +38,14 @@ jest.mock('../../../store/actions', () => ({ setSecurityAlertsEnabled: (val) => mockSetSecurityAlertsEnabled(val), })); +jest.mock('../../../ducks/app/app.ts', () => { + return { + openDeleteMetaMetricsDataModal: () => { + return mockOpenDeleteMetaMetricsDataModal; + }, + }; +}); + describe('Security Tab', () => { mockState.appState.warning = 'warning'; // This tests an otherwise untested render branch @@ -214,7 +224,23 @@ describe('Security Tab', () => { await user.click(screen.getByText(tEn('addCustomNetwork'))); expect(global.platform.openExtensionInBrowser).toHaveBeenCalled(); }); + it('clicks "Delete MetaMetrics Data"', async () => { + mockState.metamask.participateInMetaMetrics = true; + mockState.metamask.metaMetricsId = 'fake-metametrics-id'; + const localMockStore = configureMockStore([thunk])(mockState); + renderWithProvider(<SecurityTab />, localMockStore); + + expect( + screen.queryByTestId(`delete-metametrics-data-button`), + ).toBeInTheDocument(); + + fireEvent.click( + screen.getByRole('button', { name: 'Delete MetaMetrics data' }), + ); + + expect(mockOpenDeleteMetaMetricsDataModal).toHaveBeenCalled(); + }); describe('Blockaid', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/ui/selectors/metametrics.js b/ui/selectors/metametrics.js index c623e378c003..1b0a9dd603dd 100644 --- a/ui/selectors/metametrics.js +++ b/ui/selectors/metametrics.js @@ -8,6 +8,9 @@ export const getDataCollectionForMarketing = (state) => export const getParticipateInMetaMetrics = (state) => Boolean(state.metamask.participateInMetaMetrics); +export const getLatestMetricsEventTimestamp = (state) => + state.metamask.latestNonAnonymousEventTimestamp; + export const selectFragmentBySuccessEvent = createSelector( selectFragments, (_, fragmentOptions) => fragmentOptions, diff --git a/ui/selectors/metametrics.test.js b/ui/selectors/metametrics.test.js index 13185a47700b..454def7d92a4 100644 --- a/ui/selectors/metametrics.test.js +++ b/ui/selectors/metametrics.test.js @@ -2,6 +2,7 @@ const { selectFragmentBySuccessEvent, selectFragmentById, selectMatchingFragment, + getLatestMetricsEventTimestamp, } = require('.'); describe('selectFragmentBySuccessEvent', () => { @@ -68,4 +69,15 @@ describe('selectMatchingFragment', () => { }); expect(selected).toHaveProperty('id', 'randomid'); }); + describe('getLatestMetricsEventTimestamp', () => { + it('should find matching fragment in state by id', () => { + const state = { + metamask: { + latestNonAnonymousEventTimestamp: 12345, + }, + }; + const timestamp = getLatestMetricsEventTimestamp(state); + expect(timestamp).toBe(12345); + }); + }); }); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index fac2f9f52c31..644924a41e3e 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2561,6 +2561,26 @@ export function getNameSources(state) { return state.metamask.nameSources || {}; } +export function getShowDeleteMetaMetricsDataModal(state) { + return state.appState.showDeleteMetaMetricsDataModal; +} + +export function getShowDataDeletionErrorModal(state) { + return state.appState.showDataDeletionErrorModal; +} + +export function getMetaMetricsDataDeletionId(state) { + return state.metamask.metaMetricsDataDeletionId; +} + +export function getMetaMetricsDataDeletionTimestamp(state) { + return state.metamask.metaMetricsDataDeletionTimestamp; +} + +export function getMetaMetricsDataDeletionStatus(state) { + return state.metamask.metaMetricsDataDeletionStatus; +} + /** * To get all installed snaps with proper metadata * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 342e7d7187c8..24b2a2afe125 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -11,6 +11,7 @@ import { createMockInternalAccount } from '../../test/jest/mocks'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { getProviderConfig } from '../ducks/metamask/metamask'; import { mockNetworkState } from '../../test/stub/networks'; +import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import * as selectors from './selectors'; jest.mock('../../app/scripts/lib/util', () => ({ @@ -2018,4 +2019,65 @@ describe('#getConnectedSitesList', () => { }, }); }); + describe('#getShowDeleteMetaMetricsDataModal', () => { + it('returns state of showDeleteMetaMetricsDataModal', () => { + expect( + selectors.getShowDeleteMetaMetricsDataModal({ + appState: { + showDeleteMetaMetricsDataModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getShowDataDeletionErrorModal', () => { + it('returns state of showDataDeletionErrorModal', () => { + expect( + selectors.getShowDataDeletionErrorModal({ + appState: { + showDataDeletionErrorModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getMetaMetricsDataDeletionId', () => { + it('returns metaMetricsDataDeletionId', () => { + expect( + selectors.getMetaMetricsDataDeletionId({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123'); + }); + }); + describe('#getMetaMetricsDataDeletionTimestamp', () => { + it('returns metaMetricsDataDeletionTimestamp', () => { + expect( + selectors.getMetaMetricsDataDeletionTimestamp({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123345'); + }); + }); + describe('#getMetaMetricsDataDeletionStatus', () => { + it('returns metaMetricsDataDeletionStatus', () => { + expect( + selectors.getMetaMetricsDataDeletionStatus({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('INITIALIZED'); + }); + }); }); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 074568cfbf1d..6e1e33d9531f 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -99,6 +99,14 @@ export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; export const SET_PARTICIPATE_IN_METAMETRICS = 'SET_PARTICIPATE_IN_METAMETRICS'; export const SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING'; +export const DELETE_METAMETRICS_DATA_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_MODAL_OPEN'; +export const DELETE_METAMETRICS_DATA_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_MODAL_CLOSE'; +export const DATA_DELETION_ERROR_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_OPEN'; +export const DATA_DELETION_ERROR_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_CLOSE'; // locale export const SET_CURRENT_LOCALE = 'SET_CURRENT_LOCALE'; From fd4cdf0826dd4e99b10c1c7df2a528545938d23b Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:04:01 +0200 Subject: [PATCH 081/226] fix: Test coverage quality gate (#27581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27581?quickstart=1) Fixes test coverage quality gates. ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3328 ## **Manual testing steps** 1. Test coverage should be correctly reported/validated ## **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. --- .github/workflows/main.yml | 26 +++++++++- .github/workflows/run-tests.yml | 68 ++++++++++++--------------- .github/workflows/sonarcloud.yml | 30 ++++++++++++ .github/workflows/update-coverage.yml | 48 +++++++++++++++++++ coverage.json | 1 + 5 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/sonarcloud.yml create mode 100644 .github/workflows/update-coverage.yml create mode 100644 coverage.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d14fefe82717..5d1b4d73bdab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,8 +2,15 @@ name: Main on: push: - branches: [develop, master] + branches: + - develop + - master pull_request: + types: + - opened + - reopened + - synchronize + merge_group: jobs: check-workflows: @@ -21,11 +28,25 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + run-tests: + 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: @@ -37,7 +58,8 @@ jobs: name: All jobs pass if: ${{ always() }} runs-on: ubuntu-latest - needs: all-jobs-completed + needs: + - all-jobs-completed steps: - name: Check that all jobs have passed run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 77958f69da2d..3cb7c50e573a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,16 +1,14 @@ name: Run tests on: - push: - branches: - - develop - - master - pull_request: - types: - - opened - - reopened - - synchronize - merge_group: + workflow_call: + outputs: + current-coverage: + description: Current coverage + value: ${{ jobs.report-coverage.outputs.current-coverage }} + stored-coverage: + description: Stored coverage + value: ${{ jobs.report-coverage.outputs.stored-coverage }} jobs: test-unit: @@ -79,18 +77,19 @@ jobs: name: coverage-integration path: coverage/integration/coverage-integration.json - sonarcloud: - name: SonarCloud + report-coverage: + name: Report coverage runs-on: ubuntu-latest needs: - test-unit - test-webpack - test-integration + outputs: + current-coverage: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + stored-coverage: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: Setup environment uses: metamask/github-tools/.github/actions/setup-environment@main @@ -109,35 +108,28 @@ jobs: name: lcov.info path: coverage/lcov.info - - name: Get Sonar coverage - id: get-sonar-coverage - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Get current coverage + id: get-current-coverage + run: | + current_coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print int($3)}') + echo "The current coverage is $current_coverage%." + echo 'CURRENT_COVERAGE='"$current_coverage" >> "$GITHUB_OUTPUT" + + - name: Get stored coverage + id: get-stored-coverage run: | - projectKey=$(grep 'sonar.projectKey=' sonar-project.properties | cut -d'=' -f2) - sonar_coverage=$(curl --silent --header "Authorization: Bearer $SONAR_TOKEN" "https://sonarcloud.io/api/measures/component?component=$projectKey&metricKeys=coverage" | jq -r '.component.measures[0].value // 0') - echo "The Sonar coverage of $projectKey is $sonar_coverage%." - echo 'SONAR_COVERAGE='"$sonar_coverage" >> "$GITHUB_OUTPUT" + stored_coverage=$(jq ".coverage" coverage.json) + echo "The stored coverage is $stored_coverage%." + echo 'STORED_COVERAGE='"$stored_coverage" >> "$GITHUB_OUTPUT" - name: Validate test coverage env: - SONAR_COVERAGE: ${{ steps.get-sonar-coverage.outputs.SONAR_COVERAGE }} + CURRENT_COVERAGE: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + STORED_COVERAGE: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} run: | - coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print $3}') - if [ -z "$coverage" ]; then - echo "::error::Could not retrieve test coverage." - exit 1 - fi - if (( $(echo "$coverage < $SONAR_COVERAGE" | bc -l) )); then - echo "::error::Quality gate failed for test coverage. Current test coverage is $coverage%, please increase coverage to at least $SONAR_COVERAGE%." + if (( $(echo "$CURRENT_COVERAGE < $STORED_COVERAGE" | bc -l) )); then + echo "::error::Quality gate failed for test coverage. Current coverage is $CURRENT_COVERAGE%, please increase coverage to at least $STORED_COVERAGE%." exit 1 else - echo "Test coverage is $coverage%. Quality gate passed." + echo "The current coverage is $CURRENT_COVERAGE%, stored coverage is $STORED_COVERAGE%. Quality gate passed." fi - - - name: SonarCloud Scan - # This is SonarSource/sonarcloud-github-action@v2.0.0 - uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000000..460d5c140462 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,30 @@ +name: SonarCloud + +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: lcov.info + path: coverage + + - name: SonarCloud Scan + # This is SonarSource/sonarcloud-github-action@v2.0.0 + uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml new file mode 100644 index 000000000000..f246bde7eb32 --- /dev/null +++ b/.github/workflows/update-coverage.yml @@ -0,0 +1,48 @@ +name: Update coverage + +on: + schedule: + # Once per day at midnight UTC + - cron: 0 0 * * * + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + update-coverage: + if: ${{ needs.run-tests.outputs.current-coverage > needs.run-tests.outputs.stored-coverage }} + name: Update coverage + runs-on: ubuntu-latest + needs: + - run-tests + env: + CURRENT_COVERAGE: ${{ needs.run-tests.outputs.current-coverage }} + STORED_COVERAGE: ${{ needs.run-tests.outputs.stored-coverage }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Update coverage + run: | + echo "{ \"coverage\": $CURRENT_COVERAGE }" > coverage.json + + - name: Checkout/create branch, commit, and force push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b metamaskbot/update-coverage + git add coverage.json + git commit -m "chore: Update coverage.json" + git push -f origin metamaskbot/update-coverage + + - name: Create/update pull request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create --title "chore: Update coverage.json" --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." --base develop --head metamaskbot/update-coverage || gh pr edit --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." diff --git a/coverage.json b/coverage.json new file mode 100644 index 000000000000..f65ea343e9b3 --- /dev/null +++ b/coverage.json @@ -0,0 +1 @@ +{ "coverage": 0 } From bff1e2160746363085f1f5a9bb92eb5e0e958554 Mon Sep 17 00:00:00 2001 From: Howard Braham <howrad@gmail.com> Date: Mon, 7 Oct 2024 20:48:14 -0700 Subject: [PATCH 082/226] refactor: routes constants (#27078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This is one small step in the larger task to refactor routing, in order to speed up load time (MetaMask/MetaMask-planning#2898) The changes are mostly to increase DRY, and to make a closer coupling between connected routes and their analytics tracking names. I wanted to get this in separately in order to reduce complexity and merge conflicts later. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27078?quickstart=1) ## **Related issues** Progresses: MetaMask/MetaMask-planning#2898 ## **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 - [ ] 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: legobeat <109787230+legobeat@users.noreply.github.com> --- ui/helpers/constants/routes.ts | 583 ++++++++++++++++----------------- 1 file changed, 284 insertions(+), 299 deletions(-) diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index c755c9914f25..eec9075a64d8 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -1,307 +1,292 @@ -const DEFAULT_ROUTE = '/'; -const UNLOCK_ROUTE = '/unlock'; -const LOCK_ROUTE = '/lock'; -const ASSET_ROUTE = '/asset'; -const SETTINGS_ROUTE = '/settings'; -const GENERAL_ROUTE = '/settings/general'; -const ADVANCED_ROUTE = '/settings/advanced'; - -const DEVELOPER_OPTIONS_ROUTE = '/settings/developer-options'; -const EXPERIMENTAL_ROUTE = '/settings/experimental'; -const SECURITY_ROUTE = '/settings/security'; -const ABOUT_US_ROUTE = '/settings/about-us'; -const ALERTS_ROUTE = '/settings/alerts'; -const NETWORKS_ROUTE = '/settings/networks'; -const NETWORKS_FORM_ROUTE = '/settings/networks/form'; -const ADD_NETWORK_ROUTE = '/settings/networks/add-network'; -const ADD_POPULAR_CUSTOM_NETWORK = +// PATH_NAME_MAP is used to pull a convenient name for analytics tracking events. The key must +// be react-router ready path, and can include params such as :id for popup windows +export const PATH_NAME_MAP: { [key: string]: string } = {}; + +export const DEFAULT_ROUTE = '/'; +PATH_NAME_MAP[DEFAULT_ROUTE] = 'Home'; + +export const UNLOCK_ROUTE = '/unlock'; +PATH_NAME_MAP[UNLOCK_ROUTE] = 'Unlock Page'; + +export const LOCK_ROUTE = '/lock'; +PATH_NAME_MAP[LOCK_ROUTE] = 'Lock Page'; + +export const ASSET_ROUTE = '/asset'; +PATH_NAME_MAP[`${ASSET_ROUTE}/:asset/:id`] = `Asset Page`; +PATH_NAME_MAP[`${ASSET_ROUTE}/image/:asset/:id`] = `Nft Image Page`; + +export const SETTINGS_ROUTE = '/settings'; +PATH_NAME_MAP[SETTINGS_ROUTE] = 'Settings Page'; + +export const GENERAL_ROUTE = '/settings/general'; +PATH_NAME_MAP[GENERAL_ROUTE] = 'General Settings Page'; + +export const ADVANCED_ROUTE = '/settings/advanced'; +PATH_NAME_MAP[ADVANCED_ROUTE] = 'Advanced Settings Page'; + +export const DEVELOPER_OPTIONS_ROUTE = '/settings/developer-options'; +// DEVELOPER_OPTIONS_ROUTE not in PATH_NAME_MAP because we're not tracking analytics for this page + +export const EXPERIMENTAL_ROUTE = '/settings/experimental'; +PATH_NAME_MAP[EXPERIMENTAL_ROUTE] = 'Experimental Settings Page'; + +export const SECURITY_ROUTE = '/settings/security'; +PATH_NAME_MAP[SECURITY_ROUTE] = 'Security Settings Page'; + +export const ABOUT_US_ROUTE = '/settings/about-us'; +PATH_NAME_MAP[ABOUT_US_ROUTE] = 'About Us Page'; + +export const ALERTS_ROUTE = '/settings/alerts'; +PATH_NAME_MAP[ALERTS_ROUTE] = 'Alerts Settings Page'; + +export const NETWORKS_ROUTE = '/settings/networks'; +PATH_NAME_MAP[NETWORKS_ROUTE] = 'Network Settings Page'; + +export const NETWORKS_FORM_ROUTE = '/settings/networks/form'; +PATH_NAME_MAP[NETWORKS_FORM_ROUTE] = 'Network Settings Page Form'; + +export const ADD_NETWORK_ROUTE = '/settings/networks/add-network'; +PATH_NAME_MAP[ADD_NETWORK_ROUTE] = 'Add Network From Settings Page Form'; + +export const ADD_POPULAR_CUSTOM_NETWORK = '/settings/networks/add-popular-custom-network'; -const CONTACT_LIST_ROUTE = '/settings/contact-list'; -const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact'; -const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact'; -const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; -const REVEAL_SEED_ROUTE = '/seed'; -const RESTORE_VAULT_ROUTE = '/restore-vault'; -const IMPORT_TOKEN_ROUTE = '/import-token'; -const IMPORT_TOKENS_ROUTE = '/import-tokens'; -const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token'; -const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'; -const NEW_ACCOUNT_ROUTE = '/new-account'; -const CONFIRM_ADD_SUGGESTED_NFT_ROUTE = '/confirm-add-suggested-nft'; -const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; +PATH_NAME_MAP[ADD_POPULAR_CUSTOM_NETWORK] = + 'Add Network From A List Of Popular Custom Networks'; + +export const CONTACT_LIST_ROUTE = '/settings/contact-list'; +PATH_NAME_MAP[CONTACT_LIST_ROUTE] = 'Contact List Settings Page'; + +export const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact'; +PATH_NAME_MAP[`${CONTACT_EDIT_ROUTE}/:address`] = 'Edit Contact Settings Page'; + +export const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact'; +PATH_NAME_MAP[CONTACT_ADD_ROUTE] = 'Add Contact Settings Page'; + +export const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; +PATH_NAME_MAP[`${CONTACT_VIEW_ROUTE}/:address`] = 'View Contact Settings Page'; + +export const REVEAL_SEED_ROUTE = '/seed'; +PATH_NAME_MAP[REVEAL_SEED_ROUTE] = 'Reveal Secret Recovery Phrase Page'; + +export const RESTORE_VAULT_ROUTE = '/restore-vault'; +PATH_NAME_MAP[RESTORE_VAULT_ROUTE] = 'Restore Vault Page'; + +export const IMPORT_TOKEN_ROUTE = '/import-token'; +PATH_NAME_MAP[IMPORT_TOKEN_ROUTE] = 'Import Token Page'; + +export const IMPORT_TOKENS_ROUTE = '/import-tokens'; +PATH_NAME_MAP[IMPORT_TOKENS_ROUTE] = 'Import Tokens Page'; + +export const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token'; +PATH_NAME_MAP[CONFIRM_IMPORT_TOKEN_ROUTE] = 'Confirm Import Token Page'; + +export const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'; +PATH_NAME_MAP[CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE] = + 'Confirm Add Suggested Token Page'; + +export const NEW_ACCOUNT_ROUTE = '/new-account'; +PATH_NAME_MAP[NEW_ACCOUNT_ROUTE] = 'New Account Page'; + +export const CONFIRM_ADD_SUGGESTED_NFT_ROUTE = '/confirm-add-suggested-nft'; +PATH_NAME_MAP[CONFIRM_ADD_SUGGESTED_NFT_ROUTE] = + 'Confirm Add Suggested NFT Page'; + +export const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; +PATH_NAME_MAP[CONNECT_HARDWARE_ROUTE] = 'Connect Hardware Wallet Page'; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody'; -const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done'; -const CUSTODY_ACCOUNT_DONE_ROUTE = '/new-account/custody/done'; -const CONFIRM_ADD_CUSTODIAN_TOKEN = '/confirm-add-custodian-token'; -const INTERACTIVE_REPLACEMENT_TOKEN_PAGE = +export const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody'; +PATH_NAME_MAP[CUSTODY_ACCOUNT_ROUTE] = 'Connect Custody'; + +export const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done'; +PATH_NAME_MAP[INSTITUTIONAL_FEATURES_DONE_ROUTE] = + 'Institutional Features Done Page'; + +export const CUSTODY_ACCOUNT_DONE_ROUTE = '/new-account/custody/done'; +PATH_NAME_MAP[CUSTODY_ACCOUNT_DONE_ROUTE] = 'Connect Custody Account done'; + +export const CONFIRM_ADD_CUSTODIAN_TOKEN = '/confirm-add-custodian-token'; +PATH_NAME_MAP[CONFIRM_ADD_CUSTODIAN_TOKEN] = 'Confirm Add Custodian Token'; + +export const INTERACTIVE_REPLACEMENT_TOKEN_PAGE = '/interactive-replacement-token-page'; -const SRP_REMINDER = '/onboarding/remind-srp'; +PATH_NAME_MAP[INTERACTIVE_REPLACEMENT_TOKEN_PAGE] = + 'Interactive replacement token page'; + +export const SRP_REMINDER = '/onboarding/remind-srp'; +PATH_NAME_MAP[SRP_REMINDER] = 'Secret Recovery Phrase Reminder'; ///: END:ONLY_INCLUDE_IF -const SEND_ROUTE = '/send'; -const CONNECTIONS = '/connections'; -const REVIEW_PERMISSIONS = '/review-permissions'; -const PERMISSIONS = '/permissions'; -const TOKEN_DETAILS = '/token-details'; -const CONNECT_ROUTE = '/connect'; -const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'; -const CONNECT_SNAPS_CONNECT_ROUTE = '/snaps-connect'; -const CONNECT_SNAP_INSTALL_ROUTE = '/snap-install'; -const CONNECT_SNAP_UPDATE_ROUTE = '/snap-update'; -const CONNECT_SNAP_RESULT_ROUTE = '/snap-install-result'; -const SNAPS_ROUTE = '/snaps'; -const SNAPS_VIEW_ROUTE = '/snaps/view'; -const NOTIFICATIONS_ROUTE = '/notifications'; -const NOTIFICATIONS_SETTINGS_ROUTE = '/notifications/settings'; -const CONNECTED_ROUTE = '/connected'; -const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'; -const CROSS_CHAIN_SWAP_ROUTE = '/cross-chain'; -const SWAPS_ROUTE = '/swaps'; -const PREPARE_SWAP_ROUTE = '/swaps/prepare-swap-page'; -const SWAPS_NOTIFICATION_ROUTE = '/swaps/notification-page'; -const BUILD_QUOTE_ROUTE = '/swaps/build-quote'; -const VIEW_QUOTE_ROUTE = '/swaps/view-quote'; -const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'; -const AWAITING_SIGNATURES_ROUTE = '/swaps/awaiting-signatures'; -const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status'; -const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap'; -const SWAPS_ERROR_ROUTE = '/swaps/swaps-error'; -const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance'; - -const ONBOARDING_ROUTE = '/onboarding'; -const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase'; -const ONBOARDING_CONFIRM_SRP_ROUTE = '/onboarding/confirm-recovery-phrase'; -const ONBOARDING_CREATE_PASSWORD_ROUTE = '/onboarding/create-password'; -const ONBOARDING_COMPLETION_ROUTE = '/onboarding/completion'; -const MMI_ONBOARDING_COMPLETION_ROUTE = '/onboarding/account-completion'; -const ONBOARDING_UNLOCK_ROUTE = '/onboarding/unlock'; -const ONBOARDING_HELP_US_IMPROVE_ROUTE = '/onboarding/help-us-improve'; -const ONBOARDING_IMPORT_WITH_SRP_ROUTE = + +export const SEND_ROUTE = '/send'; +PATH_NAME_MAP[SEND_ROUTE] = 'Send Page'; + +export const CONNECTIONS = '/connections'; +PATH_NAME_MAP[CONNECTIONS] = 'Connections'; + +export const PERMISSIONS = '/permissions'; +PATH_NAME_MAP[PERMISSIONS] = 'Permissions'; + +export const REVIEW_PERMISSIONS = '/review-permissions'; + +export const TOKEN_DETAILS = '/token-details'; +PATH_NAME_MAP[`${TOKEN_DETAILS}/:address`] = 'Token Details Page'; + +export const CONNECT_ROUTE = '/connect'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id`] = 'Connect To Site Confirmation Page'; + +export const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_CONFIRM_PERMISSIONS_ROUTE}`] = + 'Grant Connected Site Permissions Confirmation Page'; + +export const CONNECT_SNAPS_CONNECT_ROUTE = '/snaps-connect'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAPS_CONNECT_ROUTE}`] = + 'Snaps Connect Page'; + +export const CONNECT_SNAP_INSTALL_ROUTE = '/snap-install'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAP_INSTALL_ROUTE}`] = + 'Snap Install Page'; + +export const CONNECT_SNAP_UPDATE_ROUTE = '/snap-update'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAP_UPDATE_ROUTE}`] = + 'Snap Update Page'; + +export const CONNECT_SNAP_RESULT_ROUTE = '/snap-install-result'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAP_RESULT_ROUTE}`] = + 'Snap Install Result Page'; + +export const SNAPS_ROUTE = '/snaps'; +PATH_NAME_MAP[SNAPS_ROUTE] = 'Snaps List Page'; + +export const SNAPS_VIEW_ROUTE = '/snaps/view'; +PATH_NAME_MAP[`${SNAPS_VIEW_ROUTE}/:snapId`] = 'Snap View Page'; + +export const NOTIFICATIONS_ROUTE = '/notifications'; +PATH_NAME_MAP[NOTIFICATIONS_ROUTE] = 'Notifications Page'; +PATH_NAME_MAP[`${NOTIFICATIONS_ROUTE}/:uuid`] = 'Notification Detail Page'; + +export const NOTIFICATIONS_SETTINGS_ROUTE = '/notifications/settings'; +PATH_NAME_MAP[NOTIFICATIONS_SETTINGS_ROUTE] = 'Notifications Settings Page'; + +export const CONNECTED_ROUTE = '/connected'; +PATH_NAME_MAP[CONNECTED_ROUTE] = 'Sites Connected To This Account Page'; + +export const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'; +PATH_NAME_MAP[CONNECTED_ACCOUNTS_ROUTE] = + 'Accounts Connected To This Site Page'; + +export const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'; +PATH_NAME_MAP[CONFIRM_TRANSACTION_ROUTE] = 'Confirmation Root Page'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id`] = 'Confirmation Root Page'; + +export const CONFIRMATION_V_NEXT_ROUTE = '/confirmation'; +PATH_NAME_MAP[CONFIRMATION_V_NEXT_ROUTE] = 'New Confirmation Page'; +PATH_NAME_MAP[`${CONFIRMATION_V_NEXT_ROUTE}/:id`] = 'New Confirmation Page'; + +export const CONFIRM_SEND_ETHER_PATH = '/send-ether'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_ETHER_PATH}`] = + 'Confirm Send Ether Transaction Page'; + +export const CONFIRM_SEND_TOKEN_PATH = '/send-token'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_TOKEN_PATH}`] = + 'Confirm Send Token Transaction Page'; + +export const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_DEPLOY_CONTRACT_PATH}` +] = 'Confirm Deploy Contract Transaction Page'; + +export const CONFIRM_APPROVE_PATH = '/approve'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_APPROVE_PATH}`] = + 'Confirm Approve Transaction Page'; + +export const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}` +] = 'Confirm Set Approval For All Transaction Page'; + +export const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`] = + 'Confirm Transfer From Transaction Page'; + +export const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}` +] = 'Confirm Safe Transfer From Transaction Page'; + +export const CONFIRM_TOKEN_METHOD_PATH = '/token-method'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TOKEN_METHOD_PATH}`] = + 'Confirm Token Method Transaction Page'; + +export const CONFIRM_INCREASE_ALLOWANCE_PATH = '/increase-allowance'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_INCREASE_ALLOWANCE_PATH}` +] = 'Confirm Increase Allowance Transaction Page'; + +export const SIGNATURE_REQUEST_PATH = '/signature-request'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${SIGNATURE_REQUEST_PATH}`] = + 'Signature Request Page'; + +export const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${DECRYPT_MESSAGE_REQUEST_PATH}` +] = 'Decrypt Message Request Page'; + +export const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = + '/encryption-public-key-request'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}` +] = 'Encryption Public Key Request Page'; + +export const CROSS_CHAIN_SWAP_ROUTE = '/cross-chain'; + +export const SWAPS_ROUTE = '/swaps'; + +export const PREPARE_SWAP_ROUTE = '/swaps/prepare-swap-page'; +PATH_NAME_MAP[PREPARE_SWAP_ROUTE] = 'Prepare Swap Page'; + +export const SWAPS_NOTIFICATION_ROUTE = '/swaps/notification-page'; +PATH_NAME_MAP[SWAPS_NOTIFICATION_ROUTE] = 'Swaps Notification Page'; + +export const BUILD_QUOTE_ROUTE = '/swaps/build-quote'; +PATH_NAME_MAP[BUILD_QUOTE_ROUTE] = 'Swaps Build Quote Page'; + +export const VIEW_QUOTE_ROUTE = '/swaps/view-quote'; +PATH_NAME_MAP[VIEW_QUOTE_ROUTE] = 'Swaps View Quotes Page'; + +export const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'; +PATH_NAME_MAP[LOADING_QUOTES_ROUTE] = 'Swaps Loading Quotes Page'; + +export const AWAITING_SIGNATURES_ROUTE = '/swaps/awaiting-signatures'; + +export const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status'; + +export const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap'; +PATH_NAME_MAP[AWAITING_SWAP_ROUTE] = 'Swaps Awaiting Swaps Page'; + +export const SWAPS_ERROR_ROUTE = '/swaps/swaps-error'; +PATH_NAME_MAP[SWAPS_ERROR_ROUTE] = 'Swaps Error Page'; + +export const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance'; + +export const ONBOARDING_ROUTE = '/onboarding'; +export const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase'; +export const ONBOARDING_CONFIRM_SRP_ROUTE = + '/onboarding/confirm-recovery-phrase'; +export const ONBOARDING_CREATE_PASSWORD_ROUTE = '/onboarding/create-password'; +export const ONBOARDING_COMPLETION_ROUTE = '/onboarding/completion'; +export const MMI_ONBOARDING_COMPLETION_ROUTE = '/onboarding/account-completion'; +export const ONBOARDING_UNLOCK_ROUTE = '/onboarding/unlock'; +export const ONBOARDING_HELP_US_IMPROVE_ROUTE = '/onboarding/help-us-improve'; +export const ONBOARDING_IMPORT_WITH_SRP_ROUTE = '/onboarding/import-with-recovery-phrase'; -const ONBOARDING_SECURE_YOUR_WALLET_ROUTE = '/onboarding/secure-your-wallet'; -const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings'; -const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension'; -const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome'; -const ONBOARDING_METAMETRICS = '/onboarding/metametrics'; +export const ONBOARDING_SECURE_YOUR_WALLET_ROUTE = + '/onboarding/secure-your-wallet'; +export const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings'; +export const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension'; +export const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome'; +export const ONBOARDING_METAMETRICS = '/onboarding/metametrics'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) -const INITIALIZE_EXPERIMENTAL_AREA = '/initialize/experimental-area'; -const ONBOARDING_EXPERIMENTAL_AREA = '/onboarding/experimental-area'; +export const INITIALIZE_EXPERIMENTAL_AREA = '/initialize/experimental-area'; +export const ONBOARDING_EXPERIMENTAL_AREA = '/onboarding/experimental-area'; ///: END:ONLY_INCLUDE_IF - -const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'; -const CONFIRM_SEND_ETHER_PATH = '/send-ether'; -const CONFIRM_SEND_TOKEN_PATH = '/send-token'; -const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'; -const CONFIRM_APPROVE_PATH = '/approve'; -const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all'; -const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'; -const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from'; -const CONFIRM_TOKEN_METHOD_PATH = '/token-method'; -const CONFIRM_INCREASE_ALLOWANCE_PATH = '/increase-allowance'; -const SIGNATURE_REQUEST_PATH = '/signature-request'; -const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request'; -const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request'; -const CONFIRMATION_V_NEXT_ROUTE = '/confirmation'; - -// Used to pull a convenient name for analytics tracking events. The key must -// be react-router ready path, and can include params such as :id for popup windows -const PATH_NAME_MAP = { - [DEFAULT_ROUTE]: 'Home', - [UNLOCK_ROUTE]: 'Unlock Page', - [LOCK_ROUTE]: 'Lock Page', - [`${ASSET_ROUTE}/:asset/:id`]: `Asset Page`, - [`${ASSET_ROUTE}/image/:asset/:id`]: `Nft Image Page`, - [SETTINGS_ROUTE]: 'Settings Page', - [GENERAL_ROUTE]: 'General Settings Page', - [ADVANCED_ROUTE]: 'Advanced Settings Page', - // DEVELOPER_OPTIONS_ROUTE not included because we're not tracking analytics for this page - // [DEVELOPER_OPTIONS_ROUTE]: 'Experimental Settings Page', - [EXPERIMENTAL_ROUTE]: 'Experimental Settings Page', - [SECURITY_ROUTE]: 'Security Settings Page', - [ABOUT_US_ROUTE]: 'About Us Page', - [ALERTS_ROUTE]: 'Alerts Settings Page', - [NETWORKS_ROUTE]: 'Network Settings Page', - [NETWORKS_FORM_ROUTE]: 'Network Settings Page Form', - [ADD_NETWORK_ROUTE]: 'Add Network From Settings Page Form', - [ADD_POPULAR_CUSTOM_NETWORK]: - 'Add Network From A List Of Popular Custom Networks', - [CONTACT_LIST_ROUTE]: 'Contact List Settings Page', - [`${CONTACT_EDIT_ROUTE}/:address`]: 'Edit Contact Settings Page', - [CONTACT_ADD_ROUTE]: 'Add Contact Settings Page', - [`${CONTACT_VIEW_ROUTE}/:address`]: 'View Contact Settings Page', - [REVEAL_SEED_ROUTE]: 'Reveal Secret Recovery Phrase Page', - [RESTORE_VAULT_ROUTE]: 'Restore Vault Page', - [IMPORT_TOKEN_ROUTE]: 'Import Token Page', - [CONFIRM_IMPORT_TOKEN_ROUTE]: 'Confirm Import Token Page', - [CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE]: 'Confirm Add Suggested Token Page', - [IMPORT_TOKENS_ROUTE]: 'Import Tokens Page', - [NEW_ACCOUNT_ROUTE]: 'New Account Page', - [CONFIRM_ADD_SUGGESTED_NFT_ROUTE]: 'Confirm Add Suggested NFT Page', - [CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page', - [NOTIFICATIONS_ROUTE]: 'Notifications Page', - [NOTIFICATIONS_SETTINGS_ROUTE]: 'Notifications Settings Page', - [`${NOTIFICATIONS_ROUTE}/:uuid`]: 'Notification Detail Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAPS_CONNECT_ROUTE}`]: 'Snaps Connect Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAP_INSTALL_ROUTE}`]: 'Snap Install Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAP_UPDATE_ROUTE}`]: 'Snap Update Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAP_RESULT_ROUTE}`]: - 'Snap Install Result Page', - [SNAPS_ROUTE]: 'Snaps List Page', - [`${SNAPS_VIEW_ROUTE}/:snapId`]: 'Snap View Page', - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - [INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional Features Done Page', - [CUSTODY_ACCOUNT_ROUTE]: 'Connect Custody', - [CUSTODY_ACCOUNT_DONE_ROUTE]: 'Connect Custody Account done', - [CONFIRM_ADD_CUSTODIAN_TOKEN]: 'Confirm Add Custodian Token', - [INTERACTIVE_REPLACEMENT_TOKEN_PAGE]: 'Interactive replacement token page', - [SRP_REMINDER]: 'Secret Recovery Phrase Reminder', - ///: END:ONLY_INCLUDE_IF - [SEND_ROUTE]: 'Send Page', - [CONNECTIONS]: 'Connections', - [PERMISSIONS]: 'Permissions', - [`${TOKEN_DETAILS}/:address`]: 'Token Details Page', - [`${CONNECT_ROUTE}/:id`]: 'Connect To Site Confirmation Page', - [`${CONNECT_ROUTE}/:id${CONNECT_CONFIRM_PERMISSIONS_ROUTE}`]: - 'Grant Connected Site Permissions Confirmation Page', - [CONNECTED_ROUTE]: 'Sites Connected To This Account Page', - [CONNECTED_ACCOUNTS_ROUTE]: 'Accounts Connected To This Site Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id`]: 'Confirmation Root Page', - [CONFIRM_TRANSACTION_ROUTE]: 'Confirmation Root Page', - // TODO: rename when this is the only confirmation page - [CONFIRMATION_V_NEXT_ROUTE]: 'New Confirmation Page', - [`${CONFIRMATION_V_NEXT_ROUTE}/:id`]: 'New Confirmation Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TOKEN_METHOD_PATH}`]: - 'Confirm Token Method Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_ETHER_PATH}`]: - 'Confirm Send Ether Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_TOKEN_PATH}`]: - 'Confirm Send Token Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_DEPLOY_CONTRACT_PATH}`]: - 'Confirm Deploy Contract Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_APPROVE_PATH}`]: - 'Confirm Approve Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`]: - 'Confirm Set Approval For All Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_INCREASE_ALLOWANCE_PATH}`]: - 'Confirm Increase Allowance Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`]: - 'Confirm Transfer From Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}`]: - 'Confirm Safe Transfer From Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${SIGNATURE_REQUEST_PATH}`]: - 'Signature Request Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${DECRYPT_MESSAGE_REQUEST_PATH}`]: - 'Decrypt Message Request Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`]: - 'Encryption Public Key Request Page', - [BUILD_QUOTE_ROUTE]: 'Swaps Build Quote Page', - [PREPARE_SWAP_ROUTE]: 'Prepare Swap Page', - [SWAPS_NOTIFICATION_ROUTE]: 'Swaps Notification Page', - [VIEW_QUOTE_ROUTE]: 'Swaps View Quotes Page', - [LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page', - [AWAITING_SWAP_ROUTE]: 'Swaps Awaiting Swaps Page', - [SWAPS_ERROR_ROUTE]: 'Swaps Error Page', -}; - -export { - DEFAULT_ROUTE, - ALERTS_ROUTE, - ASSET_ROUTE, - UNLOCK_ROUTE, - LOCK_ROUTE, - SETTINGS_ROUTE, - REVEAL_SEED_ROUTE, - RESTORE_VAULT_ROUTE, - IMPORT_TOKEN_ROUTE, - CONFIRM_IMPORT_TOKEN_ROUTE, - CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, - IMPORT_TOKENS_ROUTE, - NEW_ACCOUNT_ROUTE, - CONFIRM_ADD_SUGGESTED_NFT_ROUTE, - CONNECT_HARDWARE_ROUTE, - SEND_ROUTE, - CONNECTIONS, - PERMISSIONS, - REVIEW_PERMISSIONS, - TOKEN_DETAILS, - CONFIRM_TRANSACTION_ROUTE, - CONFIRM_SEND_ETHER_PATH, - CONFIRM_SEND_TOKEN_PATH, - CONFIRM_DEPLOY_CONTRACT_PATH, - CONFIRM_APPROVE_PATH, - CONFIRM_SET_APPROVAL_FOR_ALL_PATH, - CONFIRM_TRANSFER_FROM_PATH, - CONFIRM_SAFE_TRANSFER_FROM_PATH, - CONFIRM_TOKEN_METHOD_PATH, - CONFIRM_INCREASE_ALLOWANCE_PATH, - SIGNATURE_REQUEST_PATH, - DECRYPT_MESSAGE_REQUEST_PATH, - ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, - CONFIRMATION_V_NEXT_ROUTE, - ADVANCED_ROUTE, - DEVELOPER_OPTIONS_ROUTE, - EXPERIMENTAL_ROUTE, - SECURITY_ROUTE, - GENERAL_ROUTE, - ABOUT_US_ROUTE, - CONTACT_LIST_ROUTE, - CONTACT_EDIT_ROUTE, - CONTACT_ADD_ROUTE, - CONTACT_VIEW_ROUTE, - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - CUSTODY_ACCOUNT_DONE_ROUTE, - CUSTODY_ACCOUNT_ROUTE, - INSTITUTIONAL_FEATURES_DONE_ROUTE, - CONFIRM_ADD_CUSTODIAN_TOKEN, - INTERACTIVE_REPLACEMENT_TOKEN_PAGE, - SRP_REMINDER, - ///: END:ONLY_INCLUDE_IF - NETWORKS_ROUTE, - NETWORKS_FORM_ROUTE, - ADD_NETWORK_ROUTE, - ADD_POPULAR_CUSTOM_NETWORK, - CONNECT_ROUTE, - CONNECT_CONFIRM_PERMISSIONS_ROUTE, - CONNECT_SNAPS_CONNECT_ROUTE, - CONNECT_SNAP_INSTALL_ROUTE, - CONNECT_SNAP_UPDATE_ROUTE, - CONNECT_SNAP_RESULT_ROUTE, - NOTIFICATIONS_ROUTE, - NOTIFICATIONS_SETTINGS_ROUTE, - SNAPS_ROUTE, - SNAPS_VIEW_ROUTE, - CROSS_CHAIN_SWAP_ROUTE, - CONNECTED_ROUTE, - CONNECTED_ACCOUNTS_ROUTE, - PATH_NAME_MAP, - SWAPS_ROUTE, - PREPARE_SWAP_ROUTE, - SWAPS_NOTIFICATION_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, - LOADING_QUOTES_ROUTE, - AWAITING_SWAP_ROUTE, - AWAITING_SIGNATURES_ROUTE, - SWAPS_ERROR_ROUTE, - SWAPS_MAINTENANCE_ROUTE, - SMART_TRANSACTION_STATUS_ROUTE, - ONBOARDING_ROUTE, - ONBOARDING_HELP_US_IMPROVE_ROUTE, - ONBOARDING_CREATE_PASSWORD_ROUTE, - ONBOARDING_IMPORT_WITH_SRP_ROUTE, - ONBOARDING_SECURE_YOUR_WALLET_ROUTE, - ONBOARDING_REVIEW_SRP_ROUTE, - ONBOARDING_CONFIRM_SRP_ROUTE, - ONBOARDING_PRIVACY_SETTINGS_ROUTE, - ONBOARDING_COMPLETION_ROUTE, - MMI_ONBOARDING_COMPLETION_ROUTE, - ONBOARDING_UNLOCK_ROUTE, - ONBOARDING_PIN_EXTENSION_ROUTE, - ONBOARDING_WELCOME_ROUTE, - ONBOARDING_METAMETRICS, - ///: BEGIN:ONLY_INCLUDE_IF(build-flask) - INITIALIZE_EXPERIMENTAL_AREA, - ONBOARDING_EXPERIMENTAL_AREA, - ///: END:ONLY_INCLUDE_IF -}; From 44aa02715fd9ea8665440ac286e7ee3f40880823 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Tue, 8 Oct 2024 08:19:37 +0100 Subject: [PATCH 083/226] fix: banner alert to render multiple general alerts (#27339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR aims to fix the banner alert to support rendering multiple alerts. Previously we only rendered one alert and if there were more alerts we rendered the banner with a default copy informing the user there are multiple alerts. - Fixed padding on the alerts modal based on [figma](https://www.figma.com/design/gcwF9smHsgvFWQK83lT5UU/Confirmation-redesign-V4?node-id=3355-12480&node-type=frame&t=3Vbe0qFcmcfN5uCG-0) - Fixed bug Contract Interaction and Alerts - 'Cannot read properties of undefined (reading 'key')` <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27339?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2873 https://github.com/MetaMask/metamask-extension/issues/27238 ## **Manual testing steps** 1. Create a transaction with high nonce 2. Go to test dapp 3. Trigger a malicious transaction from PPOM session ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** ![Screenshot from 2024-09-23 13-38-10](https://github.com/user-attachments/assets/f4cbe8ee-7217-4718-998a-2016c9c60b88) ![Screenshot from 2024-09-23 14-09-42](https://github.com/user-attachments/assets/abb8c0c0-8cb8-4230-9469-d0b8b9f2a9a1) ![Screenshot from 2024-09-23 14-21-53](https://github.com/user-attachments/assets/0747e0d0-d50f-4f59-9a9e-0baefb4d9b5e) [bug.webm](https://github.com/user-attachments/assets/eb447959-78f0-4ccc-a554-cca272e59b19) <!-- [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. --------- Co-authored-by: Ariella Vu <20778143+digiwand@users.noreply.github.com> --- app/_locales/de/messages.json | 6 -- app/_locales/el/messages.json | 6 -- app/_locales/en/messages.json | 6 -- app/_locales/en_GB/messages.json | 6 -- app/_locales/es/messages.json | 6 -- app/_locales/fr/messages.json | 6 -- app/_locales/hi/messages.json | 6 -- app/_locales/id/messages.json | 6 -- app/_locales/ja/messages.json | 6 -- app/_locales/ko/messages.json | 6 -- app/_locales/pt/messages.json | 6 -- app/_locales/ru/messages.json | 6 -- app/_locales/tl/messages.json | 6 -- app/_locales/tr/messages.json | 6 -- app/_locales/vi/messages.json | 6 -- app/_locales/zh_CN/messages.json | 6 -- .../alert-system/alert-modal/alert-modal.tsx | 9 +- .../app/alert-system/alert-modal/index.scss | 2 - .../confirm-alert-modal.tsx | 9 +- .../general-alert/general-alert.tsx | 2 +- .../multiple-alert-modal.test.tsx | 68 ++++++++++++- .../multiple-alert-modal.tsx | 6 +- ui/hooks/useAlerts.test.ts | 97 +++++++++++++------ ui/hooks/useAlerts.ts | 4 +- .../components/confirm/title/title.test.tsx | 14 ++- .../components/confirm/title/title.tsx | 39 +++----- 26 files changed, 170 insertions(+), 176 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 92cc0f25f1a7..bda0d4d894e7 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Gas-Optionen aktualisieren" }, - "alertBannerMultipleAlertsDescription": { - "message": "Wenn Sie diese Anfrage genehmigen, könnten Dritte, die für Betrügereien bekannt sind, alle Ihre Assets an sich reißen." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Mehrere Benachrichtigungen!" - }, "alertDisableTooltip": { "message": "Dies kann in „Einstellungen > Benachrichtigungen“ geändert werden." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index c7f7137665a4..6010f1939602 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Ενημέρωση επιλογών των τελών συναλλαγών" }, - "alertBannerMultipleAlertsDescription": { - "message": "Εάν εγκρίνετε αυτό το αίτημα, ένας τρίτος που είναι γνωστός για απάτες μπορεί να αποκτήσει όλα τα περιουσιακά σας στοιχεία." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Πολλαπλές ειδοποιήσεις!" - }, "alertDisableTooltip": { "message": "Αυτό μπορεί να αλλάξει στις \"Ρυθμίσεις > Ειδοποιήσεις\"" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index de17cf4ea877..30c913d1de74 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -428,12 +428,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Update gas options" }, - "alertBannerMultipleAlertsDescription": { - "message": "If you approve this request, a third party known for scams might take all your assets." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Multiple alerts!" - }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index d02d9b8c1af5..3c8962e7f7c9 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -400,12 +400,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Update gas options" }, - "alertBannerMultipleAlertsDescription": { - "message": "If you approve this request, a third party known for scams might take all your assets." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Multiple alerts!" - }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 3430b44cad96..772471fdfd65 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Actualizar opciones de gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Si aprueba esta solicitud, un tercero conocido por estafas podría quedarse con todos sus activos." - }, - "alertBannerMultipleAlertsTitle": { - "message": "¡Alertas múltiples!" - }, "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index b2429962bad3..4a537a554315 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Mettre à jour les options de gaz" }, - "alertBannerMultipleAlertsDescription": { - "message": "Si vous approuvez cette demande, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Plusieurs alertes !" - }, "alertDisableTooltip": { "message": "Vous pouvez modifier ceci dans « Paramètres > Alertes »" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 91bcfebef973..7fb1a04cb137 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "गैस के विकल्प को अपडेट करें" }, - "alertBannerMultipleAlertsDescription": { - "message": "यदि आप इस रिक्वेस्ट को एप्रूव करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है।" - }, - "alertBannerMultipleAlertsTitle": { - "message": "एकाधिक एलर्ट!" - }, "alertDisableTooltip": { "message": "इसे \"सेटिंग > अलर्ट\" में बदला जा सकता है" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 82ab45bdfa99..be3ef95ad448 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Perbarui opsi gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Jika Anda menyetujui permintaan ini, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Beberapa peringatan!" - }, "alertDisableTooltip": { "message": "Ini dapat diubah dalam \"Pengaturan > Peringatan\"" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 280889881f57..1ffbc9f1e4eb 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "ガスオプションを更新" }, - "alertBannerMultipleAlertsDescription": { - "message": "このリクエストを承認すると、詐欺が判明しているサードパーティに資産をすべて奪われる可能性があります。" - }, - "alertBannerMultipleAlertsTitle": { - "message": "複数アラート!" - }, "alertDisableTooltip": { "message": "これは「設定」>「アラート」で変更できます" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index c1591b2fc28e..a1c79024f651 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "가스 옵션 업데이트" }, - "alertBannerMultipleAlertsDescription": { - "message": "이 요청을 승인하면 스캠을 목적으로 하는 제3자가 회원님의 자산을 모두 가져갈 수 있습니다." - }, - "alertBannerMultipleAlertsTitle": { - "message": "여러 경고!" - }, "alertDisableTooltip": { "message": "\"설정 > 경고\"에서 변경할 수 있습니다" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 95637cb057f9..52eb392f9d94 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Atualizar opções de gás" }, - "alertBannerMultipleAlertsDescription": { - "message": "Se você aprovar esta solicitação, um terceiro conhecido por aplicar golpes poderá se apropriar de todos os seus ativos." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Vários alertas!" - }, "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 6ce19f83b4ed..9f4f15461bab 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Обновить параметры газа" }, - "alertBannerMultipleAlertsDescription": { - "message": "Если вы одобрите этот запрос, третья сторона, которая, как известно, совершала мошеннические действия, может похитить все ваши активы." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Множественные оповещения!" - }, "alertDisableTooltip": { "message": "Это можно изменить в разделе «Настройки» > «Оповещения»" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 1246c2a085a1..c2ffc42763d0 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "I-update ang mga opsyon sa gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Kung aaprubahan mo ang kahilingang ito, maaaring kunin ng third party na kilala sa mga panloloko ang lahat asset mo." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Iba't ibang alerto!" - }, "alertDisableTooltip": { "message": "Puwede itong baguhin sa \"Mga Setting > Mga Alerto\"" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index eedc60659269..676896deaaae 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Gaz seçeneklerini güncelle" }, - "alertBannerMultipleAlertsDescription": { - "message": "Bu talebi onaylarsanız dolandırıcılıkla bilinen üçüncü bir taraf tüm varlıklarınızı çalabilir." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Çoklu uyarı!" - }, "alertDisableTooltip": { "message": "\"Ayarlar > Uyarılar\" kısmında değiştirilebilir" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 955c302f19a8..442478665c00 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Cập nhật tùy chọn phí gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Nếu bạn chấp thuận yêu cầu này, một bên thứ ba nổi tiếng là lừa đảo có thể lấy hết tài sản của bạn." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Có nhiều cảnh báo!" - }, "alertDisableTooltip": { "message": "Bạn có thể thay đổi trong phần \"Cài đặt > Cảnh báo\"" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 14395afca8b8..9f33ef4a6b35 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "更新燃料选项" }, - "alertBannerMultipleAlertsDescription": { - "message": "如果您批准此请求,以欺诈闻名的第三方可能会拿走您的所有资产。" - }, - "alertBannerMultipleAlertsTitle": { - "message": "多个提醒!" - }, "alertDisableTooltip": { "message": "这可以在“设置 > 提醒”中进行更改" }, diff --git a/ui/components/app/alert-system/alert-modal/alert-modal.tsx b/ui/components/app/alert-system/alert-modal/alert-modal.tsx index 46fbe1f8b8e3..10f5d90c3e77 100644 --- a/ui/components/app/alert-system/alert-modal/alert-modal.tsx +++ b/ui/components/app/alert-system/alert-modal/alert-modal.tsx @@ -157,10 +157,9 @@ function AlertDetails({ <Box key={selectedAlert.key} display={Display.InlineBlock} - padding={2} + padding={customDetails ? 0 : 2} width={BlockSize.Full} backgroundColor={customDetails ? undefined : severityStyle.background} - gap={2} borderRadius={BorderRadius.SM} > {customDetails ?? ( @@ -209,12 +208,11 @@ export function AcknowledgeCheckboxBase({ return ( <Box display={Display.Flex} - padding={3} + padding={4} width={BlockSize.Full} - gap={3} backgroundColor={severityStyle.background} - marginTop={4} borderRadius={BorderRadius.LG} + marginTop={4} > <Checkbox label={label ?? t('alertModalAcknowledge')} @@ -375,6 +373,7 @@ export function AlertModal({ display={Display.Flex} flexDirection={FlexDirection.Column} gap={4} + paddingTop={2} width={BlockSize.Full} > {customAcknowledgeButton ?? ( diff --git a/ui/components/app/alert-system/alert-modal/index.scss b/ui/components/app/alert-system/alert-modal/index.scss index 722dbf763446..c9100ae95345 100644 --- a/ui/components/app/alert-system/alert-modal/index.scss +++ b/ui/components/app/alert-system/alert-modal/index.scss @@ -8,7 +8,5 @@ &__acknowledge-checkbox { @include design-system.H6; - - padding-top: 2px; } } diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx index 96bcebab9953..f84c8113ae1e 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx @@ -90,8 +90,7 @@ function ConfirmDetails({ {t('confirmationAlertModalDetails')} </Text> <ButtonLink - paddingTop={5} - paddingBottom={5} + marginTop={4} size={ButtonLinkSize.Inherit} textProps={{ variant: TextVariant.bodyMd, @@ -103,11 +102,7 @@ function ConfirmDetails({ rel="noopener noreferrer" data-testid="confirm-alert-modal-review-all-alerts" > - <Icon - name={IconName.SecuritySearch} - size={IconSize.Inherit} - marginLeft={1} - /> + <Icon name={IconName.SecuritySearch} size={IconSize.Inherit} /> {t('alertModalReviewAllAlerts')} </ButtonLink> </Box> diff --git a/ui/components/app/alert-system/general-alert/general-alert.tsx b/ui/components/app/alert-system/general-alert/general-alert.tsx index 3ba74445acef..5ac2b2a335fb 100644 --- a/ui/components/app/alert-system/general-alert/general-alert.tsx +++ b/ui/components/app/alert-system/general-alert/general-alert.tsx @@ -27,7 +27,7 @@ export type GeneralAlertProps = { provider?: SecurityProvider; reportUrl?: string; severity: AlertSeverity; - title: string; + title?: string; }; function ReportLink({ diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx index c4b79fb28b7c..3d176e57ccd0 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx @@ -4,6 +4,7 @@ import { fireEvent } from '@testing-library/react'; import { Severity } from '../../../../helpers/constants/design-system'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import mockState from '../../../../../test/data/mock-state.json'; +import * as useAlertsModule from '../../../../hooks/useAlerts'; import { MultipleAlertModal, MultipleAlertModalProps, @@ -84,6 +85,56 @@ describe('MultipleAlertModal', () => { }, }); + it('defaults to the first alert if the selected alert is not found', async () => { + const setAlertConfirmedMock = jest.fn(); + const useAlertsSpy = jest.spyOn(useAlertsModule, 'default'); + const dangerAlertMock = alertsMock.find( + (alert) => alert.key === DATA_ALERT_KEY_MOCK, + ); + (useAlertsSpy as jest.Mock).mockReturnValue({ + setAlertConfirmed: setAlertConfirmedMock, + alerts: alertsMock, + generalAlerts: [], + fieldAlerts: alertsMock, + getFieldAlerts: () => alertsMock, + isAlertConfirmed: () => false, + }); + + const { getByText, queryByText, rerender } = renderWithProvider( + <MultipleAlertModal + {...defaultProps} + alertKey={CONTRACT_ALERT_KEY_MOCK} + />, + mockStore, + ); + + // shows the contract alert + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + + // Update the mock to return only the data alert + (useAlertsSpy as jest.Mock).mockReturnValue({ + setAlertConfirmed: setAlertConfirmedMock, + alerts: [dangerAlertMock], + generalAlerts: [], + fieldAlerts: [dangerAlertMock], + getFieldAlerts: () => [dangerAlertMock], + isAlertConfirmed: () => false, + }); + + // Rerender the component to apply the updated mock + rerender( + <MultipleAlertModal + {...defaultProps} + alertKey={CONTRACT_ALERT_KEY_MOCK} + />, + ); + + // verifies the data alert is shown + expect(queryByText(alertsMock[0].message)).not.toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + useAlertsSpy.mockRestore(); + }); + it('renders the multiple alert modal', () => { const { getByTestId } = renderWithProvider( <MultipleAlertModal {...defaultProps} />, @@ -107,7 +158,7 @@ describe('MultipleAlertModal', () => { expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); }); - it('render the next alert when the "Got it" button is clicked', () => { + it('renders the next alert when the "Got it" button is clicked', () => { const { getByTestId, getByText } = renderWithProvider( <MultipleAlertModal {...defaultProps} alertKey={DATA_ALERT_KEY_MOCK} />, mockStoreAcknowledgeAlerts, @@ -134,6 +185,20 @@ describe('MultipleAlertModal', () => { expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); }); + it('resets to the first alert if there are unconfirmed alerts and the final alert is acknowledged', () => { + const { getByTestId, getByText } = renderWithProvider( + <MultipleAlertModal + {...defaultProps} + alertKey={CONTRACT_ALERT_KEY_MOCK} + />, + mockStore, + ); + + fireEvent.click(getByTestId('alert-modal-button')); + + expect(getByText(alertsMock[0].message)).toBeInTheDocument(); + }); + describe('Navigation', () => { it('calls next alert when the next button is clicked', () => { const { getByTestId, getByText } = renderWithProvider( @@ -144,6 +209,7 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-next-button')); expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); }); it('calls previous alert when the previous button is clicked', () => { diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx index d3b289343d00..62875bffcfe0 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx @@ -162,7 +162,9 @@ export function MultipleAlertModal({ initialAlertIndex === -1 ? 0 : initialAlertIndex, ); - const selectedAlert = alerts[selectedIndex]; + // If the selected alert is not found, default to the first alert + const selectedAlert = alerts[selectedIndex] ?? alerts[0]; + const hasUnconfirmedAlerts = alerts.some( (alert: Alert) => !isAlertConfirmed(alert.key) && alert.severity === Severity.Danger, @@ -207,7 +209,7 @@ export function MultipleAlertModal({ <AlertModal ownerId={ownerId} onAcknowledgeClick={handleAcknowledgeClick} - alertKey={selectedAlert.key} + alertKey={selectedAlert?.key} onClose={onClose} headerStartAccessory={ <PageNavigation diff --git a/ui/hooks/useAlerts.test.ts b/ui/hooks/useAlerts.test.ts index 0e9687a6d874..94f1bb247541 100644 --- a/ui/hooks/useAlerts.test.ts +++ b/ui/hooks/useAlerts.test.ts @@ -56,10 +56,16 @@ describe('useAlerts', () => { ); }; - const { result } = renderHookUseAlert(); + const renderAndReturnResult = ( + ownerId?: string, + state?: { confirmAlerts: ConfirmAlertsState }, + ) => { + return renderHookUseAlert(ownerId, state).result; + }; describe('alerts', () => { it('returns all alerts', () => { + const result = renderAndReturnResult(); expect(result.current.alerts).toEqual(alertsMock); expect(result.current.hasAlerts).toEqual(true); expect(result.current.hasDangerAlerts).toEqual(true); @@ -67,6 +73,7 @@ describe('useAlerts', () => { }); it('returns alerts ordered by severity', () => { + const result = renderAndReturnResult(); const orderedAlerts = result.current.alerts; expect(orderedAlerts[0].severity).toEqual(Severity.Danger); }); @@ -74,7 +81,7 @@ describe('useAlerts', () => { describe('unconfirmedDangerAlerts', () => { it('returns all unconfirmed danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -83,15 +90,15 @@ describe('useAlerts', () => { confirmed: {}, }, }); - expect(result1.current.hasAlerts).toEqual(true); - expect(result1.current.hasUnconfirmedDangerAlerts).toEqual(true); - expect(result1.current.unconfirmedDangerAlerts).toHaveLength(1); + expect(result.current.hasAlerts).toEqual(true); + expect(result.current.hasUnconfirmedDangerAlerts).toEqual(true); + expect(result.current.unconfirmedDangerAlerts).toHaveLength(1); }); }); describe('unconfirmedFieldDangerAlerts', () => { it('returns all unconfirmed field danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -112,7 +119,7 @@ describe('useAlerts', () => { alert.field === fromAlertKeyMock && alert.severity === Severity.Danger, ); - expect(result1.current.unconfirmedFieldDangerAlerts).toEqual([ + expect(result.current.unconfirmedFieldDangerAlerts).toEqual([ expectedFieldDangerAlert, ]); }); @@ -120,7 +127,7 @@ describe('useAlerts', () => { describe('hasUnconfirmedFieldDangerAlerts', () => { it('returns true if there are unconfirmed field danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -136,11 +143,11 @@ describe('useAlerts', () => { }, }, }); - expect(result1.current.hasUnconfirmedFieldDangerAlerts).toEqual(true); + expect(result.current.hasUnconfirmedFieldDangerAlerts).toEqual(true); }); it('returns false if there are no unconfirmed field danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -156,16 +163,43 @@ describe('useAlerts', () => { }, }, }); - expect(result1.current.hasUnconfirmedFieldDangerAlerts).toEqual(false); + expect(result.current.hasUnconfirmedFieldDangerAlerts).toEqual(false); }); }); describe('generalAlerts', () => { - it('returns general alerts', () => { - const expectedGeneralAlerts = alertsMock.find( - (alert) => alert.key === dataAlertKeyMock, - ); - expect(result.current.generalAlerts).toEqual([expectedGeneralAlerts]); + it('returns general alerts sorted by severity', () => { + const warningGeneralAlert = { + key: dataAlertKeyMock, + severity: Severity.Warning as AlertSeverity, + message: 'Alert 2', + }; + const expectedGeneralAlerts = [ + { + ...warningGeneralAlert, + severity: Severity.Info as AlertSeverity, + message: 'Alert 3', + key: fromAlertKeyMock, + }, + { + ...warningGeneralAlert, + severity: Severity.Danger as AlertSeverity, + message: 'Alert 1', + key: toAlertKeyMock, + }, + warningGeneralAlert, + ]; + + const result = renderAndReturnResult(undefined, { + confirmAlerts: { + alerts: { + [ownerIdMock]: expectedGeneralAlerts, + }, + confirmed: {}, + }, + }); + + expect(result.current.generalAlerts).toEqual(expectedGeneralAlerts); }); }); @@ -174,22 +208,26 @@ describe('useAlerts', () => { (alert) => alert.field === fromAlertKeyMock, ); it('returns all alert filtered by field property', () => { + const result = renderAndReturnResult(); expect(result.current.getFieldAlerts(fromAlertKeyMock)).toEqual([ expectedFieldAlerts, ]); }); it('returns empty array if field is not provided', () => { + const result = renderAndReturnResult(); expect(result.current.getFieldAlerts()).toEqual([]); }); it('returns empty array, when no alert for specified field', () => { + const result = renderAndReturnResult(); expect(result.current.getFieldAlerts('mockedField')).toEqual([]); }); }); describe('fieldAlerts', () => { it('returns all alerts with field property', () => { + const result = renderAndReturnResult(); expect(result.current.fieldAlerts).toEqual([ alertsMock[0], alertsMock[2], @@ -197,38 +235,33 @@ describe('useAlerts', () => { }); it('returns empty array if no alerts with field property', () => { - const { result: resultAlerts } = renderHookUseAlert('mockedOwnerId'); - expect(resultAlerts.current.fieldAlerts).toEqual([]); + const result = renderAndReturnResult('mockedOwnerId'); + expect(result.current.fieldAlerts).toEqual([]); }); }); describe('isAlertConfirmed', () => { it('returns an if an alert is confirmed', () => { + const result = renderAndReturnResult(); expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(true); }); it('returns an if an alert is not confirmed', () => { - const { result: resultAlerts } = renderHookUseAlert(ownerId2Mock); - expect(resultAlerts.current.isAlertConfirmed(fromAlertKeyMock)).toBe( - false, - ); + const result = renderAndReturnResult(ownerId2Mock); + expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(false); }); }); describe('setAlertConfirmed', () => { it('dismisses alert confirmation', () => { - const { result: resultAlerts } = renderHookUseAlert(); - resultAlerts.current.setAlertConfirmed(fromAlertKeyMock, false); - expect(resultAlerts.current.isAlertConfirmed(fromAlertKeyMock)).toBe( - false, - ); + const result = renderAndReturnResult(); + result.current.setAlertConfirmed(fromAlertKeyMock, false); + expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(false); }); it('confirms an alert', () => { - const { result: resultAlerts } = renderHookUseAlert(ownerId2Mock); - resultAlerts.current.setAlertConfirmed(fromAlertKeyMock, true); - expect(resultAlerts.current.isAlertConfirmed(fromAlertKeyMock)).toBe( - true, - ); + const result = renderAndReturnResult(ownerId2Mock); + result.current.setAlertConfirmed(fromAlertKeyMock, true); + expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(true); }); }); }); diff --git a/ui/hooks/useAlerts.ts b/ui/hooks/useAlerts.ts index 06d79800f634..4b8e74a46d42 100644 --- a/ui/hooks/useAlerts.ts +++ b/ui/hooks/useAlerts.ts @@ -24,8 +24,8 @@ const useAlerts = (ownerId: string) => { selectConfirmedAlertKeys(state as AlertsState, ownerId), ); - const generalAlerts = useSelector((state) => - selectGeneralAlerts(state as AlertsState, ownerId), + const generalAlerts = sortAlertsBySeverity( + useSelector((state) => selectGeneralAlerts(state as AlertsState, ownerId)), ); const fieldAlerts = sortAlertsBySeverity( diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index eeaab80fd46b..3c03343c2afb 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -162,16 +162,23 @@ describe('ConfirmTitle', () => { reason: 'mock reason', key: 'mock key', }; + + const alertMock2 = { + ...alertMock, + key: 'mock key 2', + reason: 'mock reason 2', + }; const mockAlertState = (state: Partial<ConfirmAlertsState> = {}) => getMockPersonalSignConfirmStateForRequest(unapprovedPersonalSignMsg, { metamask: {}, confirmAlerts: { alerts: { - [unapprovedPersonalSignMsg.id]: [alertMock, alertMock, alertMock], + [unapprovedPersonalSignMsg.id]: [alertMock, alertMock2], }, confirmed: { [unapprovedPersonalSignMsg.id]: { [alertMock.key]: false, + [alertMock2.key]: false, }, }, ...state, @@ -194,7 +201,7 @@ describe('ConfirmTitle', () => { expect(queryByText(alertMock.message)).toBeInTheDocument(); }); - it('renders alert banner when there are multiple alerts', () => { + it('renders multiple alert banner when there are multiple alerts', () => { const mockStore = configureMockStore([])(mockAlertState()); const { getByText } = renderWithConfirmContextProvider( @@ -202,7 +209,8 @@ describe('ConfirmTitle', () => { mockStore, ); - expect(getByText('Multiple alerts!')).toBeInTheDocument(); + expect(getByText(alertMock.reason)).toBeInTheDocument(); + expect(getByText(alertMock2.reason)).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 702c496b4e25..2645feed8a41 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -4,7 +4,6 @@ import { } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; -import { getHighestSeverity } from '../../../../../components/app/alert-system/utils'; import { Box, Text } from '../../../../../components/component-library'; import { TextAlign, @@ -25,37 +24,27 @@ import { getIsRevokeSetApprovalForAll } from '../info/utils'; import { useCurrentSpendingCap } from './hooks/useCurrentSpendingCap'; function ConfirmBannerAlert({ ownerId }: { ownerId: string }) { - const t = useI18nContext(); const { generalAlerts } = useAlerts(ownerId); if (generalAlerts.length === 0) { return null; } - const hasMultipleAlerts = generalAlerts.length > 1; - const singleAlert = generalAlerts[0]; - const highestSeverity = hasMultipleAlerts - ? getHighestSeverity(generalAlerts) - : singleAlert.severity; return ( - <Box marginTop={4}> - <GeneralAlert - data-testid="confirm-banner-alert" - title={ - hasMultipleAlerts - ? t('alertBannerMultipleAlertsTitle') - : singleAlert.reason - } - description={ - hasMultipleAlerts - ? t('alertBannerMultipleAlertsDescription') - : singleAlert.message - } - severity={highestSeverity} - provider={hasMultipleAlerts ? undefined : singleAlert.provider} - details={hasMultipleAlerts ? undefined : singleAlert.alertDetails} - reportUrl={singleAlert.reportUrl} - /> + <Box marginTop={3}> + {generalAlerts.map((alert) => ( + <Box marginTop={1} key={alert.key}> + <GeneralAlert + data-testid="confirm-banner-alert" + title={alert.reason} + description={alert.message} + severity={alert.severity} + provider={alert.provider} + details={alert.alertDetails} + reportUrl={alert.reportUrl} + /> + </Box> + ))} </Box> ); } From 782d03783b9ef3a8e47bb7e1a25e5f8ec808c5e9 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:23:41 +0200 Subject: [PATCH 084/226] fix: test coverage quality gate (#27691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27691?quickstart=1) Fixes an issue with test coverage quality gates. ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3328 ## **Manual testing steps** 1. Test coverage should be correctly reported/validated ## **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. --- .github/workflows/update-coverage.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml index f246bde7eb32..fd1b0d5134e3 100644 --- a/.github/workflows/update-coverage.yml +++ b/.github/workflows/update-coverage.yml @@ -6,10 +6,6 @@ on: - cron: 0 0 * * * workflow_dispatch: -permissions: - contents: write - pull-requests: write - jobs: run-tests: name: Run tests @@ -27,6 +23,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} - name: Update coverage run: | @@ -34,8 +32,8 @@ jobs: - name: Checkout/create branch, commit, and force push run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "MetaMask Bot" + git config user.email "metamaskbot@users.noreply.github.com" git checkout -b metamaskbot/update-coverage git add coverage.json git commit -m "chore: Update coverage.json" @@ -43,6 +41,6 @@ jobs: - name: Create/update pull request env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} run: | gh pr create --title "chore: Update coverage.json" --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." --base develop --head metamaskbot/update-coverage || gh pr edit --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." From 1f741ff5aab1e94b8289dba5acedee621f6fa21f Mon Sep 17 00:00:00 2001 From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:44:51 -0230 Subject: [PATCH 085/226] chore: Update coverage.json (#27696) This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from 0% to 71%. Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com> --- coverage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage.json b/coverage.json index f65ea343e9b3..9887e06e2db6 100644 --- a/coverage.json +++ b/coverage.json @@ -1 +1 @@ -{ "coverage": 0 } +{ "coverage": 71 } From 261e6bfa734cb00daa6a21ef34298e927b090785 Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Tue, 8 Oct 2024 15:37:02 +0200 Subject: [PATCH 086/226] fix(btc): fix address validation (#27690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The `isBtcTestnetAddress` was fragile and was mainly relying on `isBtcMainnetAddress` and `isEthAddress` to work. However if both of those functions were falsy, then `isBtcTestnetAddress` would become truthy (which is not always correct). It was spotted with some testing with a "wrong" eth addresss that we use in some tests: `0x0` ```ts const addr = '0x0'; isEthAddress(addr); // false <- yes, this is false based on this: https://github.com/MetaMask/utils/blob/v9.2.1/src/hex.ts#L15-L22 isBtcMainnetAddress(addr); // false isBtcTestnetAddress(addr); // true <- THIS IS WRONG ``` We now rely on a third party library that will test against multiple BTC format addresses (legacy, segwit, etc...) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27690?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 | 1 + shared/lib/multichain.test.ts | 17 +++++++++++------ shared/lib/multichain.ts | 11 +++-------- yarn.lock | 26 ++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index cc317effc2ae..512b44b1b6ab 100644 --- a/package.json +++ b/package.json @@ -378,6 +378,7 @@ "base32-encode": "^1.2.0", "base64-js": "^1.5.1", "bignumber.js": "^4.1.0", + "bitcoin-address-validation": "^2.2.3", "blo": "1.2.0", "bn.js": "^5.2.1", "bowser": "^2.11.0", diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts index 6c59f506e721..3b982ff8aff3 100644 --- a/shared/lib/multichain.test.ts +++ b/shared/lib/multichain.test.ts @@ -1,22 +1,27 @@ import { isBtcMainnetAddress, isBtcTestnetAddress } from './multichain'; -const MAINNET_ADDRESSES = [ +const BTC_MAINNET_ADDRESSES = [ // P2WPKH 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', // P2PKH '1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ', ]; -const TESTNET_ADDRESSES = [ +const BTC_TESTNET_ADDRESSES = [ // P2WPKH 'tb1q6rmsq3vlfdhjdhtkxlqtuhhlr6pmj09y6w43g8', ]; const ETH_ADDRESSES = ['0x6431726EEE67570BF6f0Cf892aE0a3988F03903F']; +const SOL_ADDRESSES = [ + '7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV', + 'DpNXPNWvWoHaZ9P3WtfGCb2ZdLihW8VW1w1Ph4KDH9iG', +]; + describe('multichain', () => { // @ts-expect-error This is missing from the Mocha type definitions - it.each(MAINNET_ADDRESSES)( + it.each(BTC_MAINNET_ADDRESSES)( 'returns true if address is compatible with BTC mainnet: %s', (address: string) => { expect(isBtcMainnetAddress(address)).toBe(true); @@ -24,7 +29,7 @@ describe('multichain', () => { ); // @ts-expect-error This is missing from the Mocha type definitions - it.each([...TESTNET_ADDRESSES, ...ETH_ADDRESSES])( + it.each([...BTC_TESTNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( 'returns false if address is not compatible with BTC mainnet: %s', (address: string) => { expect(isBtcMainnetAddress(address)).toBe(false); @@ -32,7 +37,7 @@ describe('multichain', () => { ); // @ts-expect-error This is missing from the Mocha type definitions - it.each(TESTNET_ADDRESSES)( + it.each(BTC_TESTNET_ADDRESSES)( 'returns true if address is compatible with BTC testnet: %s', (address: string) => { expect(isBtcTestnetAddress(address)).toBe(true); @@ -40,7 +45,7 @@ describe('multichain', () => { ); // @ts-expect-error This is missing from the Mocha type definitions - it.each([...MAINNET_ADDRESSES, ...ETH_ADDRESSES])( + it.each([...BTC_MAINNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( 'returns false if address is compatible with BTC testnet: %s', (address: string) => { expect(isBtcTestnetAddress(address)).toBe(false); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts index fec52295eada..8ef03509541b 100644 --- a/shared/lib/multichain.ts +++ b/shared/lib/multichain.ts @@ -1,6 +1,4 @@ -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { isEthAddress } from '../../app/scripts/lib/multichain/address'; +import { validate, Network } from 'bitcoin-address-validation'; /** * Returns whether an address is on the Bitcoin mainnet. @@ -14,10 +12,7 @@ import { isEthAddress } from '../../app/scripts/lib/multichain/address'; * @returns `true` if the address is on the Bitcoin mainnet, `false` otherwise. */ export function isBtcMainnetAddress(address: string): boolean { - return ( - !isEthAddress(address) && - (address.startsWith('bc1') || address.startsWith('1')) - ); + return validate(address, Network.mainnet); } /** @@ -29,5 +24,5 @@ export function isBtcMainnetAddress(address: string): boolean { * @returns `true` if the address is on the Bitcoin testnet, `false` otherwise. */ export function isBtcTestnetAddress(address: string): boolean { - return !isEthAddress(address) && !isBtcMainnetAddress(address); + return validate(address, Network.testnet); } diff --git a/yarn.lock b/yarn.lock index 4cae5223a04c..f94e1d68786a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13263,6 +13263,13 @@ __metadata: languageName: node linkType: hard +"base58-js@npm:^1.0.0": + version: 1.0.5 + resolution: "base58-js@npm:1.0.5" + checksum: 10/46c1b39d3a70bca0a47d56069c74a25d547680afd0f28609c90f280f5d614f5de36db5df993fa334db24008a68ab784a72fcdaa13eb40078e03c8999915a1100 + languageName: node + linkType: hard + "base64-arraybuffer-es6@npm:^0.7.0": version: 0.7.0 resolution: "base64-arraybuffer-es6@npm:0.7.0" @@ -13447,6 +13454,17 @@ __metadata: languageName: node linkType: hard +"bitcoin-address-validation@npm:^2.2.3": + version: 2.2.3 + resolution: "bitcoin-address-validation@npm:2.2.3" + dependencies: + base58-js: "npm:^1.0.0" + bech32: "npm:^2.0.0" + sha256-uint8array: "npm:^0.10.3" + checksum: 10/01603b5edf610ecf0843ae546534313f1cffabc8e7435a3678bc9788f18a54e51302218a539794aafd49beb5be70b5d1d507eb7442cb33970fcd665592a71305 + languageName: node + linkType: hard + "bitcoin-ops@npm:^1.3.0, bitcoin-ops@npm:^1.4.1": version: 1.4.1 resolution: "bitcoin-ops@npm:1.4.1" @@ -26204,6 +26222,7 @@ __metadata: base64-js: "npm:^1.5.1" bify-module-groups: "npm:^2.0.0" bignumber.js: "npm:^4.1.0" + bitcoin-address-validation: "npm:^2.2.3" blo: "npm:1.2.0" bn.js: "npm:^5.2.1" bowser: "npm:^2.11.0" @@ -32839,6 +32858,13 @@ __metadata: languageName: node linkType: hard +"sha256-uint8array@npm:^0.10.3": + version: 0.10.7 + resolution: "sha256-uint8array@npm:0.10.7" + checksum: 10/e427f9d2f9c521dea552f033d3f0c3bd641ab214d214dd41bde3c805edde393519cf982b3eee7d683b32e5f28fa23b2278d25935940e13fbe831b216a37832be + languageName: node + linkType: hard + "shallow-clone@npm:^0.1.2": version: 0.1.2 resolution: "shallow-clone@npm:0.1.2" From 83455b8a871187e29000e35d2fef76dd7039d750 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:43:17 +0200 Subject: [PATCH 087/226] test: removing race condition for asserting inner values (PR-#2) (#27664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR fixes an anti-pattern in our e2e tests, where we assert that an element value is equal to a desired value. This opens the door to a race condition where the element is already present, but it does not have the value we want yet, making the assertion to fail. We should find the element by its value, instead of asserting its inner value. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27664?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/19870 Note: this is the second PR for this work. The first PR was merged [here](https://github.com/MetaMask/metamask-extension/pull/27606) ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** 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** - [ ] 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/flask/btc/create-btc-account.spec.ts | 9 ++--- .../signatures/malicious-signatures.spec.ts | 3 +- .../signatures/personal-sign.spec.ts | 3 +- .../signatures/sign-typed-data-v3.spec.ts | 23 +++++------- .../signatures/sign-typed-data-v4.spec.ts | 4 +- .../signatures/sign-typed-data.spec.ts | 3 +- .../signatures/signature-helpers.ts | 9 ++--- .../confirmations/signatures/siwe.spec.ts | 19 +++++----- .../dapp-interactions/dapp-tx-edit.spec.js | 37 ++++--------------- .../dapp-interactions/encrypt-decrypt.spec.js | 28 +++++++------- .../failing-contract.spec.js | 12 ++++-- 11 files changed, 59 insertions(+), 91 deletions(-) diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index a6031a956a37..a4ac650f8f78 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -135,11 +135,10 @@ describe('Create BTC Account', function (this: Suite) { await driver.clickElement( '[data-testid="account-options-menu-button"]', ); - const lockButton = await driver.findClickableElement( - '[data-testid="global-menu-lock"]', - ); - assert.equal(await lockButton.getText(), 'Lock MetaMask'); - await lockButton.click(); + await driver.clickElement({ + css: '[data-testid="global-menu-lock"]', + text: 'Lock MetaMask', + }); await driver.clickElement({ text: 'Forgot password?', diff --git a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts index 053c9f40f8b7..fc8a6d0ab240 100644 --- a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts +++ b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts @@ -50,11 +50,10 @@ describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.SIWE_BadDomain); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const rejectionResult = await driver.waitForSelector({ diff --git a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts index dca5e6ba27d5..418cc4ab513d 100644 --- a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts +++ b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts @@ -74,11 +74,10 @@ describe('Confirmation Signature - Personal Sign @no-mmi', function (this: Suite }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.PersonalSign); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const rejectionResult = await driver.waitForSelector({ diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts index f2c62e617899..6961f0a5eaf2 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts @@ -56,7 +56,6 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); - await driver.delay(1000); await assertSignatureConfirmedMetrics({ driver, mockedEndpoints: mockedEndpoints as MockedEndpoint[], @@ -81,10 +80,9 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: SignatureType.SignTypedDataV3, ); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.delay(1000); await assertSignatureRejectedMetrics({ driver, @@ -141,16 +139,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle('E2E Test Dapp'); await driver.clickElement('#signTypedDataV3Verify'); - await driver.delay(500); - - const verifyResult = await driver.findElement('#signTypedDataV3Result'); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataV3VerifyResult', - ); + await driver.waitForSelector({ + css: '#signTypedDataV3Result', + text: '0x0a22f7796a2a70c8dc918e7e6eb8452c8f2999d1a1eb5ad714473d36270a40d6724472e5609948c778a07216bd082b60b6f6853d6354c731fd8ccdd3a2f4af261b', + }); - assert.equal( - await verifyResult.getText(), - '0x0a22f7796a2a70c8dc918e7e6eb8452c8f2999d1a1eb5ad714473d36270a40d6724472e5609948c778a07216bd082b60b6f6853d6354c731fd8ccdd3a2f4af261b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); + await driver.waitForSelector({ + css: '#signTypedDataV3VerifyResult', + text: publicAddress, + }); } diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts index ca0dbb8f9bb6..33b94be6b332 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts @@ -50,7 +50,6 @@ describe('Confirmation Signature - Sign Typed Data V4 @no-mmi', function (this: await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); - await driver.delay(1000); await assertAccountDetailsMetrics( driver, @@ -87,10 +86,9 @@ describe('Confirmation Signature - Sign Typed Data V4 @no-mmi', function (this: SignatureType.SignTypedDataV4, ); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.delay(1000); await assertSignatureRejectedMetrics({ driver, diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts index a9a9dfd52ae9..1017d44a00dc 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts @@ -76,10 +76,9 @@ describe('Confirmation Signature - Sign Typed Data @no-mmi', function (this: Sui }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.SignTypedData); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.delay(1000); await assertSignatureRejectedMetrics({ driver, diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index d69a2f6a69ac..9b87e5b4e9cc 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -218,11 +218,10 @@ export async function clickHeaderInfoBtn(driver: Driver) { } export async function assertHeaderInfoBalance(driver: Driver) { - const headerBalanceEl = await driver.findElement( - '[data-testid="confirmation-account-details-modal__account-balance"]', - ); - await driver.waitForNonEmptyElement(headerBalanceEl); - assert.equal(await headerBalanceEl.getText(), `${WALLET_ETH_BALANCE}\nETH`); + await driver.waitForSelector({ + css: '[data-testid="confirmation-account-details-modal__account-balance"]', + text: `${WALLET_ETH_BALANCE} ETH`, + }); } export async function copyAddressAndPasteWalletAddress(driver: Driver) { diff --git a/test/e2e/tests/confirmations/signatures/siwe.spec.ts b/test/e2e/tests/confirmations/signatures/siwe.spec.ts index edc3a2020862..1dd545034731 100644 --- a/test/e2e/tests/confirmations/signatures/siwe.spec.ts +++ b/test/e2e/tests/confirmations/signatures/siwe.spec.ts @@ -47,7 +47,6 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); - await driver.delay(1000); await assertVerifiedSiweMessage( driver, @@ -77,18 +76,16 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.SIWE); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement('#siweResult'); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: '#siweResult', + text: 'Error: User rejected the request.', + }); await assertSignatureRejectedMetrics({ driver, mockedEndpoints: mockedEndpoints as MockedEndpoint[], @@ -119,6 +116,8 @@ async function assertVerifiedSiweMessage(driver: Driver, message: string) { await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const verifySigUtil = await driver.findElement('#siweResult'); - assert.equal(await verifySigUtil.getText(), message); + await driver.waitForSelector({ + css: '#siweResult', + text: message, + }); } diff --git a/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js b/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js index df98799a462d..131ebdf4ee73 100644 --- a/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js +++ b/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + logInWithBalanceValidation, openDapp, - unlockWallet, WINDOW_TITLES, + withFixtures, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); const FixtureBuilder = require('../../fixture-builder'); @@ -26,32 +25,22 @@ describe('Editing confirmations of dapp initiated contract interactions', functi const contractAddress = await contractRegistry.getContractAddress( smartContract, ); - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // deploy contract await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent await driver.findClickableElement('#deployButton'); await driver.clickElement('#depositButton'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - const editTransactionButton = await driver.isElementPresentAndVisible( + await driver.assertElementNotPresent( '[data-testid="confirm-page-back-edit-button"]', ); - assert.equal( - editTransactionButton, - false, - `Edit transaction button should not be visible on a contract interaction created by a dapp`, - ); }, ); }); @@ -68,29 +57,19 @@ describe('Editing confirmations of dapp initiated contract interactions', functi title: this.test.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); await openDapp(driver); await driver.clickElement('#sendButton'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Sending ETH', }); - const editTransactionButton = await driver.isElementPresentAndVisible( + await driver.assertElementNotPresent( '[data-testid="confirm-page-back-edit-button"]', ); - assert.equal( - editTransactionButton, - false, - `Edit transaction button should not be visible on a simple send transaction created by a dapp`, - ); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js index fbf11b16cd40..296e36fe4bbe 100644 --- a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js +++ b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -46,11 +45,10 @@ async function decryptMessage(driver) { async function verifyDecryptedMessageMM(driver, message) { await driver.clickElement({ text: 'Decrypt message', tag: 'div' }); - const notificationMessage = await driver.isElementPresent({ + await driver.waitForSelector({ text: message, tag: 'div', }); - assert.equal(notificationMessage, true); await driver.clickElement({ text: 'Decrypt', tag: 'button' }); } @@ -91,10 +89,10 @@ describe('Encrypt Decrypt', function () { await decryptMessage(driver); // Account balance is converted properly - const decryptAccountBalanceLabel = await driver.findElement( - '.request-decrypt-message__balance-value', - ); - assert.equal(await decryptAccountBalanceLabel.getText(), '25 ETH'); + await driver.waitForSelector({ + css: '.request-decrypt-message__balance-value', + text: '25 ETH', + }); // Verify message in MetaMask Notification await verifyDecryptedMessageMM(driver, message); @@ -187,10 +185,10 @@ describe('Encrypt Decrypt', function () { text: 'Request encryption public key', }); // Account balance is converted properly - const accountBalanceLabel = await driver.findElement( - '.request-encryption-public-key__balance-value', - ); - assert.equal(await accountBalanceLabel.getText(), '25 ETH'); + await driver.waitForSelector({ + css: '.request-encryption-public-key__balance-value', + text: '25 ETH', + }); }, ); }); @@ -230,10 +228,10 @@ describe('Encrypt Decrypt', function () { }); // Account balance is converted properly - const accountBalanceLabel = await driver.findElement( - '.request-encryption-public-key__balance-value', - ); - assert.equal(await accountBalanceLabel.getText(), '25 ETH'); + await driver.waitForSelector({ + css: '.request-encryption-public-key__balance-value', + text: '25 ETH', + }); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/failing-contract.spec.js b/test/e2e/tests/dapp-interactions/failing-contract.spec.js index f27768fb7e4c..5770adb1a3b9 100644 --- a/test/e2e/tests/dapp-interactions/failing-contract.spec.js +++ b/test/e2e/tests/dapp-interactions/failing-contract.spec.js @@ -46,11 +46,13 @@ describe('Failing contract interaction ', function () { // display warning when transaction is expected to fail const warningText = 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.'; - const warning = await driver.findElement('.mm-banner-alert .mm-text'); + await driver.waitForSelector({ + css: '.mm-banner-alert .mm-text', + text: warningText, + }); const confirmButton = await driver.findElement( '[data-testid="page-container-footer-next"]', ); - assert.equal(await warning.getText(), warningText); assert.equal(await confirmButton.isEnabled(), false); // dismiss warning and confirm the transaction @@ -113,11 +115,13 @@ describe('Failing contract interaction on non-EIP1559 network', function () { // display warning when transaction is expected to fail const warningText = 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.'; - const warning = await driver.findElement('.mm-banner-alert .mm-text'); + await driver.waitForSelector({ + css: '.mm-banner-alert .mm-text', + text: warningText, + }); const confirmButton = await driver.findElement( '[data-testid="page-container-footer-next"]', ); - assert.equal(await warning.getText(), warningText); assert.equal(await confirmButton.isEnabled(), false); // dismiss warning and confirm the transaction From 29bc2f5b72d4628d5305d90cdc677ae1ae7d2fbd Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:19:08 -0400 Subject: [PATCH 088/226] refactor: Typescript conversion of log-web3-shim-usage.js (#23732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> Part of #23014 Fixes #23470 Converting the level 6 dependency file `app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js` to typescript for contributing to `metamask-controller.js`. ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/23732?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask 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. --- .../handlers/get-provider-state.ts | 3 +- .../handlers/log-web3-shim-usage.js | 48 ------------ .../handlers/log-web3-shim-usage.test.ts | 46 ++++++++++++ .../handlers/log-web3-shim-usage.ts | 74 +++++++++++++++++++ .../rpc-method-middleware/handlers/types.ts | 3 +- 5 files changed, 122 insertions(+), 52 deletions(-) delete mode 100644 app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js create mode 100644 app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts create mode 100644 app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts 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 530b48b25164..c95b66e1a20d 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 @@ -7,7 +7,6 @@ import type { JsonRpcParams, Hex, } from '@metamask/utils'; -import { OriginString } from '@metamask/permission-controller'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { HandlerWrapper, @@ -28,7 +27,7 @@ export type ProviderStateHandlerResult = { }; export type GetProviderState = ( - origin: OriginString, + origin: string, ) => Promise<ProviderStateHandlerResult>; type GetProviderStateConstraint<Params extends JsonRpcParams = JsonRpcParams> = diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js deleted file mode 100644 index e7957192cd56..000000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js +++ /dev/null @@ -1,48 +0,0 @@ -import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; - -/** - * This RPC method is called by the inpage provider whenever it detects the - * accessing of a non-existent property on our window.web3 shim. We use this - * to alert the user that they are using a legacy dapp, and will have to take - * further steps to be able to use it. - */ -const logWeb3ShimUsage = { - methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE], - implementation: logWeb3ShimUsageHandler, - hookNames: { - getWeb3ShimUsageState: true, - setWeb3ShimUsageRecorded: true, - }, -}; -export default logWeb3ShimUsage; - -/** - * @typedef {object} LogWeb3ShimUsageOptions - * @property {Function} getWeb3ShimUsageState - A function that gets web3 shim - * usage state for the given origin. - * @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim - * usage for a particular origin. - */ - -/** - * @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} 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 {LogWeb3ShimUsageOptions} options - */ -function logWeb3ShimUsageHandler( - req, - res, - _next, - end, - { getWeb3ShimUsageState, setWeb3ShimUsageRecorded }, -) { - const { origin } = req; - if (getWeb3ShimUsageState(origin) === undefined) { - setWeb3ShimUsageRecorded(origin); - } - - res.result = true; - return end(); -} 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 new file mode 100644 index 000000000000..d81427af8c26 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts @@ -0,0 +1,46 @@ +import type { JsonRpcEngineEndCallback } from 'json-rpc-engine'; +import { PendingJsonRpcResponse } from '@metamask/utils'; +import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; +import { HandlerRequestType as LogWeb3ShimUsageHandlerRequest } from './types'; +import logWeb3ShimUsage, { + GetWeb3ShimUsageState, + SetWeb3ShimUsageRecorded, +} from './log-web3-shim-usage'; + +describe('logWeb3ShimUsage', () => { + let mockEnd: JsonRpcEngineEndCallback; + let mockGetWeb3ShimUsageState: GetWeb3ShimUsageState; + let mockSetWeb3ShimUsageRecorded: SetWeb3ShimUsageRecorded; + + beforeEach(() => { + mockEnd = jest.fn(); + mockGetWeb3ShimUsageState = jest.fn().mockReturnValue(undefined); + mockSetWeb3ShimUsageRecorded = jest.fn(); + }); + + it('should call getWeb3ShimUsageState and setWeb3ShimUsageRecorded when the handler is invoked', async () => { + const req: LogWeb3ShimUsageHandlerRequest = { + origin: 'testOrigin', + params: [], + id: '22', + jsonrpc: '2.0', + method: MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE, + }; + + const res: PendingJsonRpcResponse<true> = { + id: '22', + jsonrpc: '2.0', + result: true, + }; + + logWeb3ShimUsage.implementation(req, res, jest.fn(), mockEnd, { + getWeb3ShimUsageState: mockGetWeb3ShimUsageState, + setWeb3ShimUsageRecorded: mockSetWeb3ShimUsageRecorded, + }); + + expect(mockGetWeb3ShimUsageState).toHaveBeenCalledWith(req.origin); + expect(mockSetWeb3ShimUsageRecorded).toHaveBeenCalled(); + expect(res.result).toStrictEqual(true); + expect(mockEnd).toHaveBeenCalled(); + }); +}); 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 new file mode 100644 index 000000000000..bff4215ea5aa --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts @@ -0,0 +1,74 @@ +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from 'json-rpc-engine'; +import type { JsonRpcParams, PendingJsonRpcResponse } from '@metamask/utils'; +import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; +import { + HandlerWrapper, + HandlerRequestType as LogWeb3ShimUsageHandlerRequest, +} from './types'; + +export type GetWeb3ShimUsageState = (origin: string) => undefined | 1 | 2; +export type SetWeb3ShimUsageRecorded = (origin: string) => void; + +export type LogWeb3ShimUsageOptions = { + getWeb3ShimUsageState: GetWeb3ShimUsageState; + setWeb3ShimUsageRecorded: SetWeb3ShimUsageRecorded; +}; +type LogWeb3ShimUsageConstraint<Params extends JsonRpcParams = JsonRpcParams> = + { + implementation: ( + req: LogWeb3ShimUsageHandlerRequest<Params>, + res: PendingJsonRpcResponse<true>, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { + getWeb3ShimUsageState, + setWeb3ShimUsageRecorded, + }: LogWeb3ShimUsageOptions, + ) => void; + } & HandlerWrapper; +/** + * This RPC method is called by the inpage provider whenever it detects the + * accessing of a non-existent property on our window.web3 shim. We use this + * to alert the user that they are using a legacy dapp, and will have to take + * further steps to be able to use it. + */ +const logWeb3ShimUsage = { + methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE], + implementation: logWeb3ShimUsageHandler, + hookNames: { + getWeb3ShimUsageState: true, + setWeb3ShimUsageRecorded: true, + }, +} satisfies LogWeb3ShimUsageConstraint; + +export default logWeb3ShimUsage; + +/** + * @param req - The JSON-RPC request object. + * @param res - The JSON-RPC response object. + * @param _next - The json-rpc-engine 'next' callback. + * @param end - The json-rpc-engine 'end' callback. + * @param options + * @param options.getWeb3ShimUsageState - A function that gets web3 shim + * usage state for the given origin. + * @param options.setWeb3ShimUsageRecorded - A function that records web3 shim + * usage for a particular origin. + */ +function logWeb3ShimUsageHandler<Params extends JsonRpcParams = JsonRpcParams>( + req: LogWeb3ShimUsageHandlerRequest<Params>, + res: PendingJsonRpcResponse<true>, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { getWeb3ShimUsageState, setWeb3ShimUsageRecorded }: LogWeb3ShimUsageOptions, +): void { + const { origin } = req; + if (getWeb3ShimUsageState(origin) === undefined) { + setWeb3ShimUsageRecorded(origin); + } + + res.result = true; + return end(); +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/types.ts b/app/scripts/lib/rpc-method-middleware/handlers/types.ts index 46ceef442ec2..91fa9c0dd1cc 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/types.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/types.ts @@ -1,4 +1,3 @@ -import { OriginString } from '@metamask/permission-controller'; import { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { MessageType } from '../../../../../shared/constants/app'; @@ -9,5 +8,5 @@ export type HandlerWrapper = { export type HandlerRequestType<Params extends JsonRpcParams = JsonRpcParams> = Required<JsonRpcRequest<Params>> & { - origin: OriginString; + origin: string; }; From 40e5c51db7970744b3fa269c3105c87aaa624d2e Mon Sep 17 00:00:00 2001 From: Mathieu Artu <mathieu.artu@consensys.net> Date: Tue, 8 Oct 2024 18:58:51 +0200 Subject: [PATCH 089/226] feat(NOTIFY-1193): add profile sync dev menu (#27666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds a new entry in the developer menu dedicated to Profile syncing. The first entry is dedicated to resetting the account sync data. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27666?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/NOTIFY-1193 ## **Manual testing steps** 1. Go to developer settings 2. Reset account sync data 3. Reload or reinstall the extension ## **Screenshots/Recordings** ### **Before** ### **After** ![Capture d’écran 2024-10-07 à 16 48 57](https://github.com/user-attachments/assets/5356d551-05c9-4769-a122-232d68c1c7f8) ## **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 <metamaskbot@users.noreply.github.com> Co-authored-by: Derek Brans <dbrans@gmail.com> Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> Co-authored-by: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Co-authored-by: Howard Braham <howrad@gmail.com> --- app/scripts/metamask-controller.js | 4 + lavamoat/browserify/beta/policy.json | 1 + lavamoat/browserify/flask/policy.json | 1 + lavamoat/browserify/main/policy.json | 1 + lavamoat/browserify/mmi/policy.json | 1 + package.json | 2 +- .../useProfileSyncing.test.tsx | 21 ++++ .../useProfileSyncing.ts | 27 +++++ .../developer-options-tab.test.tsx.snap | 49 ++++++++++ .../developer-options-tab.tsx | 3 + .../developer-options-tab/profile-sync.tsx | 98 +++++++++++++++++++ ui/store/actions.test.js | 27 +++++ ui/store/actions.ts | 28 ++++++ yarn.lock | 10 +- 14 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 ui/pages/settings/developer-options-tab/profile-sync.tsx diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 31dc5fef3fcd..f8f93cbaa5b3 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4035,6 +4035,10 @@ export default class MetamaskController extends EventEmitter { userStorageController.syncInternalAccountsWithUserStorage.bind( userStorageController, ), + deleteAccountSyncingDataFromUserStorage: + userStorageController.performDeleteStorageAllFeatureEntries.bind( + userStorageController, + ), // NotificationServicesController checkAccountsPresence: diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index c8c97ce1dd8a..e98080fc4d5f 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2036,6 +2036,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index c8c97ce1dd8a..e98080fc4d5f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2036,6 +2036,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index c8c97ce1dd8a..e98080fc4d5f 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2036,6 +2036,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7478c04ea3aa..43297351bf21 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2128,6 +2128,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/package.json b/package.json index 512b44b1b6ab..2a90e76b51b3 100644 --- a/package.json +++ b/package.json @@ -344,7 +344,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", "@metamask/preinstalled-example-snap": "^0.1.0", - "@metamask/profile-sync-controller": "^0.9.4", + "@metamask/profile-sync-controller": "^0.9.6", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx index 481ad5deec9f..951cec333ade 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx +++ b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx @@ -9,6 +9,7 @@ import { useEnableProfileSyncing, useDisableProfileSyncing, useAccountSyncingEffect, + useDeleteAccountSyncingDataFromUserStorage, } from './useProfileSyncing'; const middlewares = [thunk]; @@ -22,6 +23,7 @@ jest.mock('../../store/actions', () => ({ showLoadingIndication: jest.fn(), hideLoadingIndication: jest.fn(), syncInternalAccountsWithUserStorage: jest.fn(), + deleteAccountSyncingDataFromUserStorage: jest.fn(), })); type ArrangeMocksMetamaskStateOverrides = { @@ -132,4 +134,23 @@ describe('useProfileSyncing', () => { ).not.toHaveBeenCalled(); }); }); + + it('should dispatch account sync data deletion', async () => { + const { store } = arrangeMocks(); + + const { result } = renderHook( + () => useDeleteAccountSyncingDataFromUserStorage(), + { + wrapper: ({ children }) => ( + <Provider store={store}>{children}</Provider> + ), + }, + ); + + act(() => { + result.current.dispatchDeleteAccountSyncingDataFromUserStorage(); + }); + + expect(actions.deleteAccountSyncingDataFromUserStorage).toHaveBeenCalled(); + }); }); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing.ts index 1306e160cb5e..67899aa73927 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing.ts +++ b/ui/hooks/metamask-notifications/useProfileSyncing.ts @@ -8,6 +8,7 @@ import { setIsProfileSyncingEnabled as setIsProfileSyncingEnabledAction, hideLoadingIndication, syncInternalAccountsWithUserStorage, + deleteAccountSyncingDataFromUserStorage, } from '../../store/actions'; import { selectIsSignedIn } from '../../selectors/metamask-notifications/authentication'; @@ -176,6 +177,32 @@ export const useAccountSyncing = () => { }; }; +/** + * Custom hook to delete a user's account syncing data from user storage + */ + +export const useDeleteAccountSyncingDataFromUserStorage = () => { + const dispatch = useDispatch(); + + const [error, setError] = useState<unknown>(null); + + const dispatchDeleteAccountSyncingDataFromUserStorage = useCallback(() => { + setError(null); + + try { + dispatch(deleteAccountSyncingDataFromUserStorage()); + } catch (e) { + log.error(e); + setError(e instanceof Error ? e.message : 'An unexpected error occurred'); + } + }, [dispatch]); + + return { + dispatchDeleteAccountSyncingDataFromUserStorage, + error, + }; +}; + /** * Custom hook to apply account syncing effect. */ diff --git a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap index f8cd5cd61006..4eea2d9cf7d1 100644 --- a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap +++ b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap @@ -240,6 +240,55 @@ exports[`Develop options tab should match snapshot 1`] = ` </div> </div> </div> + <p + class="mm-box mm-text settings-page__security-tab-sub-header__bold mm-text--body-md mm-box--color-text-default" + > + Profile Sync + </p> + <div + class="settings-page__content-padded" + > + <div + class="mm-box settings-page__content-row mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-row mm-box--justify-content-space-between" + > + <div + class="settings-page__content-item" + > + <span> + Account syncing + </span> + <div + class="settings-page__content-description" + > + Deletes all user storage entries for the current SRP. This can help if you tested Account Syncing early on and have corrupted data. This will not remove internal accounts already created and renamed. If you want to start from scratch with only the first account and restart syncing from this point on, you will need to reinstall the extension after this action. + </div> + </div> + <div + class="settings-page__content-item-col" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-primary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" + data-theme="light" + > + Reset + </button> + </div> + <div + class="settings-page__content-item-col" + > + <div + class="mm-box mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--align-items-center" + style="height: 40px; width: 40px;" + > + <span + class="mm-box settings-page-developer-options__icon-check mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-success-default" + hidden="" + style="mask-image: url('./images/icons/check.svg');" + /> + </div> + </div> + </div> + </div> <p class="mm-box mm-text settings-page__security-tab-sub-header__bold mm-text--body-md mm-box--color-text-default" > diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx index fa5d58406a14..a88d735a628f 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx @@ -39,6 +39,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm'; import ToggleRow from './developer-options-toggle-row-component'; import { SentryTest } from './sentry-test'; +import { ProfileSyncDevSettings } from './profile-sync'; /** * Settings Page for Developer Options (internal-only) @@ -260,6 +261,8 @@ const DeveloperOptionsTab = () => { {renderServiceWorkerKeepAliveToggle()} {renderEnableConfirmationsRedesignToggle()} </div> + + <ProfileSyncDevSettings /> <SentryTest /> </div> ); diff --git a/ui/pages/settings/developer-options-tab/profile-sync.tsx b/ui/pages/settings/developer-options-tab/profile-sync.tsx new file mode 100644 index 000000000000..a5a4f8893f15 --- /dev/null +++ b/ui/pages/settings/developer-options-tab/profile-sync.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useState } from 'react'; + +import { + Box, + Button, + ButtonVariant, + Icon, + IconName, + IconSize, + Text, +} from '../../../components/component-library'; + +import { + IconColor, + Display, + FlexDirection, + JustifyContent, + AlignItems, +} from '../../../helpers/constants/design-system'; +import { useDeleteAccountSyncingDataFromUserStorage } from '../../../hooks/metamask-notifications/useProfileSyncing'; + +const AccountSyncDeleteDataFromUserStorage = () => { + const [hasDeletedAccountSyncEntries, setHasDeletedAccountSyncEntries] = + useState(false); + + const { dispatchDeleteAccountSyncingDataFromUserStorage } = + useDeleteAccountSyncingDataFromUserStorage(); + + const handleDeleteAccountSyncingDataFromUserStorage = + useCallback(async () => { + await dispatchDeleteAccountSyncingDataFromUserStorage(); + setHasDeletedAccountSyncEntries(true); + }, [ + dispatchDeleteAccountSyncingDataFromUserStorage, + setHasDeletedAccountSyncEntries, + ]); + + return ( + <div className="settings-page__content-padded"> + <Box + className="settings-page__content-row" + display={Display.Flex} + flexDirection={FlexDirection.Row} + justifyContent={JustifyContent.spaceBetween} + gap={4} + > + <div className="settings-page__content-item"> + <span>Account syncing</span> + <div className="settings-page__content-description"> + Deletes all user storage entries for the current SRP. This can help + if you tested Account Syncing early on and have corrupted data. This + will not remove internal accounts already created and renamed. If + you want to start from scratch with only the first account and + restart syncing from this point on, you will need to reinstall the + extension after this action. + </div> + </div> + + <div className="settings-page__content-item-col"> + <Button + variant={ButtonVariant.Primary} + onClick={handleDeleteAccountSyncingDataFromUserStorage} + > + Reset + </Button> + </div> + <div className="settings-page__content-item-col"> + <Box + display={Display.Flex} + alignItems={AlignItems.center} + paddingLeft={2} + paddingRight={2} + style={{ height: '40px', width: '40px' }} + > + <Icon + className="settings-page-developer-options__icon-check" + name={IconName.Check} + color={IconColor.successDefault} + size={IconSize.Lg} + hidden={!hasDeletedAccountSyncEntries} + /> + </Box> + </div> + </Box> + </div> + ); +}; + +export const ProfileSyncDevSettings = () => { + return ( + <> + <Text className="settings-page__security-tab-sub-header__bold"> + Profile Sync + </Text> + <AccountSyncDeleteDataFromUserStorage /> + </> + ); +}; diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index a136287f039c..8d72ce63e32d 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -2539,6 +2539,33 @@ describe('Actions', () => { }); }); + describe('deleteAccountSyncingDataFromUserStorage', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls deleteAccountSyncingDataFromUserStorage in the background', async () => { + const store = mockStore(); + + const deleteAccountSyncingDataFromUserStorageStub = sinon + .stub() + .callsFake((_, cb) => { + return cb(); + }); + + background.getApi.returns({ + deleteAccountSyncingDataFromUserStorage: + deleteAccountSyncingDataFromUserStorageStub, + }); + setBackgroundConnection(background.getApi()); + + await store.dispatch(actions.deleteAccountSyncingDataFromUserStorage()); + expect( + deleteAccountSyncingDataFromUserStorageStub.calledOnceWith('accounts'), + ).toBe(true); + }); + }); + describe('removePermittedChain', () => { afterEach(() => { sinon.restore(); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index e64d366a7c74..a8fadb95ddeb 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -5462,6 +5462,34 @@ export function syncInternalAccountsWithUserStorage(): ThunkAction< }; } +/** + * Delete all of current user's accounts data from user storage. + * + * This function sends a request to the background script to sync accounts data and update the state accordingly. + * If the operation encounters an error, it logs the error message and rethrows the error to ensure it is handled appropriately. + * + * @returns A thunk action that, when dispatched, attempts to synchronize accounts data with user storage between devices. + */ +export function deleteAccountSyncingDataFromUserStorage(): ThunkAction< + void, + MetaMaskReduxState, + unknown, + AnyAction +> { + return async () => { + try { + const response = await submitRequestToBackground( + 'deleteAccountSyncingDataFromUserStorage', + ['accounts'], + ); + return response; + } catch (error) { + logErrorWithMessage(error); + throw error; + } + }; +} + /** * Marks MetaMask notifications as read. * diff --git a/yarn.lock b/yarn.lock index f94e1d68786a..90fe3bc46886 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6028,9 +6028,9 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^0.9.4": - version: 0.9.4 - resolution: "@metamask/profile-sync-controller@npm:0.9.4" +"@metamask/profile-sync-controller@npm:^0.9.6": + version: 0.9.6 + resolution: "@metamask/profile-sync-controller@npm:0.9.6" dependencies: "@metamask/base-controller": "npm:^7.0.1" "@metamask/keyring-api": "npm:^8.1.3" @@ -6046,7 +6046,7 @@ __metadata: "@metamask/accounts-controller": ^18.1.1 "@metamask/keyring-controller": ^17.2.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/86079da552eed316f2754bd899047de1d8d9d15d390c9cdee0aef66b95bea708b5c7929a8d8d946210cc0e4c52347fee971a5cf5130149d0ca60abdc85f47774 + checksum: 10/102572a8805dde33eb318bf87ff2cd14cd5d5eae9139f18641c72a166ffa42dd4365d7617407d98521f3ec5e9b1d46517b283742be32825faf276141413bab51 languageName: node linkType: hard @@ -26111,7 +26111,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" "@metamask/preinstalled-example-snap": "npm:^0.1.0" - "@metamask/profile-sync-controller": "npm:^0.9.4" + "@metamask/profile-sync-controller": "npm:^0.9.6" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" From ad5303761d5101b8e40df64462c43c548d607bc5 Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:54:01 -0400 Subject: [PATCH 090/226] fix: Disable redirecting Extension users using beta & flask build and dev env to the existing offboarding page (#27226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this PR, we are ensuring that only `build-main` is redirected to the offboarding page. Also, the user will not be redirected to the offboarding page while uninstalling a development build. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27226?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3239 ## **Manual testing steps** For development build: 1. Load the application after running `yarn start` 2. Create/import a wallet. 3. Uninstall the extension 4. Verify that the user is not redirected to the offboarding page. For a production-like build (for which the offboarding page will be loaded) 1. Load the application after running `yarn build:test` 2. Crate/import a wallet. 3. Uninstall the extension 4. Verify that the user is redirected to the offboarding page. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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/controllers/metametrics.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index ef1dbe02789a..28ced592fb9d 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -28,6 +28,10 @@ import { TransactionMetaMetricsEvent, } from '../../../shared/constants/transaction'; +///: BEGIN:ONLY_INCLUDE_IF(build-main) +import { ENVIRONMENT } from '../../../development/build/constants'; +///: END:ONLY_INCLUDE_IF + const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled'; export const overrideAnonymousEventNames = { @@ -484,8 +488,10 @@ export default class MetaMetricsController { this.setMarketingCampaignCookieId(null); } - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - this.updateExtensionUninstallUrl(participateInMetaMetrics, metaMetricsId); + ///: BEGIN:ONLY_INCLUDE_IF(build-main) + if (this.environment !== ENVIRONMENT.DEVELOPMENT) { + this.updateExtensionUninstallUrl(participateInMetaMetrics, metaMetricsId); + } ///: END:ONLY_INCLUDE_IF return metaMetricsId; From 83d53314a2a12c07c01279c830b5809c4e787a7a Mon Sep 17 00:00:00 2001 From: Matthew Walsh <matthew.walsh@consensys.net> Date: Tue, 8 Oct 2024 19:55:37 +0100 Subject: [PATCH 091/226] perf: add tags to UI startup trace (#27550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add the following tags to the `UI Startup` trace to help identify correlations in startup performance: - `wallet.account_count` - Total number of all accounts in wallet. - `wallet.nft_count` - Total number of all NFTs in the wallet, across all accounts and chains. - `wallet.notification_count` - Total number of notifications in the wallet. - `wallet.pending_approval` - Approval type of the first pending approval. e.g. `transaction`, `eth_signTypedData` - `wallet.token_count` - Total number of ERC-20 tokens in the wallet, across all chains and accounts. - `wallet.transaction_count` - Total number of transactions currently in the wallet, across all accounts, chains, and statuses. - `wallet.unlocked` - `true` or `false` based on if the wallet is currently locked and requires a password. - `wallet.ui_type` - Type of UI being loaded. e.g. `popup`, `notification`, `fullscreen` Tags with a `number` value are Sentry measurements to allow querying with greater than and less than logic. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27550?quickstart=1) ## **Related issues** Fixes: [#3379](https://github.com/MetaMask/MetaMask-planning/issues/3379) [#3273](https://github.com/MetaMask/MetaMask-planning/issues/3273) ## **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 - [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: Mark Stacey <markjstacey@gmail.com> --- app/scripts/lib/setupSentry.js | 3 +- package.json | 6 +- shared/lib/trace.test.ts | 38 ++++-- shared/lib/trace.ts | 131 ++++++++++++++++++++- ui/helpers/utils/tags.test.ts | 206 +++++++++++++++++++++++++++++++++ ui/helpers/utils/tags.ts | 42 +++++++ ui/index.js | 9 +- ui/selectors/nft.test.ts | 2 + ui/selectors/nft.ts | 37 +++++- ui/selectors/selectors.js | 18 +++ ui/store/store.ts | 85 +++++++------- yarn.lock | 12 +- 12 files changed, 525 insertions(+), 64 deletions(-) create mode 100644 ui/helpers/utils/tags.test.ts create mode 100644 ui/helpers/utils/tags.ts diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 14e3bc0934d8..e6f4a0d4524e 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -302,7 +302,7 @@ async function getMetaMetricsEnabled() { function setSentryClient() { const clientOptions = getClientOptions(); - const { dsn, environment, release } = clientOptions; + const { dsn, environment, release, tracesSampleRate } = clientOptions; /** * Sentry throws on initialization as it wants to avoid polluting the global namespace and @@ -322,6 +322,7 @@ function setSentryClient() { environment, dsn, release, + tracesSampleRate, }); Sentry.registerSpanErrorInstrumentation(); diff --git a/package.json b/package.json index 2a90e76b51b3..0bc5cc2f07ac 100644 --- a/package.json +++ b/package.json @@ -367,9 +367,9 @@ "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "patch:@reduxjs/toolkit@npm%3A1.9.7#~/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch", "@segment/loosely-validate-event": "^2.0.0", - "@sentry/browser": "^8.19.0", - "@sentry/types": "^8.19.0", - "@sentry/utils": "^8.19.0", + "@sentry/browser": "^8.33.1", + "@sentry/types": "^8.33.1", + "@sentry/utils": "^8.33.1", "@swc/core": "1.4.11", "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", "@zxing/browser": "^0.1.4", diff --git a/shared/lib/trace.test.ts b/shared/lib/trace.test.ts index 5154a930b7f9..7cd39eba03d1 100644 --- a/shared/lib/trace.test.ts +++ b/shared/lib/trace.test.ts @@ -1,4 +1,5 @@ import { + setMeasurement, Span, startSpan, startSpanManual, @@ -10,6 +11,7 @@ jest.mock('@sentry/browser', () => ({ withIsolationScope: jest.fn(), startSpan: jest.fn(), startSpanManual: jest.fn(), + setMeasurement: jest.fn(), })); const NAME_MOCK = TraceName.Transaction; @@ -32,7 +34,8 @@ describe('Trace', () => { const startSpanMock = jest.mocked(startSpan); const startSpanManualMock = jest.mocked(startSpanManual); const withIsolationScopeMock = jest.mocked(withIsolationScope); - const setTagsMock = jest.fn(); + const setMeasurementMock = jest.mocked(setMeasurement); + const setTagMock = jest.fn(); beforeEach(() => { jest.resetAllMocks(); @@ -41,13 +44,20 @@ describe('Trace', () => { startSpan: startSpanMock, startSpanManual: startSpanManualMock, withIsolationScope: withIsolationScopeMock, + setMeasurement: setMeasurementMock, }; startSpanMock.mockImplementation((_, fn) => fn({} as Span)); + startSpanManualMock.mockImplementation((_, fn) => + fn({} as Span, () => { + // Intentionally empty + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any withIsolationScopeMock.mockImplementation((fn: any) => - fn({ setTags: setTagsMock }), + fn({ setTag: setTagMock }), ); }); @@ -91,8 +101,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); it('invokes Sentry if no callback provided', () => { @@ -117,8 +131,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); it('invokes Sentry if no callback provided with custom start time', () => { @@ -145,8 +163,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); }); diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index 0c667a346235..a067858a969c 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -1,10 +1,13 @@ import * as Sentry from '@sentry/browser'; -import { Primitive, StartSpanOptions } from '@sentry/types'; +import { MeasurementUnit, StartSpanOptions } from '@sentry/types'; import { createModuleLogger } from '@metamask/utils'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { log as sentryLogger } from '../../app/scripts/lib/setupSentry'; +/** + * The supported trace names. + */ export enum TraceName { BackgroundConnect = 'Background Connect', DeveloperTest = 'Developer Test', @@ -36,22 +39,71 @@ type PendingTrace = { startTime: number; }; +/** + * A context object to associate traces with each other and generate nested traces. + */ export type TraceContext = unknown; +/** + * A callback function that can be traced. + */ export type TraceCallback<T> = (context?: TraceContext) => T; +/** + * A request to create a new trace. + */ export type TraceRequest = { + /** + * Custom data to associate with the trace. + */ data?: Record<string, number | string | boolean>; + + /** + * A unique identifier when not tracing a callback. + * Defaults to 'default' if not provided. + */ id?: string; + + /** + * The name of the trace. + */ name: TraceName; + + /** + * The parent context of the trace. + * If provided, the trace will be nested under the parent trace. + */ parentContext?: TraceContext; + + /** + * Override the start time of the trace. + */ startTime?: number; + + /** + * Custom tags to associate with the trace. + */ tags?: Record<string, number | string | boolean>; }; +/** + * A request to end a pending trace. + */ export type EndTraceRequest = { + /** + * The unique identifier of the trace. + * Defaults to 'default' if not provided. + */ id?: string; + + /** + * The name of the trace. + */ name: TraceName; + + /** + * Override the end time of the trace. + */ timestamp?: number; }; @@ -59,6 +111,16 @@ export function trace<T>(request: TraceRequest, fn: TraceCallback<T>): T; export function trace(request: TraceRequest): TraceContext; +/** + * Create a Sentry transaction to analyse the duration of a code flow. + * If a callback is provided, the transaction will be automatically ended when the callback completes. + * If the callback returns a promise, the transaction will be ended when the promise resolves or rejects. + * If no callback is provided, the transaction must be manually ended using `endTrace`. + * + * @param request - The data associated with the trace, such as the name and tags. + * @param fn - The optional callback to record the duration of. + * @returns The context of the trace, or the result of the callback if provided. + */ export function trace<T>( request: TraceRequest, fn?: TraceCallback<T>, @@ -70,6 +132,12 @@ export function trace<T>( return traceCallback(request, fn); } +/** + * End a pending trace that was started without a callback. + * Does nothing if the pending trace cannot be found. + * + * @param request - The data necessary to identify and end the pending trace. + */ export function endTrace(request: EndTraceRequest) { const { name, timestamp } = request; const id = getTraceId(request); @@ -101,6 +169,10 @@ function traceCallback<T>(request: TraceRequest, fn: TraceCallback<T>): T { const start = Date.now(); let error: unknown; + if (span) { + initSpan(span, request); + } + return tryCatchMaybePromise<T>( () => fn(span), (currentError) => { @@ -131,6 +203,10 @@ function startTrace(request: TraceRequest): TraceContext { span?.end(timestamp); }; + if (span) { + initSpan(span, request); + } + const pendingTrace = { end, request, startTime }; const key = getTraceKey(request); tracesByKey.set(key, pendingTrace); @@ -149,7 +225,7 @@ function startSpan<T>( request: TraceRequest, callback: (spanOptions: StartSpanOptions) => T, ) { - const { data: attributes, name, parentContext, startTime, tags } = request; + const { data: attributes, name, parentContext, startTime } = request; const parentSpan = (parentContext ?? null) as Sentry.Span | null; const spanOptions: StartSpanOptions = { @@ -161,8 +237,7 @@ function startSpan<T>( }; return sentryWithIsolationScope((scope: Sentry.Scope) => { - scope.setTags(tags as Record<string, Primitive>); - + initScope(scope, request); return callback(spanOptions); }); } @@ -182,6 +257,40 @@ function getPerformanceTimestamp(): number { return performance.timeOrigin + performance.now(); } +/** + * Initialise the isolated Sentry scope created for each trace. + * Includes setting all non-numeric tags. + * + * @param scope - The Sentry scope to initialise. + * @param request - The trace request. + */ +function initScope(scope: Sentry.Scope, request: TraceRequest) { + const tags = request.tags ?? {}; + + for (const [key, value] of Object.entries(tags)) { + if (typeof value !== 'number') { + scope.setTag(key, value); + } + } +} + +/** + * Initialise the Sentry span created for each trace. + * Includes setting all numeric tags as measurements so they can be queried numerically in Sentry. + * + * @param _span - The Sentry span to initialise. + * @param request - The trace request. + */ +function initSpan(_span: Sentry.Span, request: TraceRequest) { + const tags = request.tags ?? {}; + + for (const [key, value] of Object.entries(tags)) { + if (typeof value === 'number') { + sentrySetMeasurement(key, value, 'none'); + } + } +} + function tryCatchMaybePromise<T>( tryFn: () => T, catchFn: (error: unknown) => void, @@ -251,3 +360,17 @@ function sentryWithIsolationScope<T>(callback: (scope: Sentry.Scope) => T): T { return actual(callback); } + +function sentrySetMeasurement( + key: string, + value: number, + unit: MeasurementUnit, +) { + const actual = globalThis.sentry?.setMeasurement; + + if (!actual) { + return; + } + + actual(key, value, unit); +} diff --git a/ui/helpers/utils/tags.test.ts b/ui/helpers/utils/tags.test.ts new file mode 100644 index 000000000000..eae5e90f9ea1 --- /dev/null +++ b/ui/helpers/utils/tags.test.ts @@ -0,0 +1,206 @@ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../shared/constants/app'; +import { MetaMaskReduxState } from '../../store/store'; +import { getStartupTraceTags } from './tags'; + +jest.mock('../../../app/scripts/lib/util', () => ({ + ...jest.requireActual('../../../app/scripts/lib/util'), + getEnvironmentType: jest.fn(), +})); + +const STATE_EMPTY_MOCK = { + metamask: { + allTokens: {}, + internalAccounts: { + accounts: {}, + }, + metamaskNotificationsList: [], + }, +} as unknown as MetaMaskReduxState; + +function createMockState( + metamaskState: Partial<MetaMaskReduxState['metamask']>, +): MetaMaskReduxState { + return { + ...STATE_EMPTY_MOCK, + metamask: { + ...STATE_EMPTY_MOCK.metamask, + ...metamaskState, + }, + }; +} + +describe('Tags Utils', () => { + const getEnvironmentTypeMock = jest.mocked(getEnvironmentType); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('getStartupTraceTags', () => { + it('includes UI type', () => { + getEnvironmentTypeMock.mockReturnValue(ENVIRONMENT_TYPE_FULLSCREEN); + + const tags = getStartupTraceTags(STATE_EMPTY_MOCK); + + expect(tags['wallet.ui_type']).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN); + }); + + it('includes if unlocked', () => { + const state = createMockState({ isUnlocked: true }); + const tags = getStartupTraceTags(state); + + expect(tags['wallet.unlocked']).toStrictEqual(true); + }); + + it('includes if not unlocked', () => { + const state = createMockState({ isUnlocked: false }); + const tags = getStartupTraceTags(state); + + expect(tags['wallet.unlocked']).toStrictEqual(false); + }); + + it('includes pending approval type', () => { + const state = createMockState({ + pendingApprovals: { + 1: { + type: 'eth_sendTransaction', + }, + } as unknown as MetaMaskReduxState['metamask']['pendingApprovals'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.pending_approval']).toStrictEqual( + 'eth_sendTransaction', + ); + }); + + it('includes first pending approval type if multiple', () => { + const state = createMockState({ + pendingApprovals: { + 1: { + type: 'eth_sendTransaction', + }, + 2: { + type: 'personal_sign', + }, + } as unknown as MetaMaskReduxState['metamask']['pendingApprovals'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.pending_approval']).toStrictEqual( + 'eth_sendTransaction', + ); + }); + + it('includes account count', () => { + const state = createMockState({ + internalAccounts: { + accounts: { + '0x1234': {}, + '0x4321': {}, + }, + } as unknown as MetaMaskReduxState['metamask']['internalAccounts'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.account_count']).toStrictEqual(2); + }); + + it('includes nft count', () => { + const state = createMockState({ + allNfts: { + '0x1234': { + '0x1': [ + { + tokenId: '1', + }, + { + tokenId: '2', + }, + ], + '0x2': [ + { + tokenId: '3', + }, + { + tokenId: '4', + }, + ], + }, + '0x4321': { + '0x3': [ + { + tokenId: '5', + }, + ], + }, + } as unknown as MetaMaskReduxState['metamask']['allNfts'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.nft_count']).toStrictEqual(5); + }); + + it('includes notification count', () => { + const state = createMockState({ + metamaskNotificationsList: [ + {}, + {}, + {}, + ] as unknown as MetaMaskReduxState['metamask']['metamaskNotificationsList'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.notification_count']).toStrictEqual(3); + }); + + it('includes token count', () => { + const state = createMockState({ + allTokens: { + '0x1': { + '0x1234': [{}, {}], + '0x4321': [{}], + }, + '0x2': { + '0x5678': [{}], + }, + } as unknown as MetaMaskReduxState['metamask']['allTokens'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.token_count']).toStrictEqual(4); + }); + + it('includes transaction count', () => { + const state = createMockState({ + transactions: [ + { + id: 1, + chainId: '0x1', + }, + { + id: 2, + chainId: '0x1', + }, + { + id: 3, + chainId: '0x2', + }, + ] as unknown as MetaMaskReduxState['metamask']['transactions'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.transaction_count']).toStrictEqual(3); + }); + }); +}); diff --git a/ui/helpers/utils/tags.ts b/ui/helpers/utils/tags.ts new file mode 100644 index 000000000000..4a253e214d82 --- /dev/null +++ b/ui/helpers/utils/tags.ts @@ -0,0 +1,42 @@ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../app/scripts/lib/util'; +import { getIsUnlocked } from '../../ducks/metamask/metamask'; +import { + getInternalAccounts, + getPendingApprovals, + getTransactions, + selectAllTokensFlat, +} from '../../selectors'; +import { getMetamaskNotifications } from '../../selectors/metamask-notifications/metamask-notifications'; +import { selectAllNftsFlat } from '../../selectors/nft'; +import { MetaMaskReduxState } from '../../store/store'; + +/** + * Generate the required tags for the UI startup trace. + * + * @param state - The current flattened UI state. + * @returns The tags for the startup trace. + */ +export function getStartupTraceTags(state: MetaMaskReduxState) { + const uiType = getEnvironmentType(); + const unlocked = getIsUnlocked(state) as boolean; + const accountCount = getInternalAccounts(state).length; + const nftCount = selectAllNftsFlat(state).length; + const notificationCount = getMetamaskNotifications(state).length; + const tokenCount = selectAllTokensFlat(state).length as number; + const transactionCount = getTransactions(state).length; + const pendingApprovals = getPendingApprovals(state); + const firstApprovalType = pendingApprovals?.[0]?.type; + + return { + 'wallet.account_count': accountCount, + 'wallet.nft_count': nftCount, + 'wallet.notification_count': notificationCount, + 'wallet.pending_approval': firstApprovalType, + 'wallet.token_count': tokenCount, + 'wallet.transaction_count': transactionCount, + 'wallet.unlocked': unlocked, + 'wallet.ui_type': uiType, + }; +} diff --git a/ui/index.js b/ui/index.js index 5cb576e488d6..8cf2048cba41 100644 --- a/ui/index.js +++ b/ui/index.js @@ -39,6 +39,7 @@ import { import Root from './pages'; import txHelper from './helpers/utils/tx-helper'; import { setBackgroundConnection } from './store/background-connection'; +import { getStartupTraceTags } from './helpers/utils/tags'; log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn', false); @@ -182,8 +183,14 @@ export async function setupInitialStore( async function startApp(metamaskState, backgroundConnection, opts) { const { traceContext } = opts; + const tags = getStartupTraceTags({ metamask: metamaskState }); + const store = await trace( - { name: TraceName.SetupStore, parentContext: traceContext }, + { + name: TraceName.SetupStore, + parentContext: traceContext, + tags, + }, () => setupInitialStore(metamaskState, backgroundConnection, opts.activeTab), ); diff --git a/ui/selectors/nft.test.ts b/ui/selectors/nft.test.ts index 101eb4aae181..d6f4d956f020 100644 --- a/ui/selectors/nft.test.ts +++ b/ui/selectors/nft.test.ts @@ -38,6 +38,7 @@ describe('NFT Selectors', () => { [chainIdMock2]: [contractMock5], }, }, + allNfts: {}, }, }; @@ -80,6 +81,7 @@ describe('NFT Selectors', () => { [chainIdMock2]: [contractMock5], }, }, + allNfts: {}, }, }; diff --git a/ui/selectors/nft.ts b/ui/selectors/nft.ts index 8320c6258b1c..ab3836714923 100644 --- a/ui/selectors/nft.ts +++ b/ui/selectors/nft.ts @@ -1,14 +1,19 @@ -import { NftContract } from '@metamask/assets-controllers'; +import { Nft, NftContract } from '@metamask/assets-controllers'; import { createSelector } from 'reselect'; import { getMemoizedCurrentChainId } from './selectors'; -type NftState = { +export type NftState = { metamask: { allNftContracts: { [account: string]: { [chainId: string]: NftContract[]; }; }; + allNfts: { + [account: string]: { + [chainId: string]: Nft[]; + }; + }; }; }; @@ -16,6 +21,16 @@ function getNftContractsByChainByAccount(state: NftState) { return state.metamask.allNftContracts ?? {}; } +/** + * Get all NFTs owned by the user. + * + * @param state - Metamask state. + * @returns All NFTs owned by the user, keyed by chain ID then account address. + */ +function getNftsByChainByAccount(state: NftState) { + return state.metamask.allNfts ?? {}; +} + export const getNftContractsByAddressByChain = createSelector( getNftContractsByChainByAccount, (nftContractsByChainByAccount) => { @@ -53,3 +68,21 @@ export const getNftContractsByAddressOnCurrentChain = createSelector( return nftContractsByAddressByChain[currentChainId] ?? {}; }, ); + +/** + * Get a flattened list of all NFTs owned by the user. + * Includes all NFTs from all chains and accounts. + * + * @param state - Metamask state. + * @returns All NFTs owned by the user in a single array. + */ +export const selectAllNftsFlat = createSelector( + getNftsByChainByAccount, + (nftsByChainByAccount) => { + const nftsByChainArray = Object.values(nftsByChainByAccount); + return nftsByChainArray.reduce<Nft[]>((acc, nftsByChain) => { + const nftsArrays = Object.values(nftsByChain); + return acc.concat(...nftsArrays); + }, []); + }, +); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 644924a41e3e..17e6ffc4500a 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -490,6 +490,24 @@ export function getAllTokens(state) { return state.metamask.allTokens; } +/** + * Get a flattened list of all ERC-20 tokens owned by the user. + * Includes all tokens from all chains and accounts. + * + * @returns {object[]} All ERC-20 tokens owned by the user in a flat array. + */ +export const selectAllTokensFlat = createSelector( + getAllTokens, + (tokensByAccountByChain) => { + const tokensByAccountArray = Object.values(tokensByAccountByChain); + + return tokensByAccountArray.reduce((acc, tokensByAccount) => { + const tokensArray = Object.values(tokensByAccount); + return acc.concat(...tokensArray); + }, []); + }, +); + /** * Selector to return an origin to network ID map * diff --git a/ui/store/store.ts b/ui/store/store.ts index 6e580c137bdc..8433511380e7 100644 --- a/ui/store/store.ts +++ b/ui/store/store.ts @@ -5,6 +5,11 @@ import { ApprovalControllerState } from '@metamask/approval-controller'; import { GasEstimateType, GasFeeEstimates } from '@metamask/gas-fee-controller'; import { TransactionMeta } from '@metamask/transaction-controller'; import { InternalAccount } from '@metamask/keyring-api'; +import { + NftControllerState, + TokensControllerState, +} from '@metamask/assets-controllers'; +import { NotificationServicesControllerState } from '@metamask/notification-services-controller/notification-services'; import rootReducer from '../ducks'; import { LedgerTransportTypes } from '../../shared/constants/hardware-wallets'; import type { NetworkStatus } from '../../shared/constants/network'; @@ -45,48 +50,50 @@ export type MessagesIndexedById = { * state received from the background takes precedence over anything in the * metamask reducer. */ -type TemporaryBackgroundState = { - addressBook: { - [chainId: string]: { - name: string; - }[]; - }; - // todo: can this be deleted post network controller v20 - providerConfig: { - chainId: string; - }; - transactions: TransactionMeta[]; - ledgerTransportType: LedgerTransportTypes; - unapprovedDecryptMsgs: MessagesIndexedById; - unapprovedPersonalMsgs: MessagesIndexedById; - unapprovedTypedMessages: MessagesIndexedById; - networksMetadata: { - [NetworkClientId: string]: { - EIPS: { [eip: string]: boolean }; - status: NetworkStatus; +type TemporaryBackgroundState = NftControllerState & + NotificationServicesControllerState & + TokensControllerState & { + addressBook: { + [chainId: string]: { + name: string; + }[]; }; - }; - selectedNetworkClientId: string; - pendingApprovals: ApprovalControllerState['pendingApprovals']; - approvalFlows: ApprovalControllerState['approvalFlows']; - knownMethodData?: { - [fourBytePrefix: string]: Record<string, unknown>; - }; - gasFeeEstimates: GasFeeEstimates; - gasEstimateType: GasEstimateType; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - custodyAccountDetails?: { [key: string]: any }; - ///: END:ONLY_INCLUDE_IF - internalAccounts: { - accounts: { - [key: string]: InternalAccount; + // todo: can this be deleted post network controller v20 + providerConfig: { + chainId: string; + }; + transactions: TransactionMeta[]; + ledgerTransportType: LedgerTransportTypes; + unapprovedDecryptMsgs: MessagesIndexedById; + unapprovedPersonalMsgs: MessagesIndexedById; + unapprovedTypedMessages: MessagesIndexedById; + networksMetadata: { + [NetworkClientId: string]: { + EIPS: { [eip: string]: boolean }; + status: NetworkStatus; + }; + }; + selectedNetworkClientId: string; + pendingApprovals: ApprovalControllerState['pendingApprovals']; + approvalFlows: ApprovalControllerState['approvalFlows']; + knownMethodData?: { + [fourBytePrefix: string]: Record<string, unknown>; }; - selectedAccount: string; + gasFeeEstimates: GasFeeEstimates; + gasEstimateType: GasEstimateType; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + custodyAccountDetails?: { [key: string]: any }; + ///: END:ONLY_INCLUDE_IF + internalAccounts: { + accounts: { + [key: string]: InternalAccount; + }; + selectedAccount: string; + }; + keyrings: { type: string; accounts: string[] }[]; }; - keyrings: { type: string; accounts: string[] }[]; -}; type RootReducerReturnType = ReturnType<typeof rootReducer>; diff --git a/yarn.lock b/yarn.lock index 90fe3bc46886..e8cade3e2727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7897,7 +7897,7 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:^8.19.0": +"@sentry/browser@npm:^8.33.1": version: 8.33.1 resolution: "@sentry/browser@npm:8.33.1" dependencies: @@ -7937,14 +7937,14 @@ __metadata: languageName: node linkType: hard -"@sentry/types@npm:8.33.1, @sentry/types@npm:^8.19.0": +"@sentry/types@npm:8.33.1, @sentry/types@npm:^8.33.1": version: 8.33.1 resolution: "@sentry/types@npm:8.33.1" checksum: 10/bcd7f80e84a23cb810fa5819dc85f45bd62d52b01b1f64a1b31297df21e9d1f4de8f7ea91835c5d6a7010d7dbfc8b09cd708d057d345a6ff685b7f12db41ae57 languageName: node linkType: hard -"@sentry/utils@npm:8.33.1, @sentry/utils@npm:^8.19.0": +"@sentry/utils@npm:8.33.1, @sentry/utils@npm:^8.33.1": version: 8.33.1 resolution: "@sentry/utils@npm:8.33.1" dependencies: @@ -26143,10 +26143,10 @@ __metadata: "@popperjs/core": "npm:^2.4.0" "@reduxjs/toolkit": "patch:@reduxjs/toolkit@npm%3A1.9.7#~/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch" "@segment/loosely-validate-event": "npm:^2.0.0" - "@sentry/browser": "npm:^8.19.0" + "@sentry/browser": "npm:^8.33.1" "@sentry/cli": "npm:^2.19.4" - "@sentry/types": "npm:^8.19.0" - "@sentry/utils": "npm:^8.19.0" + "@sentry/types": "npm:^8.33.1" + "@sentry/utils": "npm:^8.33.1" "@storybook/addon-a11y": "npm:^7.6.20" "@storybook/addon-actions": "npm:^7.6.20" "@storybook/addon-designs": "npm:^7.0.9" From f41a6252290c7909feb1a1e43fc2277d35b8675b Mon Sep 17 00:00:00 2001 From: martahj <marta.hourigan.johnson@gmail.com> Date: Tue, 8 Oct 2024 14:27:51 -0500 Subject: [PATCH 092/226] fix: allow getAddTransactionRequest to pass through other params (#27117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates `getAddTransactionRequest` to pass through additional parameters so that `waitForSubmit` will get passed through to `addTransaction` . Previously, the `waitForSubmit` param, although passed by `addTransaction` and `addTransactionAndWaitForPublish`, was not included in the object returned by `getAddTransactionRequest`. This doesn't seem to have had negative consequences on a standard wallet, but when using a hardware wallet, it meant that `addTransactionAndWaitForPublish` resolved before waiting for the user to accept or reject the transaction. As a consequence, when doing a swap, the user was taken directly to the "Processing..." screen before they had a chance to take action on the transaction. If they rejected the transaction, they remained on that screen indefinitely. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27117?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMS-1189 ## **Manual testing steps** 1. Disable smart transactions 2. Confirm that the following flows work and display relevant and expected screens using both a hardware and standard wallet: * Swap that does not need approval step - accept tx * Swap that does not need approval step - reject tx * Swap that needs approval step - reject approval tx * Swap that needs approval step - accept approval tx, reject trade tx * Swap that needs approval step - accept both txs * Send + swap that does not need approval step - accept tx * Send + swap that does not need approval step - reject tx * Send + swap that needs approval step - reject approval tx * Send + swap that needs approval step - accept approval tx, reject trade tx * Send + swap that needs approval step - accept both txs ## **Screenshots/Recordings** ### **Before** Video from bug report (shows rejecting swap flow): https://github.com/user-attachments/assets/bdaa32f1-2c1d-4e23-a97d-9d370baaaf2f ### **After** Accepting swap: https://github.com/user-attachments/assets/bbe99cb3-fa3f-4580-a472-20b3b4a94f31 Rejecting swap: https://github.com/user-attachments/assets/e6ecd2dc-a1a0-4d51-8428-45d19d3a269e ## **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: Derek Brans <dbrans@gmail.com> --- app/scripts/metamask-controller.js | 2 ++ app/scripts/metamask-controller.test.js | 45 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f8f93cbaa5b3..a5b110fadec2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4950,6 +4950,7 @@ export default class MetamaskController extends EventEmitter { transactionParams, transactionOptions, dappRequest, + ...otherParams }) { return { internalAccounts: this.accountsController.listAccounts(), @@ -4969,6 +4970,7 @@ export default class MetamaskController extends EventEmitter { securityAlertsEnabled: this.preferencesController.store.getState()?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), + ...otherParams, }; } diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index d1da34c48e0e..bab66d9bc515 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -517,6 +517,51 @@ describe('MetaMaskController', () => { }); }); + describe('#getAddTransactionRequest', () => { + it('formats the transaction for submission', () => { + const transactionParams = { from: '0xa', to: '0xb' }; + const transactionOptions = { foo: true }; + const result = metamaskController.getAddTransactionRequest({ + transactionParams, + transactionOptions, + }); + expect(result).toStrictEqual({ + internalAccounts: + metamaskController.accountsController.listAccounts(), + dappRequest: undefined, + networkClientId: + metamaskController.networkController.state.selectedNetworkClientId, + selectedAccount: + metamaskController.accountsController.getAccountByAddress( + transactionParams.from, + ), + transactionController: expect.any(Object), + transactionOptions, + transactionParams, + userOperationController: expect.any(Object), + chainId: '0x1', + ppomController: expect.any(Object), + securityAlertsEnabled: expect.any(Boolean), + updateSecurityAlertResponse: expect.any(Function), + }); + }); + it('passes through any additional params to the object', () => { + const transactionParams = { from: '0xa', to: '0xb' }; + const transactionOptions = { foo: true }; + const result = metamaskController.getAddTransactionRequest({ + transactionParams, + transactionOptions, + test: '123', + }); + + expect(result).toMatchObject({ + transactionParams, + transactionOptions, + test: '123', + }); + }); + }); + describe('submitPassword', () => { it('removes any identities that do not correspond to known accounts.', async () => { const fakeAddress = '0xbad0'; From 74378eb50442cfd2accd67671cb8fd688fa4249c Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:42:11 +0200 Subject: [PATCH 093/226] test: Convert json-rpc e2e tests to TypeScript (#27659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Convert e2e tests in `test/e2e/json-rpc/*` to TS - Improve function `loginWithBalanceValidation()` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27659?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27698 ## **Manual testing steps** Tests pass on CI ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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: seaona <54408225+seaona@users.noreply.github.com> --- ..._accounts.spec.js => eth_accounts.spec.ts} | 27 ++++--- .../{eth_call.spec.js => eth_call.spec.ts} | 32 +++++---- test/e2e/json-rpc/eth_chainId.spec.js | 39 ---------- test/e2e/json-rpc/eth_chainId.spec.ts | 44 ++++++++++++ ..._coinbase.spec.js => eth_coinbase.spec.ts} | 29 ++++---- ...ateGas.spec.js => eth_estimateGas.spec.ts} | 31 ++++---- test/e2e/json-rpc/eth_gasPrice.spec.js | 39 ---------- test/e2e/json-rpc/eth_gasPrice.spec.ts | 44 ++++++++++++ ...ter.spec.js => eth_newBlockFilter.spec.ts} | 43 ++++++----- ...ts.spec.js => eth_requestAccounts.spec.ts} | 29 ++++---- test/e2e/json-rpc/eth_subscribe.spec.js | 59 --------------- test/e2e/json-rpc/eth_subscribe.spec.ts | 72 +++++++++++++++++++ test/e2e/page-objects/flows/login.flow.ts | 2 + test/e2e/page-objects/pages/homepage.ts | 7 +- 14 files changed, 280 insertions(+), 217 deletions(-) rename test/e2e/json-rpc/{eth_accounts.spec.js => eth_accounts.spec.ts} (61%) rename test/e2e/json-rpc/{eth_call.spec.js => eth_call.spec.ts} (62%) delete mode 100644 test/e2e/json-rpc/eth_chainId.spec.js create mode 100644 test/e2e/json-rpc/eth_chainId.spec.ts rename test/e2e/json-rpc/{eth_coinbase.spec.js => eth_coinbase.spec.ts} (50%) rename test/e2e/json-rpc/{eth_estimateGas.spec.js => eth_estimateGas.spec.ts} (53%) delete mode 100644 test/e2e/json-rpc/eth_gasPrice.spec.js create mode 100644 test/e2e/json-rpc/eth_gasPrice.spec.ts rename test/e2e/json-rpc/{eth_newBlockFilter.spec.js => eth_newBlockFilter.spec.ts} (62%) rename test/e2e/json-rpc/{eth_requestAccounts.spec.js => eth_requestAccounts.spec.ts} (51%) delete mode 100644 test/e2e/json-rpc/eth_subscribe.spec.js create mode 100644 test/e2e/json-rpc/eth_subscribe.spec.ts diff --git a/test/e2e/json-rpc/eth_accounts.spec.js b/test/e2e/json-rpc/eth_accounts.spec.ts similarity index 61% rename from test/e2e/json-rpc/eth_accounts.spec.js rename to test/e2e/json-rpc/eth_accounts.spec.ts index af3568a41208..149021d40a57 100644 --- a/test/e2e/json-rpc/eth_accounts.spec.js +++ b/test/e2e/json-rpc/eth_accounts.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; +import FixtureBuilder from '../fixture-builder'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; describe('eth_accounts', function () { it('executes a eth_accounts json rpc call', async function () { @@ -18,10 +17,16 @@ describe('eth_accounts', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_accounts await driver.openNewPage(`http://127.0.0.1:8080`); @@ -31,7 +36,7 @@ describe('eth_accounts', function () { method: 'eth_accounts', }); - const accounts = await driver.executeScript( + const accounts: string[] = await driver.executeScript( `return window.ethereum.request(${accountsRequest})`, ); diff --git a/test/e2e/json-rpc/eth_call.spec.js b/test/e2e/json-rpc/eth_call.spec.ts similarity index 62% rename from test/e2e/json-rpc/eth_call.spec.js rename to test/e2e/json-rpc/eth_call.spec.ts index 8b81bb2193b4..7ff1dd7489ff 100644 --- a/test/e2e/json-rpc/eth_call.spec.js +++ b/test/e2e/json-rpc/eth_call.spec.ts @@ -1,12 +1,12 @@ -const { strict: assert } = require('assert'); -const { keccak } = require('ethereumjs-util'); -const { - withFixtures, - unlockWallet, - defaultGanacheOptions, -} = require('../helpers'); -const { SMART_CONTRACTS } = require('../seeder/smart-contracts'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { keccak } from 'ethereumjs-util'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { Driver } from '../webdriver/driver'; +import FixtureBuilder from '../fixture-builder'; +import { Ganache } from '../seeder/ganache'; +import GanacheContractAddressRegistry from '../seeder/ganache-contract-address-registry'; +import { SMART_CONTRACTS } from '../seeder/smart-contracts'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; describe('eth_call', function () { const smartContract = SMART_CONTRACTS.NFTS; @@ -19,11 +19,19 @@ describe('eth_call', function () { .build(), ganacheOptions: defaultGanacheOptions, smartContract, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver, _, contractRegistry }) => { + async ({ + driver, + ganacheServer, + contractRegistry, + }: { + driver: Driver; + ganacheServer?: Ganache; + contractRegistry: GanacheContractAddressRegistry; + }) => { const contract = contractRegistry.getContractAddress(smartContract); - await unlockWallet(driver); + await loginWithBalanceValidation(driver, ganacheServer); // eth_call await driver.openNewPage(`http://127.0.0.1:8080`); diff --git a/test/e2e/json-rpc/eth_chainId.spec.js b/test/e2e/json-rpc/eth_chainId.spec.js deleted file mode 100644 index ba604552db82..000000000000 --- a/test/e2e/json-rpc/eth_chainId.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - unlockWallet, - defaultGanacheOptions, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -describe('eth_chainId', function () { - it('returns the chain ID of the current network', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // eth_chainId - await driver.openNewPage(`http://127.0.0.1:8080`); - const request = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - id: 0, - }); - const result = await driver.executeScript( - `return window.ethereum.request(${request})`, - ); - - assert.equal(result, '0x539'); - }, - ); - }); -}); diff --git a/test/e2e/json-rpc/eth_chainId.spec.ts b/test/e2e/json-rpc/eth_chainId.spec.ts new file mode 100644 index 000000000000..d4b8e4f1dbb6 --- /dev/null +++ b/test/e2e/json-rpc/eth_chainId.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; + +describe('eth_chainId', function () { + it('returns the chain ID of the current network', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + + // eth_chainId + await driver.openNewPage(`http://127.0.0.1:8080`); + const request: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + id: 0, + }); + const result = (await driver.executeScript( + `return window.ethereum.request(${request})`, + )) as string; + + assert.equal(result, '0x539'); + }, + ); + }); +}); diff --git a/test/e2e/json-rpc/eth_coinbase.spec.js b/test/e2e/json-rpc/eth_coinbase.spec.ts similarity index 50% rename from test/e2e/json-rpc/eth_coinbase.spec.js rename to test/e2e/json-rpc/eth_coinbase.spec.ts index 06fc25335572..216a3e7eedeb 100644 --- a/test/e2e/json-rpc/eth_coinbase.spec.js +++ b/test/e2e/json-rpc/eth_coinbase.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - defaultGanacheOptions, - withFixtures, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_coinbase', function () { it('executes a eth_coinbase json rpc call', async function () { @@ -15,20 +14,26 @@ describe('eth_coinbase', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.title, + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_coinbase await driver.openNewPage(`http://127.0.0.1:8080`); - const coinbaseRequest = JSON.stringify({ + const coinbaseRequest: string = JSON.stringify({ jsonrpc: '2.0', method: 'eth_coinbase', }); - const coinbase = await driver.executeScript( + const coinbase: string = await driver.executeScript( `return window.ethereum.request(${coinbaseRequest})`, ); diff --git a/test/e2e/json-rpc/eth_estimateGas.spec.js b/test/e2e/json-rpc/eth_estimateGas.spec.ts similarity index 53% rename from test/e2e/json-rpc/eth_estimateGas.spec.js rename to test/e2e/json-rpc/eth_estimateGas.spec.ts index 9ef594e1254b..11e0cb2379cb 100644 --- a/test/e2e/json-rpc/eth_estimateGas.spec.js +++ b/test/e2e/json-rpc/eth_estimateGas.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_estimateGas', function () { it('executes a estimate gas json rpc call', async function () { @@ -15,15 +14,21 @@ describe('eth_estimateGas', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_estimateGas await driver.openNewPage(`http://127.0.0.1:8080`); - const estimateGas = JSON.stringify({ + const estimateGas: string = JSON.stringify({ jsonrpc: '2.0', method: 'eth_estimateGas', params: [ @@ -34,9 +39,9 @@ describe('eth_estimateGas', function () { ], }); - const estimateGasRequest = await driver.executeScript( + const estimateGasRequest: string = (await driver.executeScript( `return window.ethereum.request(${estimateGas})`, - ); + )) as string; assert.strictEqual(estimateGasRequest, '0x5208'); }, diff --git a/test/e2e/json-rpc/eth_gasPrice.spec.js b/test/e2e/json-rpc/eth_gasPrice.spec.js deleted file mode 100644 index a3c2ef76f19b..000000000000 --- a/test/e2e/json-rpc/eth_gasPrice.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -describe('eth_gasPrice', function () { - it('executes gas price json rpc call', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // eth_gasPrice - await driver.openNewPage(`http://127.0.0.1:8080`); - - const gasPriceRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_gasPrice', - }); - - const gasPrice = await driver.executeScript( - `return window.ethereum.request(${gasPriceRequest})`, - ); - - assert.strictEqual(gasPrice, '0x77359400'); // 2000000000 - }, - ); - }); -}); diff --git a/test/e2e/json-rpc/eth_gasPrice.spec.ts b/test/e2e/json-rpc/eth_gasPrice.spec.ts new file mode 100644 index 000000000000..d9c75c29fed9 --- /dev/null +++ b/test/e2e/json-rpc/eth_gasPrice.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; + +describe('eth_gasPrice', function () { + it('executes gas price json rpc call', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + + // eth_gasPrice + await driver.openNewPage(`http://127.0.0.1:8080`); + + const gasPriceRequest: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_gasPrice', + }); + + const gasPrice: string = await driver.executeScript( + `return window.ethereum.request(${gasPriceRequest})`, + ); + + assert.strictEqual(gasPrice, '0x77359400'); // 2000000000 + }, + ); + }); +}); diff --git a/test/e2e/json-rpc/eth_newBlockFilter.spec.js b/test/e2e/json-rpc/eth_newBlockFilter.spec.ts similarity index 62% rename from test/e2e/json-rpc/eth_newBlockFilter.spec.js rename to test/e2e/json-rpc/eth_newBlockFilter.spec.ts index 1b1091f82efa..a20f0fce23c0 100644 --- a/test/e2e/json-rpc/eth_newBlockFilter.spec.js +++ b/test/e2e/json-rpc/eth_newBlockFilter.spec.ts @@ -1,13 +1,12 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_newBlockFilter', function () { - const ganacheOptions = { + const ganacheOptions: typeof defaultGanacheOptions & { blockTime: number } = { blockTime: 0.1, ...defaultGanacheOptions, }; @@ -19,10 +18,16 @@ describe('eth_newBlockFilter', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_newBlockFilter await driver.openNewPage(`http://127.0.0.1:8080`); @@ -32,9 +37,9 @@ describe('eth_newBlockFilter', function () { method: 'eth_newBlockFilter', }); - const newBlockFilter = await driver.executeScript( + const newBlockFilter = (await driver.executeScript( `return window.ethereum.request(${newBlockfilterRequest})`, - ); + )) as string; assert.strictEqual(newBlockFilter, '0x01'); @@ -52,13 +57,13 @@ describe('eth_newBlockFilter', function () { method: 'eth_getBlockByNumber', params: ['latest', false], }); - const blockByHash = await driver.executeScript( + const blockByHash = (await driver.executeScript( `return window.ethereum.request(${blockByHashRequest})`, - ); + )) as { hash: string }; - const filterChanges = await driver.executeScript( + const filterChanges = (await driver.executeScript( `return window.ethereum.request(${getFilterChangesRequest})`, - ); + )) as string[]; assert.strictEqual(filterChanges.includes(blockByHash.hash), true); @@ -69,9 +74,9 @@ describe('eth_newBlockFilter', function () { params: ['0x01'], }); - const uninstallFilter = await driver.executeScript( + const uninstallFilter = (await driver.executeScript( `return window.ethereum.request(${uninstallFilterRequest})`, - ); + )) as boolean; assert.strictEqual(uninstallFilter, true); }, diff --git a/test/e2e/json-rpc/eth_requestAccounts.spec.js b/test/e2e/json-rpc/eth_requestAccounts.spec.ts similarity index 51% rename from test/e2e/json-rpc/eth_requestAccounts.spec.js rename to test/e2e/json-rpc/eth_requestAccounts.spec.ts index 2aa510522e2b..00c043ebac51 100644 --- a/test/e2e/json-rpc/eth_requestAccounts.spec.js +++ b/test/e2e/json-rpc/eth_requestAccounts.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - defaultGanacheOptions, - withFixtures, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_requestAccounts', function () { it('executes a request accounts json rpc call', async function () { @@ -15,20 +14,26 @@ describe('eth_requestAccounts', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.title, + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_requestAccounts await driver.openNewPage(`http://127.0.0.1:8080`); - const requestAccountRequest = JSON.stringify({ + const requestAccountRequest: string = JSON.stringify({ jsonrpc: '2.0', method: 'eth_requestAccounts', }); - const requestAccount = await driver.executeScript( + const requestAccount: string[] = await driver.executeScript( `return window.ethereum.request(${requestAccountRequest})`, ); diff --git a/test/e2e/json-rpc/eth_subscribe.spec.js b/test/e2e/json-rpc/eth_subscribe.spec.js deleted file mode 100644 index 701913bb1867..000000000000 --- a/test/e2e/json-rpc/eth_subscribe.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -describe('eth_subscribe', function () { - it('executes a subscription event', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.title, - }, - async ({ driver }) => { - await unlockWallet(driver); - - // eth_subscribe - await driver.openNewPage(`http://127.0.0.1:8080`); - - const subscribeRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_subscribe', - params: ['newHeads'], - }); - - const subscribe = await driver.executeScript( - `return window.ethereum.request(${subscribeRequest})`, - ); - - const subscriptionMessage = await driver.executeAsyncScript( - `const callback = arguments[arguments.length - 1];` + - `window.ethereum.on('message', (message) => callback(message))`, - ); - - assert.strictEqual(subscribe, subscriptionMessage.data.subscription); - assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); - - // eth_unsubscribe - const unsubscribeRequest = JSON.stringify({ - jsonrpc: '2.0', - method: `eth_unsubscribe`, - params: [`${subscribe}`], - }); - - const unsubscribe = await driver.executeScript( - `return window.ethereum.request(${unsubscribeRequest})`, - ); - - assert.strictEqual(unsubscribe, true); - }, - ); - }); -}); diff --git a/test/e2e/json-rpc/eth_subscribe.spec.ts b/test/e2e/json-rpc/eth_subscribe.spec.ts new file mode 100644 index 000000000000..526bf1f3a761 --- /dev/null +++ b/test/e2e/json-rpc/eth_subscribe.spec.ts @@ -0,0 +1,72 @@ +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; + +describe('eth_subscribe', function () { + it('executes a subscription event', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + + // eth_subscribe + await driver.openNewPage(`http://127.0.0.1:8080`); + + const subscribeRequest: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_subscribe', + params: ['newHeads'], + }); + + const subscribe: string = (await driver.executeScript( + `return window.ethereum.request(${subscribeRequest})`, + )) as string; + + type SubscriptionMessage = { + data: { + subscription: string; + }; + type: string; + }; + + const subscriptionMessage: SubscriptionMessage = + (await driver.executeAsyncScript( + `const callback = arguments[arguments.length - 1]; + window.ethereum.on('message', (message) => callback(message))`, + )) as SubscriptionMessage; + + assert.strictEqual(subscribe, subscriptionMessage.data.subscription); + assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); + + // eth_unsubscribe + const unsubscribeRequest: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_unsubscribe', + params: [subscribe], + }); + + const unsubscribe: boolean = (await driver.executeScript( + `return window.ethereum.request(${unsubscribeRequest})`, + )) as boolean; + + assert.strictEqual(unsubscribe, true); + }, + ); + }); +}); diff --git a/test/e2e/page-objects/flows/login.flow.ts b/test/e2e/page-objects/flows/login.flow.ts index 2904b1b9bd38..87239e3f19f1 100644 --- a/test/e2e/page-objects/flows/login.flow.ts +++ b/test/e2e/page-objects/flows/login.flow.ts @@ -40,5 +40,7 @@ export const loginWithBalanceValidation = async ( // Verify the expected balance on the homepage if (ganacheServer) { await new HomePage(driver).check_ganacheBalanceIsDisplayed(ganacheServer); + } else { + await new HomePage(driver).check_expectedBalanceIsDisplayed(); } }; diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 59845138c8a2..23c050f49526 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -109,8 +109,13 @@ class HomePage { ); } + /** + * Checks if the expected balance is displayed on homepage. + * + * @param expectedBalance - The expected balance to be displayed. Defaults to '0'. + */ async check_expectedBalanceIsDisplayed( - expectedBalance: string, + expectedBalance: string = '0', ): Promise<void> { try { await this.driver.waitForSelector({ From b5b8b8f660d3ba3ec44aa01531457d4433c93cb3 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:53:54 +0000 Subject: [PATCH 094/226] ci: make git-diff-develop work for PRs from foreign repos (#27268) The `gh` CLI requires authentication, even if the API endpoints do not. We can just fetch the PR URL directly without shelling out. Fixes [CI error](https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/101231/workflows/7b91edba-0c97-4075-934c-5db83a71a2c0/jobs/3769045). Also removes dependency on `prep-deps` step by removing dependency on external module. --------- Co-authored-by: Howard Braham <howrad@gmail.com> --- .circleci/config.yml | 14 ++--- .circleci/scripts/git-diff-develop.ts | 82 ++++++++++++++++----------- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 095650aae02d..b2c5ab712973 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -118,11 +118,9 @@ workflows: - prep-deps - get-changed-files-with-git-diff: filters: - branches: - ignore: - - master - requires: - - prep-deps + branches: + ignore: + - master - test-deps-audit: requires: - prep-deps @@ -360,11 +358,10 @@ workflows: value: << pipeline.git.branch >> jobs: - prep-deps - - get-changed-files-with-git-diff: - requires: - - prep-deps + - get-changed-files-with-git-diff - validate-locales-only: requires: + - prep-deps - get-changed-files-with-git-diff - test-lint: requires: @@ -501,7 +498,6 @@ jobs: - run: sudo corepack enable - attach_workspace: at: . - - gh/install - run: name: Get changed files with git diff command: npx tsx .circleci/scripts/git-diff-develop.ts diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts index 3cf5022d4e12..9f6c8f0ae4df 100644 --- a/.circleci/scripts/git-diff-develop.ts +++ b/.circleci/scripts/git-diff-develop.ts @@ -1,4 +1,3 @@ -import { hasProperty } from '@metamask/utils'; import { exec as execCallback } from 'child_process'; import fs from 'fs'; import path from 'path'; @@ -6,24 +5,38 @@ import { promisify } from 'util'; const exec = promisify(execCallback); +// The CIRCLE_PR_NUMBER variable is only available on forked Pull Requests +const PR_NUMBER = + process.env.CIRCLE_PR_NUMBER || + process.env.CIRCLE_PULL_REQUEST?.split('/').pop(); + const MAIN_BRANCH = 'develop'; +const SOURCE_BRANCH = `refs/pull/${PR_NUMBER}/head`; + +const CHANGED_FILES_DIR = 'changed-files'; + +type PRInfo = { + base: { + ref: string; + }; + body: string; +}; /** - * Get the target branch for the given pull request. + * Get JSON info about the given pull request * - * @returns The name of the branch targeted by the PR. + * @returns JSON info from GitHub */ -async function getBaseRef(): Promise<string | null> { - if (!process.env.CIRCLE_PULL_REQUEST) { +async function getPrInfo(): Promise<PRInfo | null> { + if (!PR_NUMBER) { return null; } - // We're referencing the CIRCLE_PULL_REQUEST environment variable within the script rather than - // passing it in because this makes it easier to use Bash parameter expansion to extract the - // PR number from the URL. - const result = await exec(`gh pr view --json baseRefName "\${CIRCLE_PULL_REQUEST##*/}" --jq '.baseRefName'`); - const baseRef = result.stdout.trim(); - return baseRef; + return await ( + await fetch( + `https://api.github.com/repos/${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}/pulls/${PR_NUMBER}`, + ) + ).json(); } /** @@ -34,8 +47,10 @@ async function getBaseRef(): Promise<string | null> { */ async function fetchWithDepth(depth: number): Promise<boolean> { try { - await exec(`git fetch --depth ${depth} origin develop`); - await exec(`git fetch --depth ${depth} origin ${process.env.CIRCLE_BRANCH}`); + await exec(`git fetch --depth ${depth} origin "${MAIN_BRANCH}"`); + await exec( + `git fetch --depth ${depth} origin "${SOURCE_BRANCH}:${SOURCE_BRANCH}"`, + ); return true; } catch (error: unknown) { console.error(`Failed to fetch with depth ${depth}:`, error); @@ -59,18 +74,16 @@ async function fetchUntilMergeBaseFound() { await exec(`git merge-base origin/HEAD HEAD`); return; } catch (error: unknown) { - if ( - error instanceof Error && - hasProperty(error, 'code') && - error.code === 1 - ) { - console.error(`Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`); + if (error instanceof Error && 'code' in error) { + console.error( + `Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`, + ); } else { throw error; } } } - await exec(`git fetch --unshallow origin develop`); + await exec(`git fetch --unshallow origin "${MAIN_BRANCH}"`); } /** @@ -82,9 +95,11 @@ async function fetchUntilMergeBaseFound() { */ async function gitDiff(): Promise<string> { await fetchUntilMergeBaseFound(); - const { stdout: diffResult } = await exec(`git diff --name-only origin/HEAD...${process.env.CIRCLE_BRANCH}`); + const { stdout: diffResult } = await exec( + `git diff --name-only "origin/HEAD...${SOURCE_BRANCH}"`, + ); if (!diffResult) { - throw new Error('Unable to get diff after full checkout.'); + throw new Error('Unable to get diff after full checkout.'); } return diffResult; } @@ -99,30 +114,33 @@ async function storeGitDiffOutput() { // Create the directory // This is done first because our CirleCI config requires that this directory is present, // even if we want to skip this step. - const outputDir = 'changed-files'; - fs.mkdirSync(outputDir, { recursive: true }); + fs.mkdirSync(CHANGED_FILES_DIR, { recursive: true }); - console.log(`Determining whether this run is for a PR targetting ${MAIN_BRANCH}`) - if (!process.env.CIRCLE_PULL_REQUEST) { - console.log("Not a PR, skipping git diff"); + console.log( + `Determining whether this run is for a PR targeting ${MAIN_BRANCH}`, + ); + if (!PR_NUMBER) { + console.log('Not a PR, skipping git diff'); return; } - const baseRef = await getBaseRef(); - if (baseRef === null) { - console.log("Not a PR, skipping git diff"); + const prInfo = await getPrInfo(); + + const baseRef = prInfo?.base.ref; + if (!baseRef) { + console.log('Not a PR, skipping git diff'); return; } else if (baseRef !== MAIN_BRANCH) { console.log(`This is for a PR targeting '${baseRef}', skipping git diff`); return; } - console.log("Attempting to get git diff..."); + console.log('Attempting to get git diff...'); const diffOutput = await gitDiff(); console.log(diffOutput); // Store the output of git diff - const outputPath = path.resolve(outputDir, 'changed-files.txt'); + const outputPath = path.resolve(CHANGED_FILES_DIR, 'changed-files.txt'); fs.writeFileSync(outputPath, diffOutput.trim()); console.log(`Git diff results saved to ${outputPath}`); From b34484e515eab5f415d6744106de58d880aab8ff Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:20:36 -0700 Subject: [PATCH 095/226] feat: Sort/Import Tokens in Extension (#27184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ### This PR adds Token sorting to the Asset List page, and also moves Token importing to the top of the Token List. A few of the main changes introduced: 1. Include `NativeToken` in `TokenList` component to be included in sorting logic, and treated (as far as sorting is concerned) as any other token in the list 2. Intoduce a `tokenSortConfig` into state that keeps track of the sort order, the key being sorted by, and the direction of the sort order. Also includes an action to update this state. 3. Introduce a `useEffect` that subscribes to `tokenSortConfig` as well as a few other application state variables to update and sort tokenList when appropriate. 2. Clean up `asset-list` component, and move some of it's relevant code into the `useAccountTotalFiatBalance` **Acceptance Criteria:** 1. Tokens should be sorted by default by declining balance 2. Sort controls should sort tokens alphabetically, and by decreasing fiat token balance 3. Sort order should persist through refresh 4. Sort order should persist after app is closed and reopened 5. When a token gets imported, it should be included in the sort list, in the correct order in the list **A couple of disclaimers. There are still (at least) two bugs that I discovered that were not caught by tests:** 1. ~~When toggling preferred currency setting, Native Token sorted incorrectly by decreasing fiat balance~~ ✅ fixed 2. ~~When switching between accounts, token list does not update~~ ✅ fixed [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27184?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMASSETS-356 ## **Manual testing steps** 1. Go to AssetList page, and click dropdown and select option to sort by 2. Tokens should sort, and remain sorted through refresh and application close/open (it is in state) 4. Importing tokens should import them into the sort order ## **Screenshots/Recordings** https://github.com/user-attachments/assets/8ecca5e4-093f-4651-946e-31c612795427 ## **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. --- .storybook/test-data.js | 5 + app/_locales/en/messages.json | 10 + app/scripts/controllers/metametrics.js | 2 + app/scripts/controllers/metametrics.test.js | 26 ++ .../controllers/preferences-controller.ts | 10 + app/scripts/migrations/130.test.ts | 91 ++++++ app/scripts/migrations/130.ts | 44 +++ app/scripts/migrations/index.js | 1 + shared/constants/metametrics.ts | 5 + test/data/mock-state.json | 7 +- test/e2e/default-fixture.js | 5 + test/e2e/fixture-builder.js | 5 + ...rs-after-init-opt-in-background-state.json | 2 +- .../errors-after-init-opt-in-ui-state.json | 1 + ...s-before-init-opt-in-background-state.json | 7 +- .../errors-before-init-opt-in-ui-state.json | 7 +- test/e2e/tests/tokens/add-hide-token.spec.js | 2 +- .../tokens/custom-token-add-approve.spec.js | 2 +- .../tokens/custom-token-send-transfer.spec.js | 12 + test/e2e/tests/tokens/token-details.spec.ts | 2 +- test/e2e/tests/tokens/token-list.spec.ts | 2 +- test/e2e/tests/tokens/token-sort.spec.ts | 111 ++++++++ .../tests/transaction/change-assets.spec.js | 2 +- ui/components/app/app-components.scss | 2 + .../asset-list-control-bar.tsx | 99 +++++++ .../asset-list-control-bar/index.scss | 8 + .../asset-list-control-bar/index.ts | 1 + .../app/assets/asset-list/asset-list.tsx | 145 ++-------- .../import-control/import-control.tsx | 63 +++++ .../assets/asset-list/import-control/index.ts | 1 + .../assets/asset-list/native-token/index.ts | 1 + .../asset-list/native-token/native-token.tsx | 59 ++++ .../native-token/use-native-token-balance.ts | 94 +++++++ .../assets/asset-list/sort-control/index.scss | 27 ++ .../assets/asset-list/sort-control/index.ts | 1 + .../sort-control/sort-control.test.tsx | 119 ++++++++ .../asset-list/sort-control/sort-control.tsx | 116 ++++++++ .../app/assets/token-cell/token-cell.tsx | 2 +- .../app/assets/token-list/token-list.tsx | 91 ++++-- ui/components/app/assets/util/sort.test.ts | 263 ++++++++++++++++++ ui/components/app/assets/util/sort.ts | 86 ++++++ .../account-overview-btc.test.tsx | 4 +- .../import-token-link.test.js.snap | 28 -- .../import-token-link.test.js | 7 +- .../import-token-link/import-token-link.tsx | 47 +--- .../multichain/ramps-card/index.scss | 5 +- ui/css/design-system/_colors.scss | 2 + ui/helpers/constants/design-system.ts | 2 + ui/helpers/utils/common.util.js | 16 ++ ui/hooks/useAccountTotalFiatBalance.js | 42 ++- ui/hooks/useAccountTotalFiatBalance.test.js | 10 +- ...MultichainAccountTotalFiatBalance.test.tsx | 4 + ui/pages/routes/routes.component.test.js | 7 + ui/store/actionConstants.ts | 2 + ui/store/actions.ts | 10 +- 55 files changed, 1486 insertions(+), 239 deletions(-) create mode 100644 app/scripts/migrations/130.test.ts create mode 100644 app/scripts/migrations/130.ts create mode 100644 test/e2e/tests/tokens/token-sort.spec.ts create mode 100644 ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx create mode 100644 ui/components/app/assets/asset-list/asset-list-control-bar/index.scss create mode 100644 ui/components/app/assets/asset-list/asset-list-control-bar/index.ts create mode 100644 ui/components/app/assets/asset-list/import-control/import-control.tsx create mode 100644 ui/components/app/assets/asset-list/import-control/index.ts create mode 100644 ui/components/app/assets/asset-list/native-token/index.ts create mode 100644 ui/components/app/assets/asset-list/native-token/native-token.tsx create mode 100644 ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts create mode 100644 ui/components/app/assets/asset-list/sort-control/index.scss create mode 100644 ui/components/app/assets/asset-list/sort-control/index.ts create mode 100644 ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx create mode 100644 ui/components/app/assets/asset-list/sort-control/sort-control.tsx create mode 100644 ui/components/app/assets/util/sort.test.ts create mode 100644 ui/components/app/assets/util/sort.ts diff --git a/.storybook/test-data.js b/.storybook/test-data.js index de94b69f857e..cbcebb6347ed 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -677,6 +677,11 @@ const state = { currentLocale: 'en', preferences: { showNativeTokenAsMainBalance: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, }, incomingTransactionsPreferences: { [CHAIN_IDS.MAINNET]: true, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 30c913d1de74..60ec9579059d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5245,6 +5245,16 @@ "somethingWentWrong": { "message": "Oops! Something went wrong." }, + "sortBy": { + "message": "Sort by" + }, + "sortByAlphabetically": { + "message": "Alphabetically (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Declining balance ($1 high-low)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Source" }, diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 28ced592fb9d..15f4fa9b7788 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -868,6 +868,8 @@ export default class MetaMetricsController { metamaskState.participateInMetaMetrics, [MetaMetricsUserTrait.HasMarketingConsent]: metamaskState.dataCollectionForMarketing, + [MetaMetricsUserTrait.TokenSortPreference]: + metamaskState.tokenSortConfig?.key || '', }; if (!previousUserTraits) { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 3d4845e056d0..a0505700ef01 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1122,6 +1122,11 @@ describe('MetaMetricsController', function () { }, }, }, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, }); expect(traits).toStrictEqual({ @@ -1153,6 +1158,7 @@ describe('MetaMetricsController', function () { ///: BEGIN:ONLY_INCLUDE_IF(petnames) [MetaMetricsUserTrait.PetnameAddressCount]: 3, ///: END:ONLY_INCLUDE_IF + [MetaMetricsUserTrait.TokenSortPreference]: 'token-sort-key', }); }); @@ -1181,6 +1187,11 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: true, }); @@ -1208,6 +1219,11 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: false, }); @@ -1245,6 +1261,11 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: true, }); @@ -1267,6 +1288,11 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: true, }); expect(updatedTraits).toStrictEqual(null); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index a158ac0024d4..eb126b176a41 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -106,6 +106,11 @@ export type Preferences = { showMultiRpcModal: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; + tokenSortConfig: { + key: string; + order: string; + sortCallback: string; + }; shouldShowAggregatedBalancePopover: boolean; }; @@ -237,6 +242,11 @@ export default class PreferencesController { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, shouldShowAggregatedBalancePopover: true, // by default user should see popover; }, // ENS decentralized website resolution diff --git a/app/scripts/migrations/130.test.ts b/app/scripts/migrations/130.test.ts new file mode 100644 index 000000000000..94e00949c7a1 --- /dev/null +++ b/app/scripts/migrations/130.test.ts @@ -0,0 +1,91 @@ +import { migrate, version } from './130'; + +const oldVersion = 129; + +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 }); + }); + describe(`migration #${version}`, () => { + it('updates the preferences with a default tokenSortConfig', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + preferences: {}, + }, + }, + }; + const expectedData = { + PreferencesController: { + preferences: { + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(expectedData); + }); + + it('does nothing if the preferences already has a tokenSortConfig', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + preferences: { + tokenSortConfig: { + key: 'fooKey', + order: 'foo', + sortCallback: 'fooCallback', + }, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing to other preferences if they exist without a tokenSortConfig', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + preferences: { + existingPreference: true, + }, + }, + }, + }; + + const expectedData = { + PreferencesController: { + preferences: { + existingPreference: true, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(expectedData); + }); + }); +}); diff --git a/app/scripts/migrations/130.ts b/app/scripts/migrations/130.ts new file mode 100644 index 000000000000..ccf376ce1e7e --- /dev/null +++ b/app/scripts/migrations/130.ts @@ -0,0 +1,44 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record<string, unknown>; +}; +export const version = 130; +/** + * This migration adds a tokenSortConfig to the user's preferences + * + * + * @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<VersionedData> { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} +function transformState( + state: Record<string, unknown>, +): Record<string, unknown> { + if ( + hasProperty(state, 'PreferencesController') && + isObject(state.PreferencesController) && + hasProperty(state.PreferencesController, 'preferences') && + isObject(state.PreferencesController.preferences) && + !state.PreferencesController.preferences.tokenSortConfig + ) { + state.PreferencesController.preferences.tokenSortConfig = { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }; + } + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 296ff8077613..93a862b5ee02 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -149,6 +149,7 @@ const migrations = [ require('./127'), require('./128'), require('./129'), + require('./130'), ]; export default migrations; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index d0f1cfb87cbe..8faf7c7bfb79 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -472,6 +472,10 @@ export enum MetaMetricsUserTrait { * Identified when the user selects a currency from settings */ CurrentCurrency = 'current_currency', + /** + * Identified when the user changes token sort order on asset-list + */ + TokenSortPreference = 'token_sort_preference', } /** @@ -630,6 +634,7 @@ export enum MetaMetricsEventName { TokenScreenOpened = 'Token Screen Opened', TokenAdded = 'Token Added', TokenRemoved = 'Token Removed', + TokenSortPreference = 'Token Sort Preference', NFTRemoved = 'NFT Removed', TokenDetected = 'Token Detected', TokenHidden = 'Token Hidden', diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 32a61c573500..654e915a1305 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -372,7 +372,12 @@ "showFiatInTestnets": false, "showNativeTokenAsMainBalance": true, "showTestNetworks": true, - "smartTransactionsOptInStatus": false + "smartTransactionsOptInStatus": false, + "tokenSortConfig": { + "key": "tokenFiatAmount", + "order": "dsc", + "sortCallback": "stringNumeric" + } }, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 83b8b29a5e83..2c0dfe9a23cb 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -215,6 +215,11 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, shouldShowAggregatedBalancePopover: true, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 415af23071e7..f1e9a7e5ae1d 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -77,6 +77,11 @@ function onboardingFixture() { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, shouldShowAggregatedBalancePopover: true, }, useExternalServices: true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 8d8c8c1ae895..559e8a256d43 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 @@ -215,7 +215,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean", + "tokenSortConfig": "object", "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", 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 b1131ec4e7a2..2df9ee4e2f23 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 @@ -37,6 +37,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", + "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index f40b2687316b..d22b69967027 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -80,11 +80,11 @@ }, "NetworkController": { "selectedNetworkClientId": "string", + "networkConfigurations": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": "object", - "networkConfigurations": "object" + "providerConfig": "object" }, "OnboardingController": { "completedOnboarding": true, @@ -114,8 +114,9 @@ "smartTransactionsOptInStatus": false, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, - "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", + "showConfirmationAdvancedDetails": false, + "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 3c692fa59405..2dfd6ac6ef21 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -80,11 +80,11 @@ }, "NetworkController": { "selectedNetworkClientId": "string", + "networkConfigurations": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": "object", - "networkConfigurations": "object" + "providerConfig": "object" }, "OnboardingController": { "completedOnboarding": true, @@ -114,8 +114,9 @@ "smartTransactionsOptInStatus": false, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, - "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", + "showConfirmationAdvancedDetails": false, + "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index a13ef9caa2b5..535948ba1c9b 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -119,7 +119,7 @@ describe('Add existing token using search', function () { async ({ driver }) => { await unlockWallet(driver); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await driver.fill('input[placeholder="Search tokens"]', 'BAT'); await driver.clickElement({ text: 'BAT', diff --git a/test/e2e/tests/tokens/custom-token-add-approve.spec.js b/test/e2e/tests/tokens/custom-token-add-approve.spec.js index a9cf1829a808..7a59243da403 100644 --- a/test/e2e/tests/tokens/custom-token-add-approve.spec.js +++ b/test/e2e/tests/tokens/custom-token-add-approve.spec.js @@ -35,7 +35,7 @@ describe('Create token, approve token and approve token without gas', function ( ); await clickNestedButton(driver, 'Tokens'); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/custom-token-send-transfer.spec.js b/test/e2e/tests/tokens/custom-token-send-transfer.spec.js index de2aa2addcf8..40b1872011bd 100644 --- a/test/e2e/tests/tokens/custom-token-send-transfer.spec.js +++ b/test/e2e/tests/tokens/custom-token-send-transfer.spec.js @@ -136,6 +136,12 @@ describe('Transfer custom tokens @no-mmi', function () { text: '-1.5 TST', }); + // this selector helps prevent flakiness. it allows driver to wait until send transfer is "confirmed" + await driver.waitForSelector({ + text: 'Confirmed', + tag: 'div', + }); + // check token amount is correct after transaction await clickNestedButton(driver, 'Tokens'); const tokenAmount = await driver.findElement( @@ -192,6 +198,12 @@ describe('Transfer custom tokens @no-mmi', function () { text: 'Send TST', }); + // this selector helps prevent flakiness. it allows driver to wait until send transfer is "confirmed" + await driver.waitForSelector({ + text: 'Confirmed', + tag: 'div', + }); + // check token amount is correct after transaction await clickNestedButton(driver, 'Tokens'); const tokenAmount = await driver.findElement( diff --git a/test/e2e/tests/tokens/token-details.spec.ts b/test/e2e/tests/tokens/token-details.spec.ts index 349c273c721c..0d577ab20f19 100644 --- a/test/e2e/tests/tokens/token-details.spec.ts +++ b/test/e2e/tests/tokens/token-details.spec.ts @@ -27,7 +27,7 @@ describe('Token Details', function () { }; const importToken = async (driver: Driver) => { - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/token-list.spec.ts b/test/e2e/tests/tokens/token-list.spec.ts index 32b5ea85e3ae..bffef04c40dd 100644 --- a/test/e2e/tests/tokens/token-list.spec.ts +++ b/test/e2e/tests/tokens/token-list.spec.ts @@ -27,7 +27,7 @@ describe('Token List', function () { }; const importToken = async (driver: Driver) => { - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/token-sort.spec.ts b/test/e2e/tests/tokens/token-sort.spec.ts new file mode 100644 index 000000000000..e0d335ee0fd6 --- /dev/null +++ b/test/e2e/tests/tokens/token-sort.spec.ts @@ -0,0 +1,111 @@ +import { strict as assert } from 'assert'; +import { Context } from 'mocha'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import FixtureBuilder from '../../fixture-builder'; +import { + clickNestedButton, + defaultGanacheOptions, + regularDelayMs, + unlockWallet, + withFixtures, +} from '../../helpers'; +import { Driver } from '../../webdriver/driver'; + +describe('Token List', function () { + const chainId = CHAIN_IDS.MAINNET; + const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; + const symbol = 'ABC'; + + const fixtures = { + fixtures: new FixtureBuilder({ inputChainId: chainId }).build(), + ganacheOptions: { + ...defaultGanacheOptions, + chainId: parseInt(chainId, 16), + }, + }; + + const importToken = async (driver: Driver) => { + await driver.clickElement({ text: 'Import', tag: 'button' }); + await clickNestedButton(driver, 'Custom token'); + await driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + tokenAddress, + ); + await driver.waitForSelector('p.mm-box--color-error-default'); + await driver.fill( + '[data-testid="import-tokens-modal-custom-symbol"]', + symbol, + ); + await driver.delay(2000); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); + await driver.findElement({ text: 'Token imported', tag: 'h6' }); + }; + + it('should sort alphabetically and by decreasing balance', async function () { + await withFixtures( + { + ...fixtures, + title: (this as Context).test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await importToken(driver); + + const tokenListBeforeSorting = await driver.findElements( + '[data-testid="multichain-token-list-button"]', + ); + const tokenSymbolsBeforeSorting = await Promise.all( + tokenListBeforeSorting.map(async (tokenElement) => { + return tokenElement.getText(); + }), + ); + + assert.ok(tokenSymbolsBeforeSorting[0].includes('Ethereum')); + + await await driver.clickElement( + '[data-testid="sort-by-popover-toggle"]', + ); + await await driver.clickElement('[data-testid="sortByAlphabetically"]'); + + await driver.delay(regularDelayMs); + const tokenListAfterSortingAlphabetically = await driver.findElements( + '[data-testid="multichain-token-list-button"]', + ); + const tokenListSymbolsAfterSortingAlphabetically = await Promise.all( + tokenListAfterSortingAlphabetically.map(async (tokenElement) => { + return tokenElement.getText(); + }), + ); + + assert.ok( + tokenListSymbolsAfterSortingAlphabetically[0].includes('ABC'), + ); + + await await driver.clickElement( + '[data-testid="sort-by-popover-toggle"]', + ); + await await driver.clickElement( + '[data-testid="sortByDecliningBalance"]', + ); + + await driver.delay(regularDelayMs); + const tokenListBeforeSortingByDecliningBalance = + await driver.findElements( + '[data-testid="multichain-token-list-button"]', + ); + + const tokenListAfterSortingByDecliningBalance = await Promise.all( + tokenListBeforeSortingByDecliningBalance.map(async (tokenElement) => { + return tokenElement.getText(); + }), + ); + assert.ok( + tokenListAfterSortingByDecliningBalance[0].includes('Ethereum'), + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/transaction/change-assets.spec.js b/test/e2e/tests/transaction/change-assets.spec.js index 11bb7489a829..7ce971fd8d80 100644 --- a/test/e2e/tests/transaction/change-assets.spec.js +++ b/test/e2e/tests/transaction/change-assets.spec.js @@ -342,7 +342,7 @@ describe('Change assets', function () { // Make sure gas is updated by resetting amount and hex data // Note: this is needed until the race condition is fixed on the wallet level (issue #25243) - await driver.fill('[data-testid="currency-input"]', '2'); + await driver.fill('[data-testid="currency-input"]', '2.000042'); await hexDataLocator.fill('0x'); await hexDataLocator.fill(''); diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index a9f65cad0714..900c49731594 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -53,6 +53,8 @@ @import 'srp-input/srp-input'; @import 'snaps/snap-privacy-warning/index'; @import 'tab-bar/index'; +@import 'assets/asset-list/asset-list-control-bar/index'; +@import 'assets/asset-list/sort-control/index'; @import 'assets/token-cell/token-cell'; @import 'assets/token-list-display/token-list-display'; @import 'transaction-activity-log/index'; diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx new file mode 100644 index 000000000000..696c3ca7c89f --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -0,0 +1,99 @@ +import React, { useRef, useState } from 'react'; +import { + Box, + ButtonBase, + ButtonBaseSize, + IconName, + Popover, + PopoverPosition, +} from '../../../../component-library'; +import SortControl from '../sort-control'; +import { + BackgroundColor, + BorderColor, + BorderStyle, + Display, + JustifyContent, + TextColor, +} from '../../../../../helpers/constants/design-system'; +import ImportControl from '../import-control'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../../../../app/scripts/lib/util'; +import { + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, +} from '../../../../../../shared/constants/app'; + +type AssetListControlBarProps = { + showTokensLinks?: boolean; +}; + +const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { + const t = useI18nContext(); + const controlBarRef = useRef<HTMLDivElement>(null); // Create a ref + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const windowType = getEnvironmentType(); + const isFullScreen = + windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP; + + const handleOpenPopover = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + return ( + <Box + className="asset-list-control-bar" + ref={controlBarRef} + display={Display.Flex} + justifyContent={JustifyContent.spaceBetween} + marginLeft={4} + marginRight={4} + paddingTop={4} + > + <ButtonBase + data-testid="sort-by-popover-toggle" + className="asset-list-control-bar__button" + onClick={handleOpenPopover} + size={ButtonBaseSize.Sm} + endIconName={IconName.ArrowDown} + backgroundColor={ + isPopoverOpen + ? BackgroundColor.backgroundPressed + : BackgroundColor.backgroundDefault + } + borderColor={BorderColor.borderMuted} + borderStyle={BorderStyle.solid} + color={TextColor.textDefault} + > + {t('sortBy')} + </ButtonBase> + <ImportControl showTokensLinks={showTokensLinks} /> + <Popover + onClickOutside={closePopover} + isOpen={isPopoverOpen} + position={PopoverPosition.BottomStart} + referenceElement={controlBarRef.current} + matchWidth={!isFullScreen} + style={{ + zIndex: 10, + display: 'flex', + flexDirection: 'column', + padding: 0, + minWidth: isFullScreen ? '325px' : '', + }} + > + <SortControl handleClose={closePopover} /> + </Popover> + </Box> + ); +}; + +export default AssetListControlBar; diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss new file mode 100644 index 000000000000..3ed7ae082766 --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss @@ -0,0 +1,8 @@ +.asset-list-control-bar { + padding-top: 8px; + padding-bottom: 8px; + + &__button:hover { + background-color: var(--color-background-hover); + } +} diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/index.ts b/ui/components/app/assets/asset-list/asset-list-control-bar/index.ts new file mode 100644 index 000000000000..c9eff91c6fcf --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/index.ts @@ -0,0 +1 @@ +export { default } from './asset-list-control-bar'; diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index a84ec99037f9..5cfeb6803875 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -1,22 +1,15 @@ import React, { useContext, useState } from 'react'; import { useSelector } from 'react-redux'; import TokenList from '../token-list'; -import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; +import { PRIMARY } from '../../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - getShouldHideZeroBalanceTokens, getSelectedAccount, - getPreferences, } from '../../../../selectors'; import { - getMultichainCurrentNetwork, - getMultichainNativeCurrency, getMultichainIsEvm, - getMultichainShouldShowFiat, - getMultichainCurrencyImage, - getMultichainIsMainnet, getMultichainSelectedAccountCachedBalance, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getMultichainIsBitcoin, @@ -32,14 +25,10 @@ import { import DetectedToken from '../../detected-token/detected-token'; import { DetectedTokensBanner, - TokenListItem, ImportTokenLink, ReceiveModal, } from '../../../multichain'; -import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; -import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { @@ -48,42 +37,30 @@ import { } from '../../../multichain/ramps-card/ramps-card'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF +import AssetListControlBar from './asset-list-control-bar'; +import NativeToken from './native-token'; export type TokenWithBalance = { address: string; symbol: string; - string: string; + string?: string; image: string; + secondary?: string; + tokenFiatAmount?: string; + isNative?: boolean; }; -type AssetListProps = { +export type AssetListProps = { onClickAsset: (arg: string) => void; - showTokensLinks: boolean; + showTokensLinks?: boolean; }; const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const nativeCurrency = useSelector(getMultichainNativeCurrency); - const showFiat = useSelector(getMultichainShouldShowFiat); - const isMainnet = useSelector(getMultichainIsMainnet); - const { showNativeTokenAsMainBalance } = useSelector(getPreferences); - const { chainId, ticker, type, rpcUrl } = useSelector( - getMultichainCurrentNetwork, - ); - const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( - chainId, - ticker, - type, - rpcUrl, - ); + const selectedAccount = useSelector(getSelectedAccount); const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const balance = useSelector(getMultichainSelectedAccountCachedBalance); - const balanceIsLoading = !balance; - const selectedAccount = useSelector(getSelectedAccount); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); const { currency: primaryCurrency, @@ -92,27 +69,12 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ethNumberOfDecimals: 4, shouldCheckShowNativeToken: true, }); - const { - currency: secondaryCurrency, - numberOfDecimals: secondaryNumberOfDecimals, - } = useUserPreferencedCurrency(SECONDARY, { - ethNumberOfDecimals: 4, - shouldCheckShowNativeToken: true, - }); - const [primaryCurrencyDisplay, primaryCurrencyProperties] = - useCurrencyDisplay(balance, { - numberOfDecimals: primaryNumberOfDecimals, - currency: primaryCurrency, - }); - - const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = - useCurrencyDisplay(balance, { - numberOfDecimals: secondaryNumberOfDecimals, - currency: secondaryCurrency, - }); + const [, primaryCurrencyProperties] = useCurrencyDisplay(balance, { + numberOfDecimals: primaryNumberOfDecimals, + currency: primaryCurrency, + }); - const primaryTokenImage = useSelector(getMultichainCurrencyImage); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; const isTokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, @@ -126,23 +88,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { setShowReceiveModal(true); }; - const accountTotalFiatBalance = useAccountTotalFiatBalance( - selectedAccount, - shouldHideZeroBalanceTokens, - ); - - const tokensWithBalances = - accountTotalFiatBalance.tokensWithBalances as TokenWithBalance[]; - - const { loading } = accountTotalFiatBalance; - - tokensWithBalances.forEach((token) => { - token.string = roundToDecimalPlacesRemovingExtraZeroes( - token.string, - 5, - ) as string; - }); - const balanceIsZero = useSelector( getMultichainSelectedAccountCachedBalanceIsZero, ); @@ -150,6 +95,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; + const isBtc = useSelector(getMultichainIsBitcoin); ///: END:ONLY_INCLUDE_IF const isEvm = useSelector(getMultichainIsEvm); @@ -157,15 +103,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBtc = useSelector(getMultichainIsBitcoin); - ///: END:ONLY_INCLUDE_IF - - let isStakeable = isMainnet && isEvm; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - isStakeable = false; - ///: END:ONLY_INCLUDE_IF - return ( <> {detectedTokens.length > 0 && @@ -176,6 +113,21 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { margin={4} /> )} + <AssetListControlBar showTokensLinks={showTokensLinks} /> + <TokenList + nativeToken={<NativeToken onClickAsset={onClickAsset} />} + onTokenClick={(tokenAddress: string) => { + onClickAsset(tokenAddress); + trackEvent({ + event: MetaMetricsEventName.TokenScreenOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + token_symbol: primaryCurrencyProperties.suffix, + location: 'Home', + }, + }); + }} + /> { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) shouldShowBuy ? ( @@ -192,43 +144,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ) : null ///: END:ONLY_INCLUDE_IF } - <TokenListItem - onClick={() => onClickAsset(nativeCurrency)} - title={nativeCurrency} - // The primary and secondary currencies are subject to change based on the user's settings - // TODO: rename this primary/secondary concept here to be more intuitive, regardless of setting - primary={isOriginalNativeSymbol ? secondaryCurrencyDisplay : undefined} - tokenSymbol={ - showNativeTokenAsMainBalance - ? primaryCurrencyProperties.suffix - : secondaryCurrencyProperties.suffix - } - secondary={ - showFiat && isOriginalNativeSymbol - ? primaryCurrencyDisplay - : undefined - } - tokenImage={balanceIsLoading ? null : primaryTokenImage} - isOriginalTokenSymbol={isOriginalNativeSymbol} - isNativeCurrency - isStakeable={isStakeable} - showPercentage - /> - <TokenList - tokens={tokensWithBalances} - loading={loading} - onTokenClick={(tokenAddress: string) => { - onClickAsset(tokenAddress); - trackEvent({ - event: MetaMetricsEventName.TokenScreenOpened, - category: MetaMetricsEventCategory.Navigation, - properties: { - token_symbol: primaryCurrencyProperties.suffix, - location: 'Home', - }, - }); - }} - /> {shouldShowTokensLinks && ( <ImportTokenLink margin={4} diff --git a/ui/components/app/assets/asset-list/import-control/import-control.tsx b/ui/components/app/assets/asset-list/import-control/import-control.tsx new file mode 100644 index 000000000000..37af8714e2c7 --- /dev/null +++ b/ui/components/app/assets/asset-list/import-control/import-control.tsx @@ -0,0 +1,63 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + ButtonBase, + ButtonBaseSize, + IconName, +} from '../../../../component-library'; +import { + BackgroundColor, + BorderColor, + BorderStyle, + TextColor, +} from '../../../../../helpers/constants/design-system'; +import { showImportTokensModal } from '../../../../../store/actions'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../../../shared/constants/metametrics'; +import { getMultichainIsEvm } from '../../../../../selectors/multichain'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; + +type AssetListControlBarProps = { + showTokensLinks?: boolean; +}; + +const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + const t = useI18nContext(); + const isEvm = useSelector(getMultichainIsEvm); + // NOTE: Since we can parametrize it now, we keep the original behavior + // for EVM assets + const shouldShowTokensLinks = showTokensLinks ?? isEvm; + + return ( + <ButtonBase + className="asset-list-control-bar__button" + data-testid="import-token-button" + disabled={!shouldShowTokensLinks} + size={ButtonBaseSize.Sm} + startIconName={IconName.Add} + backgroundColor={BackgroundColor.backgroundDefault} + borderColor={BorderColor.borderMuted} + borderStyle={BorderStyle.solid} + color={TextColor.textDefault} + onClick={() => { + dispatch(showImportTokensModal()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'HOME', + }, + }); + }} + > + {t('import')} + </ButtonBase> + ); +}; + +export default AssetListControlBar; diff --git a/ui/components/app/assets/asset-list/import-control/index.ts b/ui/components/app/assets/asset-list/import-control/index.ts new file mode 100644 index 000000000000..b871f41ae8b4 --- /dev/null +++ b/ui/components/app/assets/asset-list/import-control/index.ts @@ -0,0 +1 @@ +export { default } from './import-control'; diff --git a/ui/components/app/assets/asset-list/native-token/index.ts b/ui/components/app/assets/asset-list/native-token/index.ts new file mode 100644 index 000000000000..6feb276bed54 --- /dev/null +++ b/ui/components/app/assets/asset-list/native-token/index.ts @@ -0,0 +1 @@ +export { default } from './native-token'; diff --git a/ui/components/app/assets/asset-list/native-token/native-token.tsx b/ui/components/app/assets/asset-list/native-token/native-token.tsx new file mode 100644 index 000000000000..cf0191b3de66 --- /dev/null +++ b/ui/components/app/assets/asset-list/native-token/native-token.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { + getMultichainCurrentNetwork, + getMultichainNativeCurrency, + getMultichainIsEvm, + getMultichainCurrencyImage, + getMultichainIsMainnet, + getMultichainSelectedAccountCachedBalance, +} from '../../../../../selectors/multichain'; +import { TokenListItem } from '../../../../multichain'; +import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol'; +import { AssetListProps } from '../asset-list'; +import { useNativeTokenBalance } from './use-native-token-balance'; +// import { getPreferences } from '../../../../../selectors'; + +const NativeToken = ({ onClickAsset }: AssetListProps) => { + const nativeCurrency = useSelector(getMultichainNativeCurrency); + const isMainnet = useSelector(getMultichainIsMainnet); + const { chainId, ticker, type, rpcUrl } = useSelector( + getMultichainCurrentNetwork, + ); + const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( + chainId, + ticker, + type, + rpcUrl, + ); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + const balanceIsLoading = !balance; + + const { string, symbol, secondary } = useNativeTokenBalance(); + + const primaryTokenImage = useSelector(getMultichainCurrencyImage); + + const isEvm = useSelector(getMultichainIsEvm); + + let isStakeable = isMainnet && isEvm; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + isStakeable = false; + ///: END:ONLY_INCLUDE_IF + + return ( + <TokenListItem + onClick={() => onClickAsset(nativeCurrency)} + title={nativeCurrency} + primary={string} + tokenSymbol={symbol} + secondary={secondary} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + isOriginalTokenSymbol={isOriginalNativeSymbol} + isNativeCurrency + isStakeable={isStakeable} + showPercentage + /> + ); +}; + +export default NativeToken; diff --git a/ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts b/ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts new file mode 100644 index 000000000000..a14e65ac572b --- /dev/null +++ b/ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts @@ -0,0 +1,94 @@ +import currencyFormatter from 'currency-formatter'; +import { useSelector } from 'react-redux'; + +import { + getMultichainCurrencyImage, + getMultichainCurrentNetwork, + getMultichainSelectedAccountCachedBalance, + getMultichainShouldShowFiat, +} from '../../../../../selectors/multichain'; +import { getCurrentCurrency, getPreferences } from '../../../../../selectors'; +import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol'; +import { PRIMARY, SECONDARY } from '../../../../../helpers/constants/common'; +import { useUserPreferencedCurrency } from '../../../../../hooks/useUserPreferencedCurrency'; +import { useCurrencyDisplay } from '../../../../../hooks/useCurrencyDisplay'; +import { TokenWithBalance } from '../asset-list'; + +export const useNativeTokenBalance = () => { + const showFiat = useSelector(getMultichainShouldShowFiat); + const primaryTokenImage = useSelector(getMultichainCurrencyImage); + const { showNativeTokenAsMainBalance } = useSelector(getPreferences); + const { chainId, ticker, type, rpcUrl } = useSelector( + getMultichainCurrentNetwork, + ); + const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( + chainId, + ticker, + type, + rpcUrl, + ); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + const currentCurrency = useSelector(getCurrentCurrency); + const { + currency: primaryCurrency, + numberOfDecimals: primaryNumberOfDecimals, + } = useUserPreferencedCurrency(PRIMARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); + const { + currency: secondaryCurrency, + numberOfDecimals: secondaryNumberOfDecimals, + } = useUserPreferencedCurrency(SECONDARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); + + const [primaryCurrencyDisplay, primaryCurrencyProperties] = + useCurrencyDisplay(balance, { + numberOfDecimals: primaryNumberOfDecimals, + currency: primaryCurrency, + }); + + const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = + useCurrencyDisplay(balance, { + numberOfDecimals: secondaryNumberOfDecimals, + currency: secondaryCurrency, + }); + + const primaryBalance = isOriginalNativeSymbol + ? secondaryCurrencyDisplay + : undefined; + + const secondaryBalance = + showFiat && isOriginalNativeSymbol ? primaryCurrencyDisplay : undefined; + + const tokenSymbol = showNativeTokenAsMainBalance + ? primaryCurrencyProperties.suffix + : secondaryCurrencyProperties.suffix; + + const unformattedTokenFiatAmount = showNativeTokenAsMainBalance + ? secondaryCurrencyDisplay.toString() + : primaryCurrencyDisplay.toString(); + + // useCurrencyDisplay passes along the symbol and formatting into the value here + // for sorting we need the raw value, without the currency and it should be decimal + // this is the easiest way to do this without extensive refactoring of useCurrencyDisplay + const tokenFiatAmount = currencyFormatter + .unformat(unformattedTokenFiatAmount, { + code: currentCurrency.toUpperCase(), + }) + .toString(); + + const nativeTokenWithBalance: TokenWithBalance = { + address: '', + symbol: tokenSymbol ?? '', + string: primaryBalance, + image: primaryTokenImage, + secondary: secondaryBalance, + tokenFiatAmount, + isNative: true, + }; + + return nativeTokenWithBalance; +}; diff --git a/ui/components/app/assets/asset-list/sort-control/index.scss b/ui/components/app/assets/asset-list/sort-control/index.scss new file mode 100644 index 000000000000..76e61c1025ae --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/index.scss @@ -0,0 +1,27 @@ +.selectable-list-item-wrapper { + position: relative; +} + +.selectable-list-item { + cursor: pointer; + padding: 16px; + + &--selected { + background: var(--color-primary-muted); + } + + &:not(.selectable-list-item--selected) { + &:hover, + &:focus-within { + background: var(--color-background-default-hover); + } + } + + &__selected-indicator { + width: 4px; + height: calc(100% - 8px); + position: absolute; + top: 4px; + left: 4px; + } +} diff --git a/ui/components/app/assets/asset-list/sort-control/index.ts b/ui/components/app/assets/asset-list/sort-control/index.ts new file mode 100644 index 000000000000..7e5ecace780f --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/index.ts @@ -0,0 +1 @@ +export { default } from './sort-control'; diff --git a/ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx b/ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx new file mode 100644 index 000000000000..4aac598bd838 --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import { useSelector } from 'react-redux'; +import { setTokenSortConfig } from '../../../../../store/actions'; +import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { getCurrentCurrency, getPreferences } from '../../../../../selectors'; +import SortControl from './sort-control'; + +// Mock the sortAssets utility +jest.mock('../../util/sort', () => ({ + sortAssets: jest.fn(() => []), // mock sorting implementation +})); + +// Mock the setTokenSortConfig action creator +jest.mock('../../../../../store/actions', () => ({ + setTokenSortConfig: jest.fn(), +})); + +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useSelector: jest.fn(), + useDispatch: () => mockDispatch, + }; +}); + +const mockHandleClose = jest.fn(); + +describe('SortControl', () => { + const mockTrackEvent = jest.fn(); + + const renderComponent = () => { + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === getPreferences) { + return { + key: 'tokenFiatAmount', + sortCallback: 'stringNumeric', + order: 'dsc', + }; + } + if (selector === getCurrentCurrency) { + return 'usd'; + } + return undefined; + }); + + return renderWithProvider( + <MetaMetricsContext.Provider value={mockTrackEvent}> + <SortControl handleClose={mockHandleClose} /> + </MetaMetricsContext.Provider>, + ); + }; + + beforeEach(() => { + mockDispatch.mockClear(); + mockTrackEvent.mockClear(); + (setTokenSortConfig as jest.Mock).mockClear(); + }); + + it('renders correctly', () => { + renderComponent(); + + expect(screen.getByTestId('sortByAlphabetically')).toBeInTheDocument(); + expect(screen.getByTestId('sortByDecliningBalance')).toBeInTheDocument(); + }); + + it('dispatches setTokenSortConfig with expected config, and tracks event when Alphabetically is clicked', () => { + renderComponent(); + + const alphabeticallyButton = screen.getByTestId( + 'sortByAlphabetically__button', + ); + fireEvent.click(alphabeticallyButton); + + expect(mockDispatch).toHaveBeenCalled(); + expect(setTokenSortConfig).toHaveBeenCalledWith({ + key: 'symbol', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + expect(mockTrackEvent).toHaveBeenCalledWith({ + category: 'Settings', + event: 'Token Sort Preference', + properties: { + token_sort_preference: 'symbol', + }, + }); + }); + + it('dispatches setTokenSortConfig with expected config, and tracks event when Declining balance is clicked', () => { + renderComponent(); + + const decliningBalanceButton = screen.getByTestId( + 'sortByDecliningBalance__button', + ); + fireEvent.click(decliningBalanceButton); + + expect(mockDispatch).toHaveBeenCalled(); + expect(setTokenSortConfig).toHaveBeenCalledWith({ + key: 'tokenFiatAmount', + sortCallback: 'stringNumeric', + order: 'dsc', + }); + + expect(mockTrackEvent).toHaveBeenCalledWith({ + category: 'Settings', + event: 'Token Sort Preference', + properties: { + token_sort_preference: 'tokenFiatAmount', + }, + }); + }); +}); diff --git a/ui/components/app/assets/asset-list/sort-control/sort-control.tsx b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx new file mode 100644 index 000000000000..c45a5488f1a6 --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx @@ -0,0 +1,116 @@ +import React, { ReactNode, useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import classnames from 'classnames'; +import { Box, Text } from '../../../../component-library'; +import { SortOrder, SortingCallbacksT } from '../../util/sort'; +import { + BackgroundColor, + BorderRadius, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { setTokenSortConfig } from '../../../../../store/actions'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsUserTrait, +} from '../../../../../../shared/constants/metametrics'; +import { getCurrentCurrency, getPreferences } from '../../../../../selectors'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { getCurrencySymbol } from '../../../../../helpers/utils/common.util'; + +// intentionally used generic naming convention for styled selectable list item +// inspired from ui/components/multichain/network-list-item +// should probably be broken out into component library +type SelectableListItemProps = { + isSelected: boolean; + onClick?: React.MouseEventHandler<HTMLSpanElement>; + testId?: string; + children: ReactNode; +}; + +export const SelectableListItem = ({ + isSelected, + onClick, + testId, + children, +}: SelectableListItemProps) => { + return ( + <Box className="selectable-list-item-wrapper" data-testid={testId}> + <Box + data-testid={`${testId}__button`} + className={classnames('selectable-list-item', { + 'selectable-list-item--selected': isSelected, + })} + onClick={onClick} + > + <Text variant={TextVariant.bodyMdMedium} color={TextColor.textDefault}> + {children} + </Text> + </Box> + {isSelected && ( + <Box + className="selectable-list-item__selected-indicator" + borderRadius={BorderRadius.pill} + backgroundColor={BackgroundColor.primaryDefault} + /> + )} + </Box> + ); +}; + +type SortControlProps = { + handleClose: () => void; +}; + +const SortControl = ({ handleClose }: SortControlProps) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const { tokenSortConfig } = useSelector(getPreferences); + const currentCurrency = useSelector(getCurrentCurrency); + + const dispatch = useDispatch(); + + const handleSort = ( + key: string, + sortCallback: keyof SortingCallbacksT, + order: SortOrder, + ) => { + dispatch( + setTokenSortConfig({ + key, + sortCallback, + order, + }), + ); + trackEvent({ + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.TokenSortPreference, + properties: { + [MetaMetricsUserTrait.TokenSortPreference]: key, + }, + }); + handleClose(); + }; + return ( + <> + <SelectableListItem + isSelected={tokenSortConfig?.key === 'symbol'} + onClick={() => handleSort('symbol', 'alphaNumeric', 'asc')} + testId="sortByAlphabetically" + > + {t('sortByAlphabetically')} + </SelectableListItem> + <SelectableListItem + isSelected={tokenSortConfig?.key === 'tokenFiatAmount'} + onClick={() => handleSort('tokenFiatAmount', 'stringNumeric', 'dsc')} + testId="sortByDecliningBalance" + > + {t('sortByDecliningBalance', [getCurrencySymbol(currentCurrency)])} + </SelectableListItem> + </> + ); +}; + +export default SortControl; diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 2cd5cb84b8ab..5f5b43d6c098 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -10,7 +10,7 @@ import { getIntlLocale } from '../../../../ducks/locale/locale'; type TokenCellProps = { address: string; symbol: string; - string: string; + string?: string; image: string; onClick?: (arg: string) => void; }; diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 194ea2762191..8a107b154fb9 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { ReactNode, useMemo } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; import TokenCell from '../token-cell'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { Box } from '../../../component-library'; @@ -8,39 +9,87 @@ import { JustifyContent, } from '../../../../helpers/constants/design-system'; import { TokenWithBalance } from '../asset-list/asset-list'; +import { sortAssets } from '../util/sort'; +import { + getPreferences, + getSelectedAccount, + getShouldHideZeroBalanceTokens, + getTokenExchangeRates, +} from '../../../../selectors'; +import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; +import { getConversionRate } from '../../../../ducks/metamask/metamask'; +import { useNativeTokenBalance } from '../asset-list/native-token/use-native-token-balance'; type TokenListProps = { onTokenClick: (arg: string) => void; - tokens: TokenWithBalance[]; - loading: boolean; + nativeToken: ReactNode; }; export default function TokenList({ onTokenClick, - tokens, - loading = false, + nativeToken, }: TokenListProps) { const t = useI18nContext(); + const { tokenSortConfig } = useSelector(getPreferences); + const selectedAccount = useSelector(getSelectedAccount); + const conversionRate = useSelector(getConversionRate); + const nativeTokenWithBalance = useNativeTokenBalance(); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const contractExchangeRates = useSelector( + getTokenExchangeRates, + shallowEqual, + ); + const { tokensWithBalances, loading } = useAccountTotalFiatBalance( + selectedAccount, + shouldHideZeroBalanceTokens, + ) as { + tokensWithBalances: TokenWithBalance[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mergedRates: any; + loading: boolean; + }; - if (loading) { - return ( - <Box - display={Display.Flex} - alignItems={AlignItems.center} - justifyContent={JustifyContent.center} - padding={7} - data-testid="token-list-loading-message" - > - {t('loadingTokens')} - </Box> + const sortedTokens = useMemo(() => { + return sortAssets( + [nativeTokenWithBalance, ...tokensWithBalances], + tokenSortConfig, ); - } + }, [ + tokensWithBalances, + tokenSortConfig, + conversionRate, + contractExchangeRates, + ]); - return ( + return loading ? ( + <Box + display={Display.Flex} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + padding={7} + data-testid="token-list-loading-message" + > + {t('loadingTokens')} + </Box> + ) : ( <div> - {tokens.map((tokenData, index) => ( - <TokenCell key={index} {...tokenData} onClick={onTokenClick} /> - ))} + {sortedTokens.map((tokenData) => { + if (tokenData?.isNative) { + // we need cloneElement so that we can pass the unique key + return React.cloneElement(nativeToken as React.ReactElement, { + key: `${tokenData.symbol}-${tokenData.address}`, + }); + } + return ( + <TokenCell + key={`${tokenData.symbol}-${tokenData.address}`} + {...tokenData} + onClick={onTokenClick} + /> + ); + })} </div> ); } diff --git a/ui/components/app/assets/util/sort.test.ts b/ui/components/app/assets/util/sort.test.ts new file mode 100644 index 000000000000..f4a99e31b641 --- /dev/null +++ b/ui/components/app/assets/util/sort.test.ts @@ -0,0 +1,263 @@ +import { sortAssets } from './sort'; + +type MockAsset = { + name: string; + balance: string; + createdAt: Date; + profile: { + id: string; + info?: { + category?: string; + }; + }; +}; + +const mockAssets: MockAsset[] = [ + { + name: 'Asset Z', + balance: '500', + createdAt: new Date('2023-01-01'), + profile: { id: '1', info: { category: 'gold' } }, + }, + { + name: 'Asset A', + balance: '600', + createdAt: new Date('2022-05-15'), + profile: { id: '4', info: { category: 'silver' } }, + }, + { + name: 'Asset B', + balance: '400', + createdAt: new Date('2021-07-20'), + profile: { id: '2', info: { category: 'bronze' } }, + }, +]; + +// Define the sorting tests +describe('sortAssets function - nested value handling with dates and numeric sorting', () => { + test('sorts by name in ascending order', () => { + const sortedById = sortAssets(mockAssets, { + key: 'name', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + expect(sortedById[0].name).toBe('Asset A'); + expect(sortedById[sortedById.length - 1].name).toBe('Asset Z'); + }); + + test('sorts by balance in ascending order (stringNumeric)', () => { + const sortedById = sortAssets(mockAssets, { + key: 'balance', + sortCallback: 'stringNumeric', + order: 'asc', + }); + + expect(sortedById[0].balance).toBe('400'); + expect(sortedById[sortedById.length - 1].balance).toBe('600'); + }); + + test('sorts by balance in ascending order (numeric)', () => { + const sortedById = sortAssets(mockAssets, { + key: 'balance', + sortCallback: 'numeric', + order: 'asc', + }); + + expect(sortedById[0].balance).toBe('400'); + expect(sortedById[sortedById.length - 1].balance).toBe('600'); + }); + + test('sorts by profile.id in ascending order', () => { + const sortedById = sortAssets(mockAssets, { + key: 'profile.id', + sortCallback: 'stringNumeric', + order: 'asc', + }); + + expect(sortedById[0].profile.id).toBe('1'); + expect(sortedById[sortedById.length - 1].profile.id).toBe('4'); + }); + + test('sorts by profile.id in descending order', () => { + const sortedById = sortAssets(mockAssets, { + key: 'profile.id', + sortCallback: 'stringNumeric', + order: 'dsc', + }); + + expect(sortedById[0].profile.id).toBe('4'); + expect(sortedById[sortedById.length - 1].profile.id).toBe('1'); + }); + + test('sorts by deeply nested profile.info.category in ascending order', () => { + const sortedByCategory = sortAssets(mockAssets, { + key: 'profile.info.category', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + // Expecting the assets with defined categories to be sorted first + expect(sortedByCategory[0].profile.info?.category).toBe('bronze'); + expect( + sortedByCategory[sortedByCategory.length - 1].profile.info?.category, + ).toBe('silver'); + }); + + test('sorts by createdAt (date) in ascending order', () => { + const sortedByDate = sortAssets(mockAssets, { + key: 'createdAt', + sortCallback: 'date', + order: 'asc', + }); + + expect(sortedByDate[0].createdAt).toEqual(new Date('2021-07-20')); + expect(sortedByDate[sortedByDate.length - 1].createdAt).toEqual( + new Date('2023-01-01'), + ); + }); + + test('sorts by createdAt (date) in descending order', () => { + const sortedByDate = sortAssets(mockAssets, { + key: 'createdAt', + sortCallback: 'date', + order: 'dsc', + }); + + expect(sortedByDate[0].createdAt).toEqual(new Date('2023-01-01')); + expect(sortedByDate[sortedByDate.length - 1].createdAt).toEqual( + new Date('2021-07-20'), + ); + }); + + test('handles undefined deeply nested value gracefully when sorting', () => { + const invlaidAsset = { + name: 'Asset Y', + balance: '600', + createdAt: new Date('2024-01-01'), + profile: { id: '3' }, // No category info + }; + const sortedByCategory = sortAssets([...mockAssets, invlaidAsset], { + key: 'profile.info.category', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + // Expect the undefined categories to be at the end + expect( + // @ts-expect-error // testing for undefined value + sortedByCategory[sortedByCategory.length - 1].profile.info?.category, + ).toBeUndefined(); + }); +}); + +// Utility function to generate large mock data +function generateLargeMockData(size: number): MockAsset[] { + const mockData: MockAsset[] = []; + for (let i = 0; i < size; i++) { + mockData.push({ + name: `Asset ${String.fromCharCode(65 + (i % 26))}`, + balance: `${Math.floor(Math.random() * 1000)}`, // Random balance between 0 and 999 + createdAt: new Date(Date.now() - Math.random() * 10000000000), // Random date within the past ~115 days + profile: { + id: `${i + 1}`, + info: { + category: ['gold', 'silver', 'bronze'][i % 3], // Cycles between 'gold', 'silver', 'bronze' + }, + }, + }); + } + return mockData; +} + +// Generate a large dataset for testing +const largeDataset = generateLargeMockData(10000); // 10,000 mock assets + +// Define the sorting tests for large datasets +describe('sortAssets function - large dataset handling', () => { + const MAX_EXECUTION_TIME_MS = 500; // Set max allowed execution time (in milliseconds) + + test('sorts large dataset by name in ascending order', () => { + const startTime = Date.now(); + const sortedByName = sortAssets(largeDataset, { + key: 'name', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + expect(sortedByName[0].name).toBe('Asset A'); + expect(sortedByName[sortedByName.length - 1].name).toBe('Asset Z'); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by balance in ascending order', () => { + const startTime = Date.now(); + const sortedByBalance = sortAssets(largeDataset, { + key: 'balance', + sortCallback: 'numeric', + order: 'asc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const balances = sortedByBalance.map((asset) => asset.balance); + expect(balances).toEqual( + balances.slice().sort((a, b) => parseInt(a, 10) - parseInt(b, 10)), + ); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by balance in descending order', () => { + const startTime = Date.now(); + const sortedByBalance = sortAssets(largeDataset, { + key: 'balance', + sortCallback: 'numeric', + order: 'dsc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const balances = sortedByBalance.map((asset) => asset.balance); + expect(balances).toEqual( + balances.slice().sort((a, b) => parseInt(b, 10) - parseInt(a, 10)), + ); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by createdAt (date) in ascending order', () => { + const startTime = Date.now(); + const sortedByDate = sortAssets(largeDataset, { + key: 'createdAt', + sortCallback: 'date', + order: 'asc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const dates = sortedByDate.map((asset) => asset.createdAt.getTime()); + expect(dates).toEqual(dates.slice().sort((a, b) => a - b)); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by createdAt (date) in descending order', () => { + const startTime = Date.now(); + const sortedByDate = sortAssets(largeDataset, { + key: 'createdAt', + sortCallback: 'date', + order: 'dsc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const dates = sortedByDate.map((asset) => asset.createdAt.getTime()); + expect(dates).toEqual(dates.slice().sort((a, b) => b - a)); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); +}); diff --git a/ui/components/app/assets/util/sort.ts b/ui/components/app/assets/util/sort.ts new file mode 100644 index 000000000000..b24a1c8e96a9 --- /dev/null +++ b/ui/components/app/assets/util/sort.ts @@ -0,0 +1,86 @@ +import { get } from 'lodash'; + +export type SortOrder = 'asc' | 'dsc'; +export type SortCriteria = { + key: string; + order?: 'asc' | 'dsc'; + sortCallback: SortCallbackKeys; +}; + +export type SortingType = number | string | Date; +type SortCallbackKeys = keyof SortingCallbacksT; + +export type SortingCallbacksT = { + numeric: (a: number, b: number) => number; + stringNumeric: (a: string, b: string) => number; + alphaNumeric: (a: string, b: string) => number; + date: (a: Date, b: Date) => number; +}; + +// All sortingCallbacks should be asc order, sortAssets function handles asc/dsc +const sortingCallbacks: SortingCallbacksT = { + numeric: (a: number, b: number) => a - b, + stringNumeric: (a: string, b: string) => { + return ( + parseFloat(parseFloat(a).toFixed(5)) - + parseFloat(parseFloat(b).toFixed(5)) + ); + }, + alphaNumeric: (a: string, b: string) => a.localeCompare(b), + date: (a: Date, b: Date) => a.getTime() - b.getTime(), +}; + +// Utility function to access nested properties by key path +function getNestedValue<T>(obj: T, keyPath: string): SortingType { + return get(obj, keyPath) as SortingType; +} + +export function sortAssets<T>(array: T[], criteria: SortCriteria): T[] { + const { key, order = 'asc', sortCallback } = criteria; + + return [...array].sort((a, b) => { + const aValue = getNestedValue(a, key); + const bValue = getNestedValue(b, key); + + // Always move undefined values to the end, regardless of sort order + if (aValue === undefined) { + return 1; + } + + if (bValue === undefined) { + return -1; + } + + let comparison: number; + + switch (sortCallback) { + case 'stringNumeric': + case 'alphaNumeric': + comparison = sortingCallbacks[sortCallback]( + aValue as string, + bValue as string, + ); + break; + case 'numeric': + comparison = sortingCallbacks.numeric( + aValue as number, + bValue as number, + ); + break; + case 'date': + comparison = sortingCallbacks.date(aValue as Date, bValue as Date); + break; + default: + if (aValue < bValue) { + comparison = -1; + } else if (aValue > bValue) { + comparison = 1; + } else { + comparison = 0; + } + } + + // Modify to sort in ascending or descending order + return order === 'asc' ? comparison : -comparison; + }); +} diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-btc.test.tsx index 34cbed54c127..9d265657432b 100644 --- a/ui/components/multichain/account-overview/account-overview-btc.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-btc.test.tsx @@ -40,7 +40,9 @@ describe('AccountOverviewBtc', () => { const { queryByTestId } = render(); expect(queryByTestId('account-overview__asset-tab')).toBeInTheDocument(); - expect(queryByTestId('import-token-button')).not.toBeInTheDocument(); + const button = queryByTestId('import-token-button'); + expect(button).toBeInTheDocument(); // Verify the button is present + expect(button).toBeDisabled(); // Verify the button is disabled // TODO: This one might be required, but we do not really handle tokens for BTC yet... expect(queryByTestId('refresh-list-button')).not.toBeInTheDocument(); }); diff --git a/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap b/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap index 9dd39fea147f..e8fa1e945dba 100644 --- a/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap +++ b/ui/components/multichain/import-token-link/__snapshots__/import-token-link.test.js.snap @@ -5,20 +5,6 @@ exports[`Import Token Link should match snapshot for goerli chainId 1`] = ` <div class="mm-box multichain-import-token-link" > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <button - class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" - data-testid="import-token-button" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-end-1 mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/add.svg');" - /> - Import tokens - </button> - </div> <div class="mm-box mm-box--padding-top-2 mm-box--display-flex mm-box--align-items-center" > @@ -42,20 +28,6 @@ exports[`Import Token Link should match snapshot for mainnet chainId 1`] = ` <div class="mm-box multichain-import-token-link" > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <button - class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" - data-testid="import-token-button" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-end-1 mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/add.svg');" - /> - Import tokens - </button> - </div> <div class="mm-box mm-box--padding-top-2 mm-box--display-flex mm-box--align-items-center" > diff --git a/ui/components/multichain/import-token-link/import-token-link.test.js b/ui/components/multichain/import-token-link/import-token-link.test.js index 722d2b4106ff..641a39d1bff3 100644 --- a/ui/components/multichain/import-token-link/import-token-link.test.js +++ b/ui/components/multichain/import-token-link/import-token-link.test.js @@ -5,6 +5,7 @@ import { detectTokens } from '../../../store/actions'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { mockNetworkState } from '../../../../test/stub/networks'; +import ImportControl from '../../app/assets/asset-list/import-control'; import { ImportTokenLink } from '.'; const mockPushHistory = jest.fn(); @@ -65,7 +66,7 @@ describe('Import Token Link', () => { const store = configureMockStore()(mockState); - renderWithProvider(<ImportTokenLink />, store); + renderWithProvider(<ImportTokenLink />, store); // should this be RefreshTokenLink? const refreshList = screen.getByTestId('refresh-list-button'); fireEvent.click(refreshList); @@ -82,11 +83,11 @@ describe('Import Token Link', () => { const store = configureMockStore()(mockState); - renderWithProvider(<ImportTokenLink />, store); + renderWithProvider(<ImportControl />, store); const importToken = screen.getByTestId('import-token-button'); fireEvent.click(importToken); - expect(screen.getByText('Import tokens')).toBeInTheDocument(); + expect(screen.getByText('Import')).toBeInTheDocument(); }); }); diff --git a/ui/components/multichain/import-token-link/import-token-link.tsx b/ui/components/multichain/import-token-link/import-token-link.tsx index 6c7c9b6a8e6b..022369dd5002 100644 --- a/ui/components/multichain/import-token-link/import-token-link.tsx +++ b/ui/components/multichain/import-token-link/import-token-link.tsx @@ -1,5 +1,5 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React from 'react'; +import { useDispatch } from 'react-redux'; import classnames from 'classnames'; import { ButtonLink, @@ -9,16 +9,7 @@ import { } from '../../component-library'; import { AlignItems, Display } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { detectTokens, showImportTokensModal } from '../../../store/actions'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; -import { - getIsTokenDetectionSupported, - getIsTokenDetectionInactiveOnMainnet, -} from '../../../selectors'; +import { detectTokens } from '../../../store/actions'; import type { BoxProps } from '../../component-library/box'; import type { ImportTokenLinkProps } from './import-token-link.types'; @@ -26,46 +17,14 @@ export const ImportTokenLink: React.FC<ImportTokenLinkProps> = ({ className = '', ...props }): JSX.Element => { - const trackEvent = useContext(MetaMetricsContext); const t = useI18nContext(); const dispatch = useDispatch(); - const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported); - const isTokenDetectionInactiveOnMainnet = useSelector( - getIsTokenDetectionInactiveOnMainnet, - ); - - const isTokenDetectionAvailable = - isTokenDetectionSupported || - isTokenDetectionInactiveOnMainnet || - Boolean(process.env.IN_TEST); return ( <Box className={classnames('multichain-import-token-link', className)} {...(props as BoxProps<'div'>)} > - <Box display={Display.Flex} alignItems={AlignItems.center}> - <ButtonLink - size={ButtonLinkSize.Md} - data-testid="import-token-button" - startIconName={IconName.Add} - onClick={() => { - dispatch(showImportTokensModal()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.TokenImportButtonClicked, - properties: { - location: 'HOME', - }, - }); - }} - > - {isTokenDetectionAvailable - ? t('importTokensCamelCase') - : t('importTokensCamelCase').charAt(0).toUpperCase() + - t('importTokensCamelCase').slice(1)} - </ButtonLink> - </Box> <Box display={Display.Flex} alignItems={AlignItems.center} paddingTop={2}> <ButtonLink size={ButtonLinkSize.Md} diff --git a/ui/components/multichain/ramps-card/index.scss b/ui/components/multichain/ramps-card/index.scss index d46a317e9357..2886649e7d9a 100644 --- a/ui/components/multichain/ramps-card/index.scss +++ b/ui/components/multichain/ramps-card/index.scss @@ -1,5 +1,8 @@ .ramps-card { - padding: 8px 12px; + margin-top: 8px; + margin-left: 16px; + margin-right: 16px; + padding: 12px 16px; &__cta-button { width: fit-content; diff --git a/ui/css/design-system/_colors.scss b/ui/css/design-system/_colors.scss index f8ac7cf93da9..2d948b928012 100644 --- a/ui/css/design-system/_colors.scss +++ b/ui/css/design-system/_colors.scss @@ -1,6 +1,8 @@ $color-map: ( 'background-default': --color-background-default, 'background-alternative': --color-background-alternative, + 'background-hover': --color-background-hover, + 'background-pressed': --color-background-pressed, 'text-default': --color-text-default, 'text-alternative': --color-text-alternative, 'text-muted': --color-text-muted, diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index f8d4f19389a5..8374e812b017 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -54,6 +54,8 @@ export enum Color { export enum BackgroundColor { backgroundDefault = 'background-default', backgroundAlternative = 'background-alternative', + backgroundHover = 'background-hover', + backgroundPressed = 'background-pressed', overlayDefault = 'overlay-default', overlayAlternative = 'overlay-alternative', primaryDefault = 'primary-default', diff --git a/ui/helpers/utils/common.util.js b/ui/helpers/utils/common.util.js index 06272009a2dd..1a22a131d6c3 100644 --- a/ui/helpers/utils/common.util.js +++ b/ui/helpers/utils/common.util.js @@ -1,3 +1,19 @@ export function camelCaseToCapitalize(str = '') { return str.replace(/([A-Z])/gu, ' $1').replace(/^./u, (s) => s.toUpperCase()); } + +export function getCurrencySymbol(currencyCode) { + const supportedCurrencyCodes = { + EUR: '\u20AC', + HKD: '\u0024', + JPY: '\u00A5', + PHP: '\u20B1', + RUB: '\u20BD', + SGD: '\u0024', + USD: '\u0024', + }; + if (supportedCurrencyCodes[currencyCode.toUpperCase()]) { + return supportedCurrencyCodes[currencyCode.toUpperCase()]; + } + return currencyCode.toUpperCase(); +} diff --git a/ui/hooks/useAccountTotalFiatBalance.js b/ui/hooks/useAccountTotalFiatBalance.js index 9a3fe389b4de..b0c9b293c906 100644 --- a/ui/hooks/useAccountTotalFiatBalance.js +++ b/ui/hooks/useAccountTotalFiatBalance.js @@ -1,10 +1,12 @@ import { shallowEqual, useSelector } from 'react-redux'; +import { toChecksumAddress } from 'ethereumjs-util'; import { getAllTokens, getCurrentChainId, getCurrentCurrency, getMetaMaskCachedBalances, getTokenExchangeRates, + getConfirmationExchangeRates, getNativeCurrencyImage, getTokenList, } from '../selectors'; @@ -19,7 +21,7 @@ import { } from '../ducks/metamask/metamask'; import { formatCurrency } from '../helpers/utils/confirm-tx.util'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; -import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; +import { roundToDecimalPlacesRemovingExtraZeroes } from '../helpers/utils/util'; import { useTokenTracker } from './useTokenTracker'; export const useAccountTotalFiatBalance = ( @@ -34,6 +36,7 @@ export const useAccountTotalFiatBalance = ( getTokenExchangeRates, shallowEqual, ); + const confirmationExchangeRates = useSelector(getConfirmationExchangeRates); const cachedBalances = useSelector(getMetaMaskCachedBalances); const balance = cachedBalances?.[account?.address] ?? 0; @@ -59,15 +62,14 @@ export const useAccountTotalFiatBalance = ( hideZeroBalanceTokens: shouldHideZeroBalanceTokens, }); + const mergedRates = { + ...contractExchangeRates, + ...confirmationExchangeRates, + }; + // Create fiat values for token balances const tokenFiatBalances = tokensWithBalances.map((token) => { - const contractExchangeTokenKey = Object.keys(contractExchangeRates).find( - (key) => isEqualCaseInsensitive(key, token.address), - ); - const tokenExchangeRate = - (contractExchangeTokenKey && - contractExchangeRates[contractExchangeTokenKey]) ?? - 0; + const tokenExchangeRate = mergedRates[toChecksumAddress(token.address)]; const totalFiatValue = getTokenFiatAmount( tokenExchangeRate, @@ -136,6 +138,29 @@ export const useAccountTotalFiatBalance = ( ...tokenFiatBalances, ).toString(10); + // we need to append some values to tokensWithBalance for UI + // this code was ported from asset-list + tokensWithBalances.forEach((token) => { + // token.string is the balance displayed in the TokenList UI + token.string = roundToDecimalPlacesRemovingExtraZeroes(token.string, 5); + }); + + // to sort by fiat balance, we need to compute this at this level + tokensWithBalances.forEach((token) => { + const tokenExchangeRate = mergedRates[toChecksumAddress(token.address)]; + + token.tokenFiatAmount = + getTokenFiatAmount( + tokenExchangeRate, + conversionRate, + currentCurrency, + token.string, // tokenAmount + token.symbol, // tokenSymbol + false, // no currency symbol prefix + false, // no ticker symbol suffix + ) || '0'; + }); + // Fiat balance formatted in user's desired currency (ex: "$8.90") const formattedFiat = formatCurrency(totalFiatBalance, currentCurrency); @@ -160,5 +185,6 @@ export const useAccountTotalFiatBalance = ( tokensWithBalances, loading, orderedTokenList, + mergedRates, }; }; diff --git a/ui/hooks/useAccountTotalFiatBalance.test.js b/ui/hooks/useAccountTotalFiatBalance.test.js index c883eb37cc1e..9fb1227367e1 100644 --- a/ui/hooks/useAccountTotalFiatBalance.test.js +++ b/ui/hooks/useAccountTotalFiatBalance.test.js @@ -125,19 +125,25 @@ describe('useAccountTotalFiatBalance', () => { image: undefined, isERC721: undefined, decimals: 6, - string: '0.04857', + string: 0.04857, balanceError: null, + tokenFiatAmount: '0.05', }, { address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', symbol: 'YFI', balance: '1409247882142934', decimals: 18, - string: '0.001409247882142934', + string: 0.00141, balanceError: null, + tokenFiatAmount: '7.52', }, ], loading: false, + mergedRates: { + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': 3.304588, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 0.0006189, + }, orderedTokenList: [ { fiatBalance: '1.85', diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx index 888d70e1d62c..ffd664612a02 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx @@ -141,6 +141,10 @@ describe('useMultichainAccountTotalFiatBalance', () => { expect(result.current).toStrictEqual({ formattedFiat: '$9.41', loading: false, + mergedRates: { + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': 3.304588, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 0.0006189, + }, totalWeiBalance: '14ba1e6a08a9ed', tokensWithBalances: mockTokenBalances, totalFiatBalance: '9.41', diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index ec8c4e96c864..6151fedc687b 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -115,6 +115,13 @@ describe('Routes Component', () => { announcements: {}, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), newPrivacyPolicyToastShownDate: new Date('0'), + preferences: { + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, }, send: { ...mockSendState.send, diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 6e1e33d9531f..6f8080e516ae 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -174,3 +174,5 @@ export const HIDE_KEYRING_SNAP_REMOVAL_RESULT = export const SET_SHOW_NFT_AUTO_DETECT_MODAL_UPGRADE = 'SET_SHOW_NFT_AUTO_DETECT_MODAL_UPGRADE'; + +export const TOKEN_SORT_CRITERIA = 'TOKEN_SORT_CRITERIA'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index a8fadb95ddeb..3dbf61ba0386 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -119,6 +119,7 @@ import { getMethodDataAsync } from '../../shared/lib/four-byte'; import { DecodedTransactionDataResponse } from '../../shared/types/transaction-decode'; import { LastInteractedConfirmationInfo } from '../pages/confirmations/types/confirm'; import { EndTraceRequest } from '../../shared/lib/trace'; +import { SortCriteria } from '../components/app/assets/util/sort'; import { CaveatTypes, EndowmentTypes, @@ -3000,6 +3001,7 @@ export function setFeatureFlag( export function setPreference( preference: string, value: boolean | string | object, + showLoading: boolan = true, ): ThunkAction< Promise<TemporaryPreferenceFlagDef>, MetaMaskReduxState, @@ -3007,13 +3009,13 @@ export function setPreference( AnyAction > { return (dispatch: MetaMaskReduxDispatch) => { - dispatch(showLoadingIndication()); + showLoading && dispatch(showLoadingIndication()); return new Promise<TemporaryPreferenceFlagDef>((resolve, reject) => { callBackgroundMethod<TemporaryPreferenceFlagDef>( 'setPreference', [preference, value], (err, updatedPreferences) => { - dispatch(hideLoadingIndication()); + showLoading && dispatch(hideLoadingIndication()); if (err) { dispatch(displayWarning(err)); reject(err); @@ -3083,6 +3085,10 @@ export function setRedesignedConfirmationsDeveloperEnabled(value: boolean) { return setPreference('isRedesignedConfirmationsDeveloperEnabled', value); } +export function setTokenSortConfig(value: SortCriteria) { + return setPreference('tokenSortConfig', value, false); +} + export function setSmartTransactionsOptInStatus( value: boolean, ): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { From ad1fb6c77f3c53ffa62031874e4a3b70626d28f2 Mon Sep 17 00:00:00 2001 From: Matthew Walsh <matthew.walsh@consensys.net> Date: Wed, 9 Oct 2024 10:01:38 +0100 Subject: [PATCH 096/226] fix: UI startup with no Sentry DSN (#27714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Provide the correct functions to the mock scope used when Sentry is not enabled. Add unit tests to verify trace functions with no Sentry global object. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27714?quickstart=1) ## **Related issues** ## **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 - [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.test.ts | 37 +++++++++++++++++++++++++++++++++++++ shared/lib/trace.ts | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/shared/lib/trace.test.ts b/shared/lib/trace.test.ts index 7cd39eba03d1..ff55ec0f2df0 100644 --- a/shared/lib/trace.test.ts +++ b/shared/lib/trace.test.ts @@ -170,6 +170,27 @@ describe('Trace', () => { expect(setMeasurementMock).toHaveBeenCalledTimes(1); expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); + + it('supports no global Sentry object', () => { + globalThis.sentry = undefined; + + let callbackExecuted = false; + + trace( + { + name: NAME_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + startTime: 123, + }, + () => { + callbackExecuted = true; + }, + ); + + expect(callbackExecuted).toBe(true); + }); }); describe('endTrace', () => { @@ -264,5 +285,21 @@ describe('Trace', () => { expect(spanEndMock).toHaveBeenCalledTimes(0); }); + + it('supports no global Sentry object', () => { + globalThis.sentry = undefined; + + expect(() => { + trace({ + name: NAME_MOCK, + id: ID_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + endTrace({ name: NAME_MOCK, id: ID_MOCK }); + }).not.toThrow(); + }); }); }); diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index a067858a969c..5ca256371502 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -352,7 +352,7 @@ function sentryWithIsolationScope<T>(callback: (scope: Sentry.Scope) => T): T { if (!actual) { const scope = { // eslint-disable-next-line no-empty-function - setTags: () => {}, + setTag: () => {}, } as unknown as Sentry.Scope; return callback(scope); From ca92f78cc1e94bdcd1fc7ade35a973ddf4bf484f Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Wed, 9 Oct 2024 13:53:10 +0200 Subject: [PATCH 097/226] fix(btc): fetch btc balance right after account creation (#27628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The BTC balance is being refreshed automatically by the `(Multichain)BalancesController` and the `(Multichain)BalanceTracker`, this logic is running periodically and is being cached to avoid asking the balance to frequently to the Bitcoin Snap. An non-EVM is automatically being tracked when a `AccountsController:accountAdded` is fired. However, since the messenger's events are being run synchronously, having some asynchronous calls might not be processed right away. To workaround this, we are now force-fetching after the account creation. This logic is being executed in an asynchronous context, which means we can truly `await` the balance fetching which allows the UI to be updated "instantly" (as fast as the Snap will fetch the balance in the Bitcoin case). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27628?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** 1. `yarn start:flask` 2. Enable bitcoin support: Settings > Experimental > "Enable bitcoin support" 3. Create a mainnet account 4. The balance should show right away You can also try the same steps with a testnet account, but it seems that our current providers is having some problems with the testnet balances. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/23f3c499-008a-4456-a00c-ad0cf4232b73 ### **After** https://github.com/user-attachments/assets/c602040c-922f-48c7-b526-a906da41d4f6 ## **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. --- .../lib/accounts/BalancesController.ts | 9 +++ app/scripts/lib/accounts/BalancesTracker.ts | 3 +- .../lib/snap-keyring/bitcoin-wallet-snap.ts | 30 ---------- shared/lib/accounts/bitcoin-wallet-snap.ts | 11 ++++ .../account-list-menu/account-list-menu.tsx | 28 +++------ .../useBitcoinWalletSnapClient.test.ts | 57 +++++++++++++++++++ .../accounts/useBitcoinWalletSnapClient.ts | 52 +++++++++++++++++ 7 files changed, 140 insertions(+), 50 deletions(-) delete mode 100644 app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts create mode 100644 shared/lib/accounts/bitcoin-wallet-snap.ts create mode 100644 ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts create mode 100644 ui/hooks/accounts/useBitcoinWalletSnapClient.ts diff --git a/app/scripts/lib/accounts/BalancesController.ts b/app/scripts/lib/accounts/BalancesController.ts index 9f9ead59ed90..e657fe47e64f 100644 --- a/app/scripts/lib/accounts/BalancesController.ts +++ b/app/scripts/lib/accounts/BalancesController.ts @@ -274,6 +274,8 @@ export class BalancesController extends BaseController< * @param accountId - The account ID. */ async updateBalance(accountId: string) { + // NOTE: No need to track the account here, since we start tracking those when + // the "AccountsController:accountAdded" is fired. await this.#tracker.updateBalance(accountId); } @@ -311,6 +313,13 @@ export class BalancesController extends BaseController< } this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME); + // NOTE: Unfortunately, we cannot update the balance right away here, because + // messenger's events are running synchronously and fetching the balance is + // asynchronous. + // Updating the balance here would resume at some point but the event emitter + // will not `await` this (so we have no real control "when" the balance will + // really be updated), see: + // - https://github.com/MetaMask/core/blob/v213.0.0/packages/accounts-controller/src/AccountsController.ts#L1036-L1039 } /** diff --git a/app/scripts/lib/accounts/BalancesTracker.ts b/app/scripts/lib/accounts/BalancesTracker.ts index 48ecd6f84cca..7359bcd2f8b6 100644 --- a/app/scripts/lib/accounts/BalancesTracker.ts +++ b/app/scripts/lib/accounts/BalancesTracker.ts @@ -102,7 +102,8 @@ export class BalancesTracker { // and try to sync with the "real block time"! const info = this.#balances[accountId]; const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; - if (isOutdated) { + const hasNoBalanceYet = info.lastUpdated === 0; + if (hasNoBalanceYet || isOutdated) { await this.#updateBalance(accountId); this.#balances[accountId].lastUpdated = Date.now(); } diff --git a/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts b/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts deleted file mode 100644 index 98f231607dba..000000000000 --- a/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SnapId } from '@metamask/snaps-sdk'; -import { Sender } from '@metamask/keyring-api'; -import { HandlerType } from '@metamask/snaps-utils'; -import { Json, JsonRpcRequest } from '@metamask/utils'; -// This dependency is still installed as part of the `package.json`, however -// the Snap is being pre-installed only for Flask build (for the moment). -import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { handleSnapRequest } from '../../../../ui/store/actions'; - -export const BITCOIN_WALLET_SNAP_ID: SnapId = - BitcoinWalletSnap.snapId as SnapId; - -export const BITCOIN_WALLET_NAME: string = - BitcoinWalletSnap.manifest.proposedName; - -export class BitcoinWalletSnapSender implements Sender { - send = async (request: JsonRpcRequest): Promise<Json> => { - // We assume the caller of this module is aware of this. If we try to use this module - // without having the pre-installed Snap, this will likely throw an error in - // the `handleSnapRequest` action. - return (await handleSnapRequest({ - origin: 'metamask', - snapId: BITCOIN_WALLET_SNAP_ID, - handler: HandlerType.OnKeyringRequest, - request, - })) as Json; - }; -} diff --git a/shared/lib/accounts/bitcoin-wallet-snap.ts b/shared/lib/accounts/bitcoin-wallet-snap.ts new file mode 100644 index 000000000000..58f367b173e1 --- /dev/null +++ b/shared/lib/accounts/bitcoin-wallet-snap.ts @@ -0,0 +1,11 @@ +import { SnapId } from '@metamask/snaps-sdk'; +// This dependency is still installed as part of the `package.json`, however +// the Snap is being pre-installed only for Flask build (for the moment). +import 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; + +export const BITCOIN_WALLET_NAME: string = + BitcoinWalletSnap.manifest.proposedName; 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 99051042d2dd..2e5925dbf9cf 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -9,18 +9,13 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) InternalAccount, KeyringAccountType, - KeyringClient, ///: END:ONLY_INCLUDE_IF } from '@metamask/keyring-api'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) -import { CaipChainId } from '@metamask/utils'; import { BITCOIN_WALLET_NAME, BITCOIN_WALLET_SNAP_ID, - BitcoinWalletSnapSender, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../../app/scripts/lib/snap-keyring/bitcoin-wallet-snap'; +} from '../../../../shared/lib/accounts/bitcoin-wallet-snap'; ///: END:ONLY_INCLUDE_IF import { Box, @@ -97,6 +92,7 @@ import { hasCreatedBtcTestnetAccount, } from '../../../selectors/accounts'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; +import { useBitcoinWalletSnapClient } from '../../../hooks/accounts/useBitcoinWalletSnapClient'; ///: END:ONLY_INCLUDE_IF import { InternalAccountWithBalance, @@ -261,15 +257,7 @@ export const AccountListMenu = ({ hasCreatedBtcTestnetAccount, ); - const createBitcoinAccount = async (scope: CaipChainId) => { - // Client to create the account using the Bitcoin Snap - const client = new KeyringClient(new BitcoinWalletSnapSender()); - - // This will trigger the Snap account creation flow (+ account renaming) - await client.createAccount({ - scope, - }); - }; + const bitcoinWalletSnapClient = useBitcoinWalletSnapClient(); ///: END:ONLY_INCLUDE_IF const [searchQuery, setSearchQuery] = useState(''); @@ -413,10 +401,12 @@ export const AccountListMenu = ({ // The account creation + renaming is handled by the // Snap account bridge, so we need to close the current - // model + // modal onClose(); - await createBitcoinAccount(MultichainNetworks.BITCOIN); + await bitcoinWalletSnapClient.createAccount( + MultichainNetworks.BITCOIN, + ); }} data-testid="multichain-account-menu-popover-add-btc-account" > @@ -436,10 +426,10 @@ export const AccountListMenu = ({ startIconName={IconName.Add} onClick={async () => { // The account creation + renaming is handled by the Snap account bridge, so - // we need to close the current model + // we need to close the current modal onClose(); - await createBitcoinAccount( + await bitcoinWalletSnapClient.createAccount( MultichainNetworks.BITCOIN_TESTNET, ); }} diff --git a/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts b/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts new file mode 100644 index 000000000000..6032a7636128 --- /dev/null +++ b/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts @@ -0,0 +1,57 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { HandlerType } from '@metamask/snaps-utils'; +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; +import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; +import { + handleSnapRequest, + multichainUpdateBalance, +} from '../../store/actions'; +import { useBitcoinWalletSnapClient } from './useBitcoinWalletSnapClient'; + +jest.mock('../../store/actions', () => ({ + handleSnapRequest: jest.fn(), + multichainUpdateBalance: jest.fn(), +})); + +const mockHandleSnapRequest = handleSnapRequest as jest.Mock; +const mockMultichainUpdateBalance = multichainUpdateBalance as jest.Mock; + +describe('useBitcoinWalletSnapClient', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockAccount = { + address: 'tb1q2hjrlnf8kmtt5dj6e49gqzy6jnpe0sj7ty50cl', + id: '11a33c6b-0d46-43f4-a401-01587d575fd0', + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, + }; + + it('dispatch a Snap keyring request to create a Bitcoin account', async () => { + const { result } = renderHook(() => useBitcoinWalletSnapClient()); + const bitcoinWalletSnapClient = result.current; + + mockHandleSnapRequest.mockResolvedValue(mockAccount); + + await bitcoinWalletSnapClient.createAccount(MultichainNetworks.BITCOIN); + expect(mockHandleSnapRequest).toHaveBeenCalledWith({ + origin: 'metamask', + snapId: BITCOIN_WALLET_SNAP_ID, + handler: HandlerType.OnKeyringRequest, + request: expect.any(Object), + }); + }); + + it('force fetches the balance after creating a Bitcoin account', async () => { + const { result } = renderHook(() => useBitcoinWalletSnapClient()); + const bitcoinWalletSnapClient = result.current; + + mockHandleSnapRequest.mockResolvedValue(mockAccount); + + await bitcoinWalletSnapClient.createAccount(MultichainNetworks.BITCOIN); + expect(mockMultichainUpdateBalance).toHaveBeenCalledWith(mockAccount.id); + }); +}); diff --git a/ui/hooks/accounts/useBitcoinWalletSnapClient.ts b/ui/hooks/accounts/useBitcoinWalletSnapClient.ts new file mode 100644 index 000000000000..debe911ac391 --- /dev/null +++ b/ui/hooks/accounts/useBitcoinWalletSnapClient.ts @@ -0,0 +1,52 @@ +import { KeyringClient, Sender } from '@metamask/keyring-api'; +import { HandlerType } from '@metamask/snaps-utils'; +import { CaipChainId, Json, JsonRpcRequest } from '@metamask/utils'; +import { useMemo } from 'react'; +import { + handleSnapRequest, + multichainUpdateBalance, +} from '../../store/actions'; +import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; + +export class BitcoinWalletSnapSender implements Sender { + send = async (request: JsonRpcRequest): Promise<Json> => { + // We assume the caller of this module is aware of this. If we try to use this module + // without having the pre-installed Snap, this will likely throw an error in + // the `handleSnapRequest` action. + return (await handleSnapRequest({ + origin: 'metamask', + snapId: BITCOIN_WALLET_SNAP_ID, + handler: HandlerType.OnKeyringRequest, + request, + })) as Json; + }; +} + +export class BitcoinWalletSnapClient { + readonly #client: KeyringClient; + + constructor() { + this.#client = new KeyringClient(new BitcoinWalletSnapSender()); + } + + async createAccount(scope: CaipChainId) { + // This will trigger the Snap account creation flow (+ account renaming) + const account = await this.#client.createAccount({ + scope, + }); + + // NOTE: The account's balance is going to be tracked automatically on when the new account + // will be added to the Snap bridge keyring (see `BalancesController:#handleOnAccountAdded`). + // However, the balance won't be fetched right away. To workaround this, we trigger the + // fetch explicitly here (since we are already in a `async` call) and wait for it to be updated! + await multichainUpdateBalance(account.id); + } +} + +export function useBitcoinWalletSnapClient() { + const client = useMemo(() => { + return new BitcoinWalletSnapClient(); + }, []); + + return client; +} From 583d400d6d88087f6ec1659589f9f4c1a3ae4650 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Wed, 9 Oct 2024 13:22:24 +0100 Subject: [PATCH 098/226] fix: Prefer token symbol to token name (#27693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** The petnames component defaults to show the name of the token instead of the symbol. Adding `preferContractSymbol` to the component overrides it to show the token symbol instead, for brevity. The PR adds the prop to petnames components inside tx simulations and the address row component. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27693?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3371 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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/confirm/info/row/address.tsx | 6 +++++- .../approve-static-simulation.tsx | 1 + .../revoke-static-simulation/revoke-static-simulation.tsx | 2 ++ .../revoke-set-approval-for-all-static-simulation.tsx | 7 ++++++- .../set-approval-for-all-static-simulation.tsx | 1 + .../permit-simulation/value-display/value-display.tsx | 6 +++++- 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ui/components/app/confirm/info/row/address.tsx b/ui/components/app/confirm/info/row/address.tsx index ec8a0c7c669d..7d28851ece92 100644 --- a/ui/components/app/confirm/info/row/address.tsx +++ b/ui/components/app/confirm/info/row/address.tsx @@ -44,7 +44,11 @@ export const ConfirmInfoRowAddress = memo( // component can support variations. See this comment for context: // // https://github.com/MetaMask/metamask-extension/pull/23487#discussion_r1525055546 isPetNamesEnabled && !isSnapUsingThis ? ( - <Name value={hexAddress} type={NameType.ETHEREUM_ADDRESS} /> + <Name + value={hexAddress} + type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol + /> ) : ( <> <Box diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx index 2a97577756b2..bdbe0e6fbae3 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx @@ -87,6 +87,7 @@ export const ApproveStaticSimulation = () => { <Name value={transactionMeta.txParams.to as string} type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol /> </Box> </Box> diff --git a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx index 199852538f17..38ff93ba9b36 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx @@ -23,6 +23,7 @@ export const RevokeStaticSimulation = () => { <Name value={transactionMeta.txParams.to as string} type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol /> </Box> </Box> @@ -36,6 +37,7 @@ export const RevokeStaticSimulation = () => { <Name value={transactionMeta.txParams.from as string} type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol /> </Box> </Box> diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx index 7cc141fb64e5..64e90a7066e6 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx @@ -29,6 +29,7 @@ export const RevokeSetApprovalForAllStaticSimulation = ({ <Name value={transactionMeta.txParams.to as string} type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol /> </Box> </Box> @@ -39,7 +40,11 @@ export const RevokeSetApprovalForAllStaticSimulation = ({ <ConfirmInfoRow label={t('permissionFrom')}> <Box style={{ marginLeft: 'auto', maxWidth: '100%' }}> <Box display={Display.Flex} alignItems={AlignItems.center}> - <Name value={spender} type={NameType.ETHEREUM_ADDRESS} /> + <Name + value={spender} + type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol + /> </Box> </Box> </ConfirmInfoRow> diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx index c50d10094486..177ef4080860 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx @@ -47,6 +47,7 @@ export const SetApprovalForAllStaticSimulation = () => { <Name value={transactionMeta.txParams.to as string} type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol /> </Box> </Box> 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 25fad3020103..633191cd2638 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 @@ -116,7 +116,11 @@ const PermitSimulationValueDisplay: React.FC< </Text> </Tooltip> </Box> - <Name value={tokenContract} type={NameType.ETHEREUM_ADDRESS} /> + <Name + value={tokenContract} + type={NameType.ETHEREUM_ADDRESS} + preferContractSymbol + /> </Box> <Box> {fiatValue && <IndividualFiatDisplay fiatAmount={fiatValue} shorten />} From 65e656c95fb71fbc401bcedb4600f9c9ec13b5bb Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:35:20 +0200 Subject: [PATCH 099/226] test: [POM] Migrate create snap account e2e tests to page object modal (#27697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the create-snap-account e2e tests to the Page Object Model (POM) pattern, improving test stability and maintainability. Changes include: - Migrate test create-snap-account.spec.ts to POM - Avoid several delays in the original function implementation - remove create single account testcase because we already test it in `test/e2e/tests/account/create-remove-account-snap.spec.ts ` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27699 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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 <chloe.gao@consensys.net> --- test/e2e/accounts/create-snap-account.spec.ts | 345 ------------------ .../pages/snap-simple-keyring-page.ts | 73 +++- ....ts => create-remove-account-snap.spec.ts} | 4 +- .../tests/account/create-snap-account.spec.ts | 140 +++++++ 4 files changed, 208 insertions(+), 354 deletions(-) delete mode 100644 test/e2e/accounts/create-snap-account.spec.ts rename test/e2e/tests/account/{remove-account-snap.spec.ts => create-remove-account-snap.spec.ts} (93%) create mode 100644 test/e2e/tests/account/create-snap-account.spec.ts diff --git a/test/e2e/accounts/create-snap-account.spec.ts b/test/e2e/accounts/create-snap-account.spec.ts deleted file mode 100644 index 2a35b4b4c805..000000000000 --- a/test/e2e/accounts/create-snap-account.spec.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { Suite } from 'mocha'; - -import FixtureBuilder from '../fixture-builder'; -import { defaultGanacheOptions, WINDOW_TITLES, withFixtures } from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { installSnapSimpleKeyring } from './common'; - -/** - * Starts the flow to create a Snap account, including unlocking the wallet, - * connecting to the test Snaps page, installing the Snap, and initiating the - * create account process on the dapp. The function ends with switching to the - * first confirmation in the extension. - * - * @param driver - The WebDriver instance used to control the browser. - * @returns A promise that resolves when the setup steps are complete. - */ -async function startCreateSnapAccountFlow(driver: Driver): Promise<void> { - await installSnapSimpleKeyring(driver, false); - - // move back to the Snap window to test the create account flow - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // check the dapp connection status - await driver.waitForSelector({ - css: '#snapConnected', - text: 'Connected', - }); - - // create new account on dapp - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - // Wait until dialog is opened before proceeding - await driver.waitAndSwitchToWindowWithTitle(3, WINDOW_TITLES.Dialog); -} - -describe('Create Snap Account', function (this: Suite) { - it('create Snap account popup contains correct Snap name and snapId', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - await driver.findElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Create', - }); - - await driver.findElement({ - css: '[data-testid="confirmation-cancel-button"]', - text: 'Cancel', - }); - - await driver.findElement({ - css: '[data-testid="create-snap-account-content-title"]', - text: 'Create account', - }); - }, - ); - }); - - it('create Snap account confirmation flow ends in approval success', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // click the create button on the confirmation modal - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // click the add account button on the naming modal - await driver.clickElement( - '[data-testid="submit-add-account-with-name"]', - ); - - // success screen should show account created with the snap suggested name - await driver.findElement({ - tag: 'h3', - text: 'Account created', - }); - await driver.findElement({ - css: '.multichain-account-list-item__account-name__button', - text: 'SSK Account', - }); - - // click the okay button - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should be created on the dapp - await driver.findElement({ - tag: 'p', - text: 'Successful request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should be created with the snap suggested name - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: 'SSK Account', - }); - }, - ); - }); - - it('creates multiple Snap accounts with increasing numeric suffixes', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); - - const expectedNames = ['SSK Account', 'SSK Account 2', 'SSK Account 3']; - - for (const [index, expectedName] of expectedNames.entries()) { - // move to the dapp window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // create new account on dapp - if (index === 0) { - // Only click the div for the first snap account creation - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - } - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - // wait until dialog is opened before proceeding - await driver.waitAndSwitchToWindowWithTitle(3, WINDOW_TITLES.Dialog); - - // click the create button on the confirmation modal - await driver.clickElement( - '[data-testid="confirmation-submit-button"]', - ); - - // click the add account button on the naming modal - await driver.clickElement( - '[data-testid="submit-add-account-with-name"]', - ); - - // click the okay button on the success screen - await driver.clickElement( - '[data-testid="confirmation-submit-button"]', - ); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // verify the account is created with the expected name - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: expectedName, - }); - } - }, - ); - }); - - it('create Snap account confirmation flow ends in approval success with custom name input', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // click the create button on the confirmation modal - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // Add a custom name to the account - const newAccountLabel = 'Custom name'; - await driver.fill('[placeholder="SSK Account"]', newAccountLabel); - // click the add account button on the naming modal - await driver.clickElement( - '[data-testid="submit-add-account-with-name"]', - ); - - // success screen should show account created with the custom name - await driver.findElement({ - tag: 'h3', - text: 'Account created', - }); - await driver.findElement({ - css: '.multichain-account-list-item__account-name__button', - text: newAccountLabel, - }); - - // click the okay button - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should be created on the dapp - await driver.findElement({ - tag: 'p', - text: 'Successful request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should be created with the custom name - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: newAccountLabel, - }); - }, - ); - }); - - it('create Snap account confirmation cancellation results in error in Snap', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // cancel account creation - await driver.clickElement('[data-testid="confirmation-cancel-button"]'); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should not be created in Snap - await driver.findElement({ - tag: 'p', - text: 'Error request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should not be created - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: 'SSK Account', - }); - }, - ); - }); - - it('cancelling naming Snap account results in account not created', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // confirm account creation - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // click the cancel button on the naming modal - await driver.clickElement( - '[data-testid="cancel-add-account-with-name"]', - ); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should not be created in Snap - await driver.findElement({ - tag: 'p', - text: 'Error request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should not be created - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: 'SSK Account', - }); - }, - ); - }); -}); 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 8b20f1bc3bc0..fd4ae9d1ecc1 100644 --- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -19,11 +19,17 @@ class SnapSimpleKeyringPage { tag: 'h3', }; + private readonly cancelAddAccountWithNameButton = + '[data-testid="cancel-add-account-with-name"]'; + private readonly confirmAddtoMetamask = { text: 'Confirm', tag: 'button', }; + private readonly confirmationCancelButton = + '[data-testid="confirmation-cancel-button"]'; + private readonly confirmationSubmitButton = '[data-testid="confirmation-submit-button"]'; @@ -54,6 +60,11 @@ class SnapSimpleKeyringPage { private readonly createSnapAccountName = '#account-name'; + private readonly errorRequestMessage = { + text: 'Error request', + tag: 'p', + }; + private readonly installationCompleteMessage = { text: 'Installation complete', tag: 'h2', @@ -95,19 +106,41 @@ class SnapSimpleKeyringPage { console.log('Snap Simple Keyring page is loaded'); } + async cancelCreateSnapOnConfirmationScreen(): Promise<void> { + console.log('Cancel create snap on confirmation screen'); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationCancelButton, + ); + } + + async cancelCreateSnapOnFillNameScreen(): Promise<void> { + console.log('Cancel create snap on fill name screen'); + await this.driver.clickElementAndWaitForWindowToClose( + this.cancelAddAccountWithNameButton, + ); + } + + async confirmCreateSnapOnConfirmationScreen(): Promise<void> { + console.log('Confirm create snap on confirmation screen'); + await this.driver.clickElement(this.confirmationSubmitButton); + } + /** * Creates a new account on the Snap Simple Keyring page and checks the account is created. + * + * @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. */ - async createNewAccount(): Promise<void> { + async createNewAccount( + accountName: string = 'SSK Account', + isFirstAccount: boolean = true, + ): Promise<void> { console.log('Create new account on Snap Simple Keyring page'); - await this.driver.clickElement(this.createAccountSection); - await this.driver.clickElement(this.createAccountButton); - - await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await this.driver.waitForSelector(this.createAccountMessage); - await this.driver.clickElement(this.confirmationSubmitButton); + await this.openCreateSnapAccountConfirmationScreen(isFirstAccount); + await this.confirmCreateSnapOnConfirmationScreen(); await this.driver.waitForSelector(this.createSnapAccountName); + await this.driver.fill(this.createSnapAccountName, accountName); await this.driver.clickElement(this.submitAddAccountWithNameButton); await this.driver.waitForSelector(this.accountCreatedMessage); @@ -146,6 +179,25 @@ class SnapSimpleKeyringPage { await this.check_simpleKeyringSnapConnected(); } + /** + * Opens the create snap account confirmation screen. + * + * @param isFirstAccount - Indicates if this is the first snap account being created. Defaults to true. + */ + async openCreateSnapAccountConfirmationScreen( + isFirstAccount: boolean = true, + ): Promise<void> { + console.log('Open create snap account confirmation screen'); + if (isFirstAccount) { + await this.driver.clickElement(this.createAccountSection); + } + await this.driver.clickElement(this.createAccountButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.createAccountMessage); + await this.driver.waitForSelector(this.confirmationCancelButton); + } + async toggleUseSyncApproval() { console.log('Toggle Use Synchronous Approval'); await this.driver.clickElement(this.useSyncApprovalToggle); @@ -158,6 +210,13 @@ class SnapSimpleKeyringPage { await this.driver.waitForSelector(this.accountSupportedMethods); } + async check_errorRequestMessageDisplayed(): Promise<void> { + console.log( + 'Check error request message is displayed on snap simple keyring page', + ); + await this.driver.waitForSelector(this.errorRequestMessage); + } + async check_simpleKeyringSnapConnected(): Promise<void> { console.log('Check simple keyring snap is connected'); await this.driver.waitForSelector(this.snapConnectedMessage); diff --git a/test/e2e/tests/account/remove-account-snap.spec.ts b/test/e2e/tests/account/create-remove-account-snap.spec.ts similarity index 93% rename from test/e2e/tests/account/remove-account-snap.spec.ts rename to test/e2e/tests/account/create-remove-account-snap.spec.ts index 2f0e2ab96a33..5d8517f66b26 100644 --- a/test/e2e/tests/account/remove-account-snap.spec.ts +++ b/test/e2e/tests/account/create-remove-account-snap.spec.ts @@ -9,8 +9,8 @@ import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring- import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; -describe('Remove Account Snap @no-mmi', function (this: Suite) { - it('disable a snap and remove it', async function () { +describe('Create and remove Snap Account @no-mmi', function (this: Suite) { + it('create snap account and remove it by removing snap', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), diff --git a/test/e2e/tests/account/create-snap-account.spec.ts b/test/e2e/tests/account/create-snap-account.spec.ts new file mode 100644 index 000000000000..387b7149c53c --- /dev/null +++ b/test/e2e/tests/account/create-snap-account.spec.ts @@ -0,0 +1,140 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { withFixtures, WINDOW_TITLES } from '../../helpers'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Create Snap Account @no-mmi', function (this: Suite) { + it('create Snap account with custom name input ends in approval success', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + const newCustomAccountLabel = 'Custom name'; + await snapSimpleKeyringPage.createNewAccount(newCustomAccountLabel); + + // Check snap account is displayed after adding the custom snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).check_accountLabel( + newCustomAccountLabel, + ); + }, + ); + }); + + it('creates multiple Snap accounts with increasing numeric suffixes', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const expectedNames = ['SSK Account', 'SSK Account 2', 'SSK Account 3']; + + // Create multiple snap accounts on snap simple keyring page + for (const expectedName of expectedNames) { + if (expectedName === 'SSK Account') { + await snapSimpleKeyringPage.createNewAccount(expectedName, true); + } else { + await snapSimpleKeyringPage.createNewAccount(expectedName, false); + } + } + + // Check 3 created snap accounts are displayed in the account list. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + for (const expectedName of expectedNames) { + await accountListPage.check_accountDisplayedInAccountList( + expectedName, + ); + } + }, + ); + }); + + it('create Snap account canceling on confirmation screen results in error on Snap', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // cancel snap account creation on confirmation screen + await snapSimpleKeyringPage.openCreateSnapAccountConfirmationScreen(); + await snapSimpleKeyringPage.cancelCreateSnapOnConfirmationScreen(); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await snapSimpleKeyringPage.check_errorRequestMessageDisplayed(); + + // Check snap account is not displayed in account list after canceling the creation + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); + + it('create Snap account canceling on fill name screen results in error on Snap', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // cancel snap account creation on fill name screen + await snapSimpleKeyringPage.openCreateSnapAccountConfirmationScreen(); + await snapSimpleKeyringPage.confirmCreateSnapOnConfirmationScreen(); + await snapSimpleKeyringPage.cancelCreateSnapOnFillNameScreen(); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await snapSimpleKeyringPage.check_errorRequestMessageDisplayed(); + + // Check snap account is not displayed in account list after canceling the creation + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); +}); From 8f2bab54f6cb8bc092348b0863bdb4239398efc0 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Wed, 9 Oct 2024 17:30:11 +0100 Subject: [PATCH 100/226] fix: Limit amount of decimals on spending cap modal (#27672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> If the user tries to add more decimals to the spending cap than what the token supports, the spending cap cannot be submitted and a notice is displayed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27672?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27618 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="472" alt="Screenshot 2024-10-08 at 13 40 02" src="https://github.com/user-attachments/assets/1a54330a-0fb2-479f-b077-dd3d9c7485a9"> ## **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-spending-cap-modal.test.tsx | 28 ++++++++++++++++++- .../edit-spending-cap-modal.tsx | 25 +++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 60ec9579059d..b7345f8d4f6c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1838,6 +1838,9 @@ "editSpendingCapDesc": { "message": "Enter the amount that you feel comfortable being spent on your behalf." }, + "editSpendingCapError": { + "message": "The spending cap can’t exceed $1 decimal digits. Remove decimal digits to continue." + }, "enable": { "message": "Enable" }, diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx index e4604fb715ab..448506f17126 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx @@ -3,7 +3,10 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { getMockApproveConfirmState } from '../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; -import { EditSpendingCapModal } from './edit-spending-cap-modal'; +import { + countDecimalDigits, + EditSpendingCapModal, +} from './edit-spending-cap-modal'; jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), @@ -78,3 +81,26 @@ describe('<EditSpendingCapModal />', () => { expect(container).toMatchSnapshot(); }); }); + +describe('countDecimalDigits()', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { numberString: '0', expectedDecimals: 0 }, + { numberString: '100', expectedDecimals: 0 }, + { numberString: '100.123', expectedDecimals: 3 }, + { numberString: '3.141592654', expectedDecimals: 9 }, + ])( + 'should return $expectedDecimals decimals for `$numberString`', + ({ + numberString, + expectedDecimals, + }: { + numberString: string; + expectedDecimals: number; + }) => { + const actual = countDecimalDigits(numberString); + + expect(actual).toEqual(expectedDecimals); + }, + ); +}); diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index e7431457f5c2..2762e99652a5 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -32,6 +32,10 @@ import { useConfirmContext } from '../../../../../context/confirm'; import { useAssetDetails } from '../../../../../hooks/useAssetDetails'; import { useApproveTokenSimulation } from '../hooks/use-approve-token-simulation'; +export function countDecimalDigits(numberString: string) { + return numberString.split('.')[1]?.length || 0; +} + export const EditSpendingCapModal = ({ isOpenEditSpendingCapModal, setIsOpenEditSpendingCapModal, @@ -116,10 +120,14 @@ export const EditSpendingCapModal = ({ setCustomSpendingCapInputValue(formattedSpendingCap.toString()); }, [customSpendingCapInputValue, formattedSpendingCap]); + const showDecimalError = + decimals && + parseInt(decimals, 10) < countDecimalDigits(customSpendingCapInputValue); + return ( <Modal isOpen={isOpenEditSpendingCapModal} - onClose={() => setIsOpenEditSpendingCapModal(false)} + onClose={handleCancel} isClosedOnEscapeKey isClosedOnOutsideClick className="edit-spending-cap-modal" @@ -154,6 +162,15 @@ export const EditSpendingCapModal = ({ style={{ width: '100%' }} inputProps={{ 'data-testid': 'custom-spending-cap-input' }} /> + {showDecimalError && ( + <Text + variant={TextVariant.bodySm} + color={TextColor.errorDefault} + paddingTop={1} + > + {t('editSpendingCapError', [decimals])} + </Text> + )} <Text variant={TextVariant.bodySm} color={TextColor.textAlternative} @@ -168,7 +185,11 @@ export const EditSpendingCapModal = ({ <ModalFooter onSubmit={handleSubmit} onCancel={handleCancel} - submitButtonProps={{ children: t('save'), loading: isModalSaving }} + submitButtonProps={{ + children: t('save'), + loading: isModalSaving, + disabled: showDecimalError, + }} /> </ModalContent> </Modal> From 420e4a668305860ee7a3e05853f64ebf546c71bf Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Wed, 9 Oct 2024 19:03:29 +0200 Subject: [PATCH 101/226] fix(multichain): fix getMultichainCurrentCurrency selector (#27726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The `getMultichainCurrentCurrency` selector was buggy. It resulted in a very weird interactions where the "current currency" was being selected based on the currently selected account. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27726?quickstart=1) ## **Related issues** Fixes: - https://github.com/MetaMask/accounts-planning/issues/612 ## **Manual testing steps** 1. `yarn start:flask` 2. Settings > Experimental > "Enable Bitcoin support" 3. Create some Bitcoin accounts 4. Change your preferred currency on: Settings > General 5. Look at the account list: - Bitcoin should always use USD or BTC unit - Other EVM acounts should always use the preferred unit (fiat or crypto) ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/0ea7e341-8d04-43c6-aea6-8ff5f004c024 ### **After** https://github.com/user-attachments/assets/9e2feafd-97ed-4b51-b712-aad21f7d99b7 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/selectors/multichain.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 2ef892db3353..1148e8d86468 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -244,10 +244,13 @@ export function getMultichainNativeCurrency( : getMultichainProviderConfig(state, account).ticker; } -export function getMultichainCurrentCurrency(state: MultichainState) { +export function getMultichainCurrentCurrency( + state: MultichainState, + account?: InternalAccount, +) { const currentCurrency = getCurrentCurrency(state); - if (getMultichainIsEvm(state)) { + if (getMultichainIsEvm(state, account)) { return currentCurrency; } @@ -256,7 +259,7 @@ export function getMultichainCurrentCurrency(state: MultichainState) { // fallback to the current ticker symbol value return currentCurrency && currentCurrency.toLowerCase() === 'usd' ? 'usd' - : getMultichainProviderConfig(state).ticker; + : getMultichainProviderConfig(state, account).ticker; } export function getMultichainCurrencyImage( From b9a24a7403a5988d0acc32e36ff53bf0195a42ce Mon Sep 17 00:00:00 2001 From: Jack Clancy <jack.clancy93@gmail.com> Date: Wed, 9 Oct 2024 18:30:25 +0100 Subject: [PATCH 102/226] =?UTF-8?q?fix:=20trying=20to=20access=20an=20unde?= =?UTF-8?q?fined=20object=20in=20swaps=20review=20quote=20compo=E2=80=A6?= =?UTF-8?q?=20(#27708)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes the two errors with trade and decimals being undefined that have been causing crashes starting around the 12.1/12.2 release. I was unable to find the root cause of this issue. Variables in the redux store seem to return as undefined, which leads me to think it might be some sort of redux race condition. The lowest common denominator of this error seems to be that `getUsedQuote` selector in the `ReviewQuote` component. I have added an additional condition to the render guard in the parent component `prepare-swaps-page` to prevent these errors [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27708?quickstart=1) ## **Related issues** [MMS-1569](https://consensyssoftware.atlassian.net/jira/software/projects/MMS/boards/447/backlog?assignee=5ae37c7e42b8a62c4e15d92a&selectedIssue=MMS-1569) ## **Manual testing steps** 1. Open Swaps Page 2. Enter swap amount 3. Edit to token and amount rapidly multiple times 4. Page should not crash ## **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. [MMS-1569]: https://consensyssoftware.atlassian.net/browse/MMS-1569?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- ui/pages/swaps/prepare-swap-page/prepare-swap-page.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 7ea900c5eb59..72050df4aca9 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 50dceb55ee2e4c6421d7eaf0e4a28fbd7b232489 Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:03:01 -0400 Subject: [PATCH 103/226] fix: remove old phishfort list from clients (#27743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR fixes an issue that prevented users from receiving the updated hotlist from ETH Phishing Detect. While the client still fetched the hotlist, the `PhishingDetector` was unable to update with the new URLs included in the hotlist. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27743?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27737 ## **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: Mark Stacey <markjstacey@gmail.com> --- 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<string, unknown>; + + 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<string, unknown>; + + 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<string, unknown>; + + 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<string, unknown> = { + 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<string, unknown>; +}; + +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<VersionedData> { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState( + state: Record<string, unknown>, +): Record<string, unknown> { + 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 93a862b5ee02..a72fd34c3c28 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'), require('./127'), require('./128'), require('./129'), From 687cf3a2d23a6dba6fb672029095a537d7902582 Mon Sep 17 00:00:00 2001 From: Howard Braham <howrad@gmail.com> Date: Thu, 10 Oct 2024 00:39:15 -0700 Subject: [PATCH 104/226] ci: followup to CircleCI Sentry reporting (#27548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Implementing some suggestions from @matthewwalsh0 from #27412 ~Also incorporates changes from @legobeat's #27268~ - Renamed `doNotForceSentryForThisTest` to `doNotForceForThisTest` because the `Sentry` is now implied by the parent property - Abstracted to `addFlagsFromPrBody()` and `addFlagsFromGitMessage()` functions - Only supports one flag right now (`tracesSampleRate`) but it's built to be easily extendable for anything - It's now an incredibly powerful general way to pass runtime flags into the Extension in CircleCI, either through the PR body or through the Git commit message - In either the PR body or the Git commit message, add a line like `flags = {"sentry": {"tracesSampleRate": x.xx}}` If you do both, Git commit message takes precedence - This changes the format from `[flags.sentry.tracesSampleRate: x.xx]` to `flags = {"sentry": {"tracesSampleRate": x.xx}}` Note: This PR, as is, will hit the following error because it's trying to actually parse the sample code above with `x.xx`. The good news is it fails gracefully. ``` Error parsing flags from PR body, ignoring flags SyntaxError: Unexpected token 'x', ..."pleRate": x.xx}}" is not valid JSON ``` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27548?quickstart=1) ## **Related issues** Followup to: #27412 ## **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/scripts/git-diff-develop.ts | 19 +++-- app/scripts/lib/manifestFlags.ts | 2 +- app/scripts/lib/setupSentry.js | 6 +- test/e2e/set-manifest-flags.ts | 95 ++++++++++++++++++++----- test/e2e/tests/metrics/errors.spec.js | 28 ++++---- test/e2e/tests/metrics/sessions.spec.ts | 4 +- test/e2e/tests/metrics/traces.spec.ts | 8 +-- 7 files changed, 117 insertions(+), 45 deletions(-) diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts index 9f6c8f0ae4df..43435db17418 100644 --- a/.circleci/scripts/git-diff-develop.ts +++ b/.circleci/scripts/git-diff-develop.ts @@ -104,12 +104,18 @@ async function gitDiff(): Promise<string> { return diffResult; } +function writePrBodyToFile(prBody: string) { + const prBodyPath = path.resolve(CHANGED_FILES_DIR, 'pr-body.txt'); + fs.writeFileSync(prBodyPath, prBody.trim()); + console.log(`PR body saved to ${prBodyPath}`); +} + /** - * Stores the output of git diff to a file. + * Main run function, stores the output of git diff and the body of the matching PR to a file. * - * @returns Returns a promise that resolves when the git diff output is successfully stored. + * @returns Returns a promise that resolves when the git diff output and PR body is successfully stored. */ -async function storeGitDiffOutput() { +async function storeGitDiffOutputAndPrBody() { try { // Create the directory // This is done first because our CirleCI config requires that this directory is present, @@ -132,6 +138,7 @@ async function storeGitDiffOutput() { return; } else if (baseRef !== MAIN_BRANCH) { console.log(`This is for a PR targeting '${baseRef}', skipping git diff`); + writePrBodyToFile(prInfo.body); return; } @@ -142,8 +149,10 @@ async function storeGitDiffOutput() { // Store the output of git diff const outputPath = path.resolve(CHANGED_FILES_DIR, 'changed-files.txt'); fs.writeFileSync(outputPath, diffOutput.trim()); - console.log(`Git diff results saved to ${outputPath}`); + + writePrBodyToFile(prInfo.body); + process.exit(0); } catch (error: any) { console.error('An error occurred:', error.message); @@ -151,4 +160,4 @@ async function storeGitDiffOutput() { } } -storeGitDiffOutput(); +storeGitDiffOutputAndPrBody(); diff --git a/app/scripts/lib/manifestFlags.ts b/app/scripts/lib/manifestFlags.ts index a013373ac9f2..93925bf63a0c 100644 --- a/app/scripts/lib/manifestFlags.ts +++ b/app/scripts/lib/manifestFlags.ts @@ -11,7 +11,7 @@ export type ManifestFlags = { }; sentry?: { tracesSampleRate?: number; - doNotForceSentryForThisTest?: boolean; + forceEnable?: boolean; }; }; diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index e6f4a0d4524e..d440578144cc 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -123,7 +123,7 @@ function getTracesSampleRate(sentryTarget) { if (flags.circleci) { // Report very frequently on develop branch, and never on other branches - // (Unless you do a [flags.sentry.tracesSampleRate: x.xx] override) + // (Unless you use a `flags = {"sentry": {"tracesSampleRate": x.xx}}` override) if (flags.circleci.branch === 'develop') { return 0.03; } @@ -238,7 +238,7 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - getManifestFlags().sentry?.doNotForceSentryForThisTest || + !getManifestFlags().sentry?.forceEnable || (process.env.IN_TEST && !SENTRY_DSN_DEV) ) { return SENTRY_DSN_FAKE; @@ -272,7 +272,7 @@ async function getMetaMetricsEnabled() { if ( METAMASK_BUILD_TYPE === 'mmi' || - (flags.circleci && !flags.sentry?.doNotForceSentryForThisTest) + (flags.circleci && flags.sentry.forceEnable) ) { return true; } diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index e8d02a12e2cd..290e8b863a9e 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -1,5 +1,6 @@ import { execSync } from 'child_process'; import fs from 'fs'; +import { merge } from 'lodash'; import { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; export const folder = `dist/${process.env.SELENIUM_BROWSER}`; @@ -8,23 +9,82 @@ function parseIntOrUndefined(value: string | undefined): number | undefined { return value ? parseInt(value, 10) : undefined; } -// Grab the tracesSampleRate from the git message if it's set -function getTracesSampleRateFromGitMessage(): number | undefined { +/** + * Search a string for `flags = {...}` and return ManifestFlags if it exists + * + * @param str - The string to search + * @param errorType - The type of error to log if parsing fails + * @returns The ManifestFlags object if valid, otherwise undefined + */ +function regexSearchForFlags( + str: string, + errorType: string, +): ManifestFlags | undefined { + // Search str for `flags = {...}` + const flagsMatch = str.match(/flags\s*=\s*(\{.*\})/u); + + if (flagsMatch) { + try { + // Get 1st capturing group from regex + return JSON.parse(flagsMatch[1]); + } catch (error) { + console.error( + `Error parsing flags from ${errorType}, ignoring flags\n`, + error, + ); + } + } + + return undefined; +} + +/** + * Add flags from the GitHub PR body if they are set + * + * To use this feature, add a line to your PR body like: + * `flags = {"sentry": {"tracesSampleRate": 0.1}}` + * (must be valid JSON) + * + * @param flags - The flags object to add to + */ +function addFlagsFromPrBody(flags: ManifestFlags) { + let body; + + try { + body = fs.readFileSync('changed-files/pr-body.txt', 'utf8'); + } catch (error) { + console.debug('No pr-body.txt, ignoring flags'); + return; + } + + const newFlags = regexSearchForFlags(body, 'PR body'); + + if (newFlags) { + // Use lodash merge to do a deep merge (spread operator is shallow) + merge(flags, newFlags); + } +} + +/** + * Add flags from the Git message if they are set + * + * To use this feature, add a line to your commit message like: + * `flags = {"sentry": {"tracesSampleRate": 0.1}}` + * (must be valid JSON) + * + * @param flags - The flags object to add to + */ +function addFlagsFromGitMessage(flags: ManifestFlags) { const gitMessage = execSync( `git show --format='%B' --no-patch "HEAD"`, ).toString(); - // Search gitMessage for `[flags.sentry.tracesSampleRate: 0.000 to 1.000]` - const tracesSampleRateMatch = gitMessage.match( - /\[flags\.sentry\.tracesSampleRate: (0*(\.\d+)?|1(\.0*)?)\]/u, - ); + const newFlags = regexSearchForFlags(gitMessage, 'git message'); - if (tracesSampleRateMatch) { - // Return 1st capturing group from regex - return parseFloat(tracesSampleRateMatch[1]); + if (newFlags) { + // Use lodash merge to do a deep merge (spread operator is shallow) + merge(flags, newFlags); } - - return undefined; } // Alter the manifest with CircleCI environment variables and custom flags @@ -41,12 +101,15 @@ export function setManifestFlags(flags: ManifestFlags = {}) { ), }; - const tracesSampleRate = getTracesSampleRateFromGitMessage(); + addFlagsFromPrBody(flags); + addFlagsFromGitMessage(flags); - // 0 is a valid value, so must explicitly check for undefined - if (tracesSampleRate !== undefined) { - // Add tracesSampleRate to flags.sentry (which may or may not already exist) - flags.sentry = { ...flags.sentry, tracesSampleRate }; + // Set `flags.sentry.forceEnable` to true by default + if (flags.sentry === undefined) { + flags.sentry = {}; + } + if (flags.sentry.forceEnable === undefined) { + flags.sentry.forceEnable = true; } } diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index fdeb4437d428..dfe77f758fcb 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -247,7 +247,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -278,7 +278,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -319,7 +319,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -365,7 +365,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -426,7 +426,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryInvariantMigrationError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -475,7 +475,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -521,7 +521,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -585,7 +585,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -621,7 +621,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -656,7 +656,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -702,7 +702,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -766,7 +766,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -810,7 +810,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -898,7 +898,7 @@ describe('Sentry errors', function () { ganacheOptions, title: this.test.fullTitle(), manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver }) => { diff --git a/test/e2e/tests/metrics/sessions.spec.ts b/test/e2e/tests/metrics/sessions.spec.ts index f1bdee4538fb..7c79e5510116 100644 --- a/test/e2e/tests/metrics/sessions.spec.ts +++ b/test/e2e/tests/metrics/sessions.spec.ts @@ -38,7 +38,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -60,7 +60,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { diff --git a/test/e2e/tests/metrics/traces.spec.ts b/test/e2e/tests/metrics/traces.spec.ts index 194f36ff73b0..9166281f90e5 100644 --- a/test/e2e/tests/metrics/traces.spec.ts +++ b/test/e2e/tests/metrics/traces.spec.ts @@ -51,7 +51,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -73,7 +73,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -95,7 +95,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -117,7 +117,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { From 97758a6a10edc7e2f19b16b6496818bf9d35cd68 Mon Sep 17 00:00:00 2001 From: sahar-fehri <sahar.fehri@consensys.net> Date: Thu, 10 Oct 2024 12:14:44 +0200 Subject: [PATCH 105/226] feat: upgrade assets-controllers to v38.2.0 (#27629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrade assets-controllers to v38.2.0 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27629?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 <metamaskbot@users.noreply.github.com> --- ...ts-controllers-npm-38.2.0-40af2afaa7.patch | 35 ++++++++ lavamoat/browserify/beta/policy.json | 12 +-- lavamoat/browserify/flask/policy.json | 12 +-- lavamoat/browserify/main/policy.json | 12 +-- lavamoat/browserify/mmi/policy.json | 12 +-- package.json | 2 +- yarn.lock | 88 +++++++++++++------ 7 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 .yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch diff --git a/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch b/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch new file mode 100644 index 000000000000..7a5837cd4818 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch @@ -0,0 +1,35 @@ +diff --git a/dist/assetsUtil.cjs b/dist/assetsUtil.cjs +index e90a1b6767bc8ac54b7a4d580035cf5db6861dca..a5e0f03d2541b4e3540431ef2e6e4b60fb7ae9fe 100644 +--- a/dist/assetsUtil.cjs ++++ b/dist/assetsUtil.cjs +@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); ++function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } + exports.fetchTokenContractExchangeRates = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.hasNewCollectionFields = exports.compareNftMetadata = exports.TOKEN_PRICES_BATCH_SIZE = void 0; + const controller_utils_1 = require("@metamask/controller-utils"); + const utils_1 = require("@metamask/utils"); +@@ -221,7 +222,7 @@ async function getIpfsCIDv1AndPath(ipfsUrl) { + const index = url.indexOf('/'); + const cid = index !== -1 ? url.substring(0, index) : url; + const path = index !== -1 ? url.substring(index) : undefined; +- const { CID } = await import("multiformats"); ++ const { CID } = _interopRequireWildcard(require("multiformats")); + // We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) + // because most cid v0s appear to be incompatible with IPFS subdomains + return { +diff --git a/dist/token-prices-service/codefi-v2.mjs b/dist/token-prices-service/codefi-v2.mjs +index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..b89849c0caf7e5db3b53cf03dd5746b6b1433543 100644 +--- a/dist/token-prices-service/codefi-v2.mjs ++++ b/dist/token-prices-service/codefi-v2.mjs +@@ -12,8 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function ( + var _CodefiTokenPricesServiceV2_tokenPricePolicy; + import { handleFetch } from "@metamask/controller-utils"; + import { hexToNumber } from "@metamask/utils"; +-import $cockatiel from "cockatiel"; +-const { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } = $cockatiel; ++import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel"; + /** + * The list of currencies that can be supplied as the `vsCurrency` parameter to + * the `/spot-prices` endpoint, in lowercase form. diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index e98080fc4d5f..ec02c2756185 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -698,8 +698,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -715,14 +715,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -730,7 +722,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index e98080fc4d5f..ec02c2756185 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -698,8 +698,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -715,14 +715,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -730,7 +722,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index e98080fc4d5f..ec02c2756185 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -698,8 +698,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -715,14 +715,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -730,7 +722,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 43297351bf21..7eaa06a954b0 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -790,8 +790,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -807,14 +807,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -822,7 +814,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } diff --git a/package.json b/package.json index 4d88e907dd2a..416b3e1b0420 100644 --- a/package.json +++ b/package.json @@ -302,7 +302,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "^37.0.0", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.6.1", "@metamask/browser-passworder": "^4.3.0", diff --git a/yarn.lock b/yarn.lock index e8cade3e2727..1e00e14c6cf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4861,9 +4861,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^37.0.0": - version: 37.0.0 - resolution: "@metamask/assets-controllers@npm:37.0.0" +"@metamask/assets-controllers@npm:38.2.0": + version: 38.2.0 + resolution: "@metamask/assets-controllers@npm:38.2.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4871,12 +4871,12 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/base-controller": "npm:^6.0.2" + "@metamask/base-controller": "npm:^7.0.1" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.0.2" + "@metamask/controller-utils": "npm:^11.3.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/polling-controller": "npm:^9.0.1" + "@metamask/polling-controller": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/utils": "npm:^9.1.0" "@types/bn.js": "npm:^5.1.5" @@ -4893,9 +4893,47 @@ __metadata: "@metamask/accounts-controller": ^18.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 - "@metamask/network-controller": ^20.0.0 + "@metamask/network-controller": ^21.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/89798930cb80a134263ce82db736feebd064fe6c999ddcf41ca86fad81cfadbb9e37d1919a6384aaf6d3aa0cb520684e7b8228da3b9bc1e70e7aea174a69c4ac + checksum: 10/96ae724a002289e4df97bab568e0bba4d28ef18320298b12d828fc3b58c58ebc54b9f9d659c5e6402aad82088b699e52469d897dd4356e827e35b8f8cebb4483 + languageName: node + linkType: hard + +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch": + version: 38.2.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch::version=38.2.0&hash=e14ff8" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/polling-controller": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/utils": "npm:^9.1.0" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bn.js: "npm:^5.2.1" + cockatiel: "npm:^3.1.2" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^13.1.0" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/accounts-controller": ^18.0.0 + "@metamask/approval-controller": ^7.0.0 + "@metamask/keyring-controller": ^17.0.0 + "@metamask/network-controller": ^21.0.0 + "@metamask/preferences-controller": ^13.0.0 + checksum: 10/0ba3673bf9c87988d6c569a14512b8c9bb97db3516debfedf24cbcf38110e99afec8d9fc50cb0b627bfbc1d1a62069298e4e27278587197f67812cb38ee2c778 languageName: node linkType: hard @@ -5958,6 +5996,22 @@ __metadata: languageName: node linkType: hard +"@metamask/polling-controller@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/polling-controller@npm:10.0.1" + dependencies: + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/utils": "npm:^9.1.0" + "@types/uuid": "npm:^8.3.0" + fast-json-stable-stringify: "npm:^2.1.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/network-controller": ^21.0.0 + checksum: 10/25c11e65eeccb08a2b4b7dec21ccabb4b797907edb03a1534ebacb87d0754a3ade52aad061aad8b3ac23bfc39917c0d61b9734e32bc748c210b2997410ae45a9 + languageName: node + linkType: hard + "@metamask/polling-controller@npm:^8.0.0": version: 8.0.0 resolution: "@metamask/polling-controller@npm:8.0.0" @@ -5975,22 +6029,6 @@ __metadata: languageName: node linkType: hard -"@metamask/polling-controller@npm:^9.0.1": - version: 9.0.1 - resolution: "@metamask/polling-controller@npm:9.0.1" - dependencies: - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/controller-utils": "npm:^11.0.2" - "@metamask/utils": "npm:^9.1.0" - "@types/uuid": "npm:^8.3.0" - fast-json-stable-stringify: "npm:^2.1.0" - uuid: "npm:^8.3.2" - peerDependencies: - "@metamask/network-controller": ^20.0.0 - checksum: 10/e9e8c51013290a2e4b2817ba1e0915783474f6a55fe614e20acf92bf707e300bec1fa612c8019ae9afe9635d018fb5d5b106c8027446ba12767220db91cf1ee5 - languageName: node - linkType: hard - "@metamask/post-message-stream@npm:^8.0.0, @metamask/post-message-stream@npm:^8.1.1": version: 8.1.1 resolution: "@metamask/post-message-stream@npm:8.1.1" @@ -26059,7 +26097,7 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "npm:^37.0.0" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" From 11b9bd4caa84c795ec940d0984741b5ec18757d1 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari <nidhi.kumari@consensys.net> Date: Thu, 10 Oct 2024 12:09:30 +0100 Subject: [PATCH 106/226] feat: Release Chain Permissions (#27561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to remove feature flags and add e2e for Chain Permissions ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/2713](https://github.com/MetaMask/MetaMask-planning/issues/2713) ## **Manual testing steps** 1. Run yarn start 2. Everything should work ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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: Jiexi Luan <jiexiluan@gmail.com> Co-authored-by: Alex <adonesky@gmail.com> Co-authored-by: David Walsh <davidwalsh83@gmail.com> Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- app/_locales/de/messages.json | 15 -- app/_locales/el/messages.json | 15 -- app/_locales/en/messages.json | 15 -- app/_locales/es/messages.json | 15 -- app/_locales/fr/messages.json | 15 -- app/_locales/hi/messages.json | 15 -- app/_locales/id/messages.json | 15 -- app/_locales/ja/messages.json | 15 -- app/_locales/ko/messages.json | 15 -- app/_locales/pt/messages.json | 15 -- app/_locales/ru/messages.json | 15 -- app/_locales/tl/messages.json | 15 -- app/_locales/tr/messages.json | 15 -- app/_locales/vi/messages.json | 15 -- app/_locales/zh_CN/messages.json | 15 -- .../handlers/add-ethereum-chain.js | 37 +-- .../handlers/add-ethereum-chain.test.js | 37 +-- .../handlers/ethereum-chain-utils.js | 41 ++- .../handlers/switch-ethereum-chain.js | 35 +-- .../handlers/switch-ethereum-chain.test.js | 20 -- app/scripts/metamask-controller.js | 16 +- test/e2e/accounts/common.ts | 15 +- .../api-specs/ConfirmationRejectionRule.ts | 35 ++- test/e2e/helpers.js | 13 +- test/e2e/json-rpc/switchEthereumChain.spec.js | 145 ++++++++--- .../wallet_requestPermissions.spec.js | 7 +- .../e2e/snaps/test-snap-txinsights-v2.spec.js | 31 +-- test/e2e/snaps/test-snap-txinsights.spec.js | 30 +-- .../connections/connect-with-metamask.spec.js | 79 ++++++ .../connections/edit-account-flow.spec.js | 101 ++++++++ .../connections/edit-networks-flow.spec.js | 85 +++++++ .../review-permissions-page.spec.js | 145 +++++++++++ .../review-switch-permission-page.spec.js | 154 ++++++++++++ .../dapp-interactions.spec.js | 3 +- .../dapp-interactions/permissions.spec.js | 6 +- test/e2e/tests/metrics/dapp-viewed.spec.js | 87 +------ .../tests/multichain/connection-page.spec.js | 219 ---------------- .../tests/network/add-custom-network.spec.js | 7 - .../tests/network/chain-interactions.spec.js | 49 ---- .../tests/network/deprecated-networks.spec.js | 21 -- .../network/switch-custom-network.spec.js | 55 +--- .../batch-txs-per-dapp-diff-network.spec.js | 61 ++--- .../batch-txs-per-dapp-extra-tx.spec.js | 144 +++++------ .../batch-txs-per-dapp-same-network.spec.js | 62 +++-- .../request-queuing/chainid-check.spec.js | 48 ++-- .../dapp1-send-dapp2-signTypedData.spec.js | 60 ++--- .../dapp1-subscribe-network-switch.spec.js | 12 +- ...-switch-dapp2-eth-request-accounts.spec.js | 9 +- .../dapp1-switch-dapp2-send.spec.js | 50 +--- ...multi-dapp-sendTx-revokePermission.spec.js | 65 +++-- .../multiple-networks-dapps-txs.spec.js | 56 ++--- .../switchChain-sendTx.spec.js | 44 ++-- .../switchChain-watchAsset.spec.js | 36 ++- test/e2e/tests/request-queuing/ui.spec.js | 104 ++++---- .../watchAsset-switchChain-watchAsset.spec.js | 2 - .../permission-cell/permission-cell-status.js | 2 +- ...ission-page-container-content.component.js | 32 +-- .../permissions-connect-permission-list.js | 7 +- .../app-header-unlocked-content.tsx | 11 +- .../disconnect-all-modal.tsx | 13 +- .../network-list-menu/network-list-menu.tsx | 5 +- .../permissions-page.test.js.snap | 105 ++------ .../permissions-page/connection-list-item.js | 71 ++---- .../connection-list-item.test.js | 36 +-- .../permissions-page/permissions-page.js | 12 +- .../review-permissions-page.tsx | 1 + .../permissions-connect.test.tsx.snap | 236 ------------------ .../permissions-connect.component.js | 25 +- .../permissions-connect.test.tsx | 180 ------------- ui/pages/routes/routes.component.js | 2 +- 70 files changed, 1200 insertions(+), 1989 deletions(-) create mode 100644 test/e2e/tests/connections/connect-with-metamask.spec.js create mode 100644 test/e2e/tests/connections/edit-account-flow.spec.js create mode 100644 test/e2e/tests/connections/edit-networks-flow.spec.js create mode 100644 test/e2e/tests/connections/review-permissions-page.spec.js create mode 100644 test/e2e/tests/connections/review-switch-permission-page.spec.js delete mode 100644 test/e2e/tests/multichain/connection-page.spec.js delete mode 100644 ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap delete mode 100644 ui/pages/permissions-connect/permissions-connect.test.tsx diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index bda0d4d894e7..8c91aec52887 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask ist mit dieser Seite verbunden, aber es sind noch keine Konten verbunden" }, - "connectedWith": { - "message": "Verbunden mit" - }, "connecting": { "message": "Verbinden" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Wenn Sie die Verbindung zwischen $1 und $2 unterbrechen, müssen Sie die Verbindung wiederherstellen, um sie erneut zu verwenden.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Alle $1 trennen", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 trennen" }, @@ -2835,10 +2824,6 @@ "message": "$1 bittet um Ihre Zustimmung zu:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Möchten Sie, dass diese Website Folgendes tut?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Das native Token dieses Netzwerks ist $1. Dieses Token wird für die Gas-Gebühr verwendet. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 6010f1939602..4f29362124bd 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "Το MetaMask είναι συνδεδεμένο σε αυτόν τον ιστότοπο, αλλά δεν έχουν συνδεθεί ακόμα λογαριασμοί" }, - "connectedWith": { - "message": "Συνδέεται με" - }, "connecting": { "message": "Σύνδεση" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Αν αποσυνδέσετε τo $1 από τo $2, θα πρέπει να επανασυνδεθείτε για να τα χρησιμοποιήσετε ξανά.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Αποσύνδεση όλων των $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Αποσύνδεση $1" }, @@ -2835,10 +2824,6 @@ "message": "Το $1 ζητάει την έγκρισή σας για:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Θέλετε αυτός ο ιστότοπος να κάνει τα εξής;", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Το αρχικό token σε αυτό το δίκτυο είναι το $1. Είναι το token που χρησιμοποιείται για τα τέλη συναλλαγών.", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b7345f8d4f6c..ecaedb3201d0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1180,9 +1180,6 @@ "connectedSnaps": { "message": "Connected Snaps" }, - "connectedWith": { - "message": "Connected with" - }, "connectedWithAccount": { "message": "$1 accounts connected", "description": "$1 represents account length" @@ -1647,14 +1644,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "If you disconnect your $1 from $2, you'll need to reconnect to use them again.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Disconnect all $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectMessage": { "message": "This will disconnect you from $1", "description": "$1 is the name of the dapp" @@ -3053,10 +3042,6 @@ "message": "$1 is asking for your approval to:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Do you want this site to do the following?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "The native token on this network is $1. It is the token used for gas fees. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 772471fdfd65..49c523b184f6 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1085,9 +1085,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask está conectado a este sitio, pero aún no hay cuentas conectadas" }, - "connectedWith": { - "message": "Conectado con" - }, "connecting": { "message": "Conectando" }, @@ -1504,14 +1501,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Si desconecta su $1 de su $2, tendrá que volver a conectarlos para usarlos nuevamente.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Desconectar todos/as $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Desconectar $1" }, @@ -2832,10 +2821,6 @@ "message": "$1 solicita su aprobación para:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "¿Desea que este sitio haga lo siguiente?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "El token nativo en esta red es de $1. Es el token utilizado para las tarifas de gas. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 4a537a554315..0c5015f67665 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask est connecté à ce site, mais aucun compte n’est encore connecté" }, - "connectedWith": { - "message": "Connecté avec" - }, "connecting": { "message": "Connexion…" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Si vous déconnectez vos $1 de $2, vous devrez vous reconnecter pour les utiliser à nouveau.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Déconnecter tous les $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Déconnecter $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 vous demande votre approbation pour :", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Voulez-vous que ce site fasse ce qui suit ?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Le jeton natif de ce réseau est $1. C’est le jeton utilisé pour les frais de gaz. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 7fb1a04cb137..274aae47e2e3 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask इस साइट से कनेक्टेड है, लेकिन अभी तक कोई अकाउंट कनेक्ट नहीं किया गया है" }, - "connectedWith": { - "message": "से कनेक्ट किया गया" - }, "connecting": { "message": "कनेक्ट किया जा रहा है" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "अगर आप अपने $1 को $2 से डिस्कनेक्ट करते हैं, तो आपको उन्हें दोबारा इस्तेमाल करने के लिए रिकनेक्ट करना होगा।", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "सभी $1 को डिस्कनेक्ट करें", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 डिस्कनेक्ट करें" }, @@ -2835,10 +2824,6 @@ "message": "$1 निम्नलिखित के लिए आपका एप्रूवल मांग रहा है:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "क्या आप चाहते हैं कि यह साइट निम्नलिखित कार्य करे?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "इस नेटवर्क पर ओरिजिनल टोकन $1 है। यह गैस फ़ीस के लिए इस्तेमाल किया जाने वाला टोकन है।", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index be3ef95ad448..5f36af7a382d 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask terhubung ke situs ini, tetapi belum ada akun yang terhubung" }, - "connectedWith": { - "message": "Terhubung dengan" - }, "connecting": { "message": "Menghubungkan" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "Jika Anda memutus koneksi $1 dari $2, Anda harus menghubungkannya kembali agar dapat menggunakannya lagi.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Putuskan semua koneksi $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Putuskan koneksi $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 meminta persetujuan Anda untuk:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Ingin situs ini melakukan hal berikut?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Token asli di jaringan ini adalah $1. Ini merupakan token yang digunakan untuk biaya gas. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 1ffbc9f1e4eb..c8adf1ff5af9 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMaskはこのサイトに接続されていますが、まだアカウントは接続されていません" }, - "connectedWith": { - "message": "接続先" - }, "connecting": { "message": "接続中..." }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "$1と$2の接続を解除した場合、再び使用するには再度接続する必要があります。", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "すべての$1の接続を解除", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1を接続解除" }, @@ -2835,10 +2824,6 @@ "message": "$1が次の承認を求めています:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "このサイトに次のことを希望しますか?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "このネットワークのネイティブトークンは$1です。ガス代にもこのトークンが使用されます。", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index a1c79024f651..5868672bce32 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask는 이 사이트와 연결되어 있지만, 아직 연결된 계정이 없습니다" }, - "connectedWith": { - "message": "연결 대상:" - }, "connecting": { "message": "연결 중" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "$2에서 $1의 연결을 끊은 경우, 다시 사용하려면 다시 연결해야 합니다.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "모든 $1 연결 해제", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 연결 해제" }, @@ -2835,10 +2824,6 @@ "message": "$1에서 다음 승인을 요청합니다:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "이 사이트가 다음을 수행하기 원하십니까?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "이 네트워크의 네이티브 토큰은 $1입니다. 이는 가스비 지불에 사용하는 토큰입니다. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 52eb392f9d94..298f4b8b8d70 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "A MetaMask está conectada a este site, mas nenhuma conta está conectada ainda" }, - "connectedWith": { - "message": "Conectado com" - }, "connecting": { "message": "Conectando" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Se desconectar $1 de $2, você precisará reconectar para usar novamente.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Desconectar $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Desconectar $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 solicita sua aprovação para:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Deseja que este site faça o seguinte?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "O token nativo dessa rede é $1. Esse é o token usado para taxas de gás.", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 9f4f15461bab..999f237f73ea 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask подключен к этому сайту, но счета пока не подключены" }, - "connectedWith": { - "message": "Подключен(-а) к" - }, "connecting": { "message": "Подключение..." }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Если вы отключите свои $1 от $2, вам придется повторно подключиться, чтобы использовать их снова.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Отключить все $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Отключить $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 запрашивает ваше одобрение на:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Вы хотите, чтобы этот сайт делал следующее?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Нативный токен этой сети — $1. Этот токен используется для внесения платы за газ. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index c2ffc42763d0..df021e9dfdad 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "Konektado ang MetaMask sa site na ito, ngunit wala pang mga account ang konektado" }, - "connectedWith": { - "message": "Nakakonekta sa" - }, "connecting": { "message": "Kumokonekta" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Mga Snap" }, - "disconnectAllText": { - "message": "Kapag idiniskonekta mo ang iyong $1 mula sa $2, kailangan mong muling ikonekta para gamitin muli.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Idiskonekta ang lahat ng $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Idiskonekta $1" }, @@ -2835,10 +2824,6 @@ "message": "Ang $1 ay humihiling ng iyong pag-apruba para:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Gusto mo bang gawin ng site na ito ang mga sumusunod?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Ang native token sa network na ito ay $1. Ito ang token na ginagamit para sa mga gas fee. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 676896deaaae..ce36a61ca716 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask bu siteye bağlı ancak henüz bağlı hesap yok" }, - "connectedWith": { - "message": "Şununla bağlanıldı:" - }, "connecting": { "message": "Bağlanıyor" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap'ler" }, - "disconnectAllText": { - "message": "$1 ile $2 bağlantısını keserseniz onları tekrar kullanmak için tekrar bağlamanız gerekir.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Tüm $1 bağlantısını kes", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 bağlantısını kes" }, @@ -2835,10 +2824,6 @@ "message": "$1 sizden şunun için onay istiyor:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Bu sitenin aşağıdakileri yapmasına izin vermek istiyor musunuz?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Bu ağdaki yerli token $1. Bu, gaz ücretleri için kullanılan tokendir. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 442478665c00..5766a1789d24 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask được kết nối với trang web này, nhưng chưa có tài khoản nào được kết nối" }, - "connectedWith": { - "message": "Đã kết nối với" - }, "connecting": { "message": "Đang kết nối" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "Nếu bạn ngắt kết nối $1 khỏi $2, bạn sẽ cần kết nối lại để sử dụng lại.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Ngắt kết nối tất cả $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Ngắt kết nối $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 đang yêu cầu sự chấp thuận của bạn cho:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Bạn có muốn trang web này thực hiện những điều sau không?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Token gốc của mạng này là $1. Token này được dùng làm phí gas.", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 9f33ef4a6b35..a5e2b1175862 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask 已连接到此网站,但尚未连接任何账户" }, - "connectedWith": { - "message": "已连接" - }, "connecting": { "message": "连接中……" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "如果您将 $1 与 $2 断开连接,则需要重新连接才能再次使用。", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "断开连接所有 $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "断开连接 $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 请求您的批准,以便:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "您希望此网站执行以下操作吗?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "此网络上的原生代币为 $1。它是用于燃料费的代币。 ", "description": "$1 represents the name of the native token on the current network" 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 e224cb4a2b38..2f4727fdab36 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 @@ -23,7 +23,6 @@ const addEthereumChain = { getCurrentChainIdForDomain: true, getCaveat: true, requestPermittedChainsPermission: true, - getChainPermissionsFeatureFlag: true, grantPermittedChainsPermissionIncremental: true, }, }; @@ -46,7 +45,6 @@ async function addEthereumChainHandler( getCurrentChainIdForDomain, getCaveat, requestPermittedChainsPermission, - getChainPermissionsFeatureFlag, grantPermittedChainsPermissionIncremental, }, ) { @@ -67,9 +65,6 @@ async function addEthereumChainHandler( const { origin } = req; const currentChainIdForDomain = getCurrentChainIdForDomain(origin); - const currentNetworkConfiguration = getNetworkConfigurationByChainId( - currentChainIdForDomain, - ); const existingNetwork = getNetworkConfigurationByChainId(chainId); if ( @@ -198,30 +193,14 @@ async function addEthereumChainHandler( const { networkClientId } = updatedNetwork.rpcEndpoints[updatedNetwork.defaultRpcEndpointIndex]; - const requestData = { - toNetworkConfiguration: updatedNetwork, - fromNetworkConfiguration: currentNetworkConfiguration, - }; - - return switchChain( - res, - end, - origin, - chainId, - requestData, - networkClientId, - approvalFlowId, - { - isAddFlow: true, - getChainPermissionsFeatureFlag, - setActiveNetwork, - requestUserApproval, - getCaveat, - requestPermittedChainsPermission, - endApprovalFlow, - grantPermittedChainsPermissionIncremental, - }, - ); + return switchChain(res, end, chainId, networkClientId, approvalFlowId, { + isAddFlow: true, + setActiveNetwork, + endApprovalFlow, + getCaveat, + requestPermittedChainsPermission, + grantPermittedChainsPermissionIncremental, + }); } else if (approvalFlowId) { endApprovalFlow({ id: approvalFlowId }); } 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 f6be2deb6f08..945953cff562 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 @@ -54,14 +54,8 @@ const createMockNonInfuraConfiguration = () => ({ describe('addEthereumChainHandler', () => { const addEthereumChainHandler = addEthereumChain.implementation; - - const makeMocks = ({ - permissionedChainIds = [], - permissionsFeatureFlagIsActive, - overrides = {}, - } = {}) => { + const makeMocks = ({ permissionedChainIds = [], overrides = {} } = {}) => { return { - getChainPermissionsFeatureFlag: () => permissionsFeatureFlagIsActive, getCurrentChainIdForDomain: jest .fn() .mockReturnValue(NON_INFURA_CHAIN_ID), @@ -92,9 +86,7 @@ describe('addEthereumChainHandler', () => { describe('with `endowment:permitted-chains` permissioning inactive', () => { it('creates a new network configuration for the given chainid and switches to it if none exists', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); await addEthereumChainHandler( { origin: 'example.com', @@ -118,8 +110,7 @@ describe('addEthereumChainHandler', () => { mocks, ); - // called twice, once for the add and once for the switch - expect(mocks.requestUserApproval).toHaveBeenCalledTimes(2); + expect(mocks.requestUserApproval).toHaveBeenCalledTimes(1); expect(mocks.addNetwork).toHaveBeenCalledTimes(1); expect(mocks.addNetwork).toHaveBeenCalledWith({ blockExplorerUrls: ['https://optimistic.etherscan.io'], @@ -141,9 +132,7 @@ describe('addEthereumChainHandler', () => { }); it('creates a new networkConfiguration when called without "blockExplorerUrls" property', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); await addEthereumChainHandler( { origin: 'example.com', @@ -172,7 +161,6 @@ describe('addEthereumChainHandler', () => { describe('if a networkConfiguration for the given chainId already exists', () => { it('updates the existing networkConfiguration with the new rpc url if it doesnt already exist', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -258,7 +246,6 @@ describe('addEthereumChainHandler', () => { }; const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -305,7 +292,6 @@ describe('addEthereumChainHandler', () => { const existingNetwork = createMockMainnetConfiguration(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { // Start on sepolia getCurrentChainIdForDomain: jest @@ -349,9 +335,7 @@ describe('addEthereumChainHandler', () => { }); it('should return error for invalid chainId', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); const mockEnd = jest.fn(); await addEthereumChainHandler( @@ -380,7 +364,6 @@ describe('addEthereumChainHandler', () => { const mocks = makeMocks({ permissionedChainIds: [], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -427,7 +410,6 @@ describe('addEthereumChainHandler', () => { it('create a new networkConfiguration and switches to it without requesting permissions, if the requested chainId has `endowment:permitted-chains` permission granted for requesting origin', async () => { const mocks = makeMocks({ permissionedChainIds: [CHAIN_IDS.MAINNET], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -465,7 +447,6 @@ describe('addEthereumChainHandler', () => { it('create a new networkConfiguration, requests permissions and switches to it, if the requested chainId does not have permittedChains permission granted for requesting origin', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive: true, permissionedChainIds: [], overrides: { getNetworkConfigurationByChainId: jest @@ -516,7 +497,6 @@ describe('addEthereumChainHandler', () => { createMockOptimismConfiguration().chainId, CHAIN_IDS.MAINNET, ], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -562,9 +542,7 @@ describe('addEthereumChainHandler', () => { }); it('should return an error if an unexpected parameter is provided', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); const mockEnd = jest.fn(); const unexpectedParam = 'unexpected'; @@ -604,7 +582,6 @@ describe('addEthereumChainHandler', () => { it('should handle errors during the switch network permission request', async () => { const mockError = new Error('Permission request failed'); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: true, permissionedChainIds: [], overrides: { getCurrentChainIdForDomain: jest @@ -649,7 +626,6 @@ describe('addEthereumChainHandler', () => { it('should return an error if nativeCurrency.symbol does not match an existing network with the same chainId', async () => { const mocks = makeMocks({ permissionedChainIds: [CHAIN_IDS.MAINNET], - permissionsFeatureFlagIsActive: true, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -691,7 +667,6 @@ describe('addEthereumChainHandler', () => { const CURRENT_RPC_CONFIG = createMockNonInfuraConfiguration(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getCurrentChainIdForDomain: jest .fn() 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 57d14eb6e6b8..080fef549564 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,5 +1,4 @@ import { errorCodes, ethErrors } from 'eth-rpc-errors'; -import { ApprovalType } from '@metamask/controller-utils'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -156,46 +155,34 @@ export function validateAddEthereumChainParams(params, end) { export async function switchChain( res, end, - origin, chainId, - requestData, networkClientId, approvalFlowId, { isAddFlow, - getChainPermissionsFeatureFlag, setActiveNetwork, endApprovalFlow, - requestUserApproval, getCaveat, requestPermittedChainsPermission, grantPermittedChainsPermissionIncremental, }, ) { try { - if (getChainPermissionsFeatureFlag()) { - const { value: permissionedChainIds } = - getCaveat({ - target: PermissionNames.permittedChains, - caveatType: CaveatTypes.restrictNetworkSwitching, - }) ?? {}; - - if ( - permissionedChainIds === undefined || - !permissionedChainIds.includes(chainId) - ) { - if (isAddFlow) { - await grantPermittedChainsPermissionIncremental([chainId]); - } else { - await requestPermittedChainsPermission([chainId]); - } + const { value: permissionedChainIds } = + getCaveat({ + target: PermissionNames.permittedChains, + caveatType: CaveatTypes.restrictNetworkSwitching, + }) ?? {}; + + if ( + permissionedChainIds === undefined || + !permissionedChainIds.includes(chainId) + ) { + if (isAddFlow) { + await grantPermittedChainsPermissionIncremental([chainId]); + } else { + await requestPermittedChainsPermission([chainId]); } - } else { - await requestUserApproval({ - origin, - type: ApprovalType.SwitchEthereumChain, - requestData, - }); } await setActiveNetwork(networkClientId); 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 847cdf8abe24..f43973e4ba57 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 @@ -14,8 +14,7 @@ const switchEthereumChain = { getCaveat: true, requestPermittedChainsPermission: true, getCurrentChainIdForDomain: true, - requestUserApproval: true, - getChainPermissionsFeatureFlag: true, + grantPermittedChainsPermissionIncremental: true, }, }; @@ -32,8 +31,7 @@ async function switchEthereumChainHandler( requestPermittedChainsPermission, getCaveat, getCurrentChainIdForDomain, - requestUserApproval, - getChainPermissionsFeatureFlag, + grantPermittedChainsPermissionIncremental, }, ) { let chainId; @@ -66,27 +64,10 @@ async function switchEthereumChainHandler( ); } - const requestData = { - toNetworkConfiguration: networkConfigurationForRequestedChainId, - fromNetworkConfiguration: getNetworkConfigurationByChainId( - currentChainIdForOrigin, - ), - }; - - return switchChain( - res, - end, - origin, - chainId, - requestData, - networkClientIdToSwitchTo, - null, - { - getChainPermissionsFeatureFlag, - setActiveNetwork, - requestUserApproval, - getCaveat, - requestPermittedChainsPermission, - }, - ); + return switchChain(res, end, chainId, networkClientIdToSwitchTo, null, { + setActiveNetwork, + getCaveat, + requestPermittedChainsPermission, + grantPermittedChainsPermissionIncremental, + }); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js index 30a9f9aa8f8e..be612fbc7d8e 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js @@ -6,10 +6,6 @@ import switchEthereumChain from './switch-ethereum-chain'; const NON_INFURA_CHAIN_ID = '0x123456789'; -const mockRequestUserApproval = ({ requestData }) => { - return Promise.resolve(requestData.toNetworkConfiguration); -}; - const createMockMainnetConfiguration = () => ({ chainId: CHAIN_IDS.MAINNET, defaultRpcEndpointIndex: 0, @@ -33,7 +29,6 @@ const createMockLineaMainnetConfiguration = () => ({ describe('switchEthereumChainHandler', () => { const makeMocks = ({ permissionedChainIds = [], - permissionsFeatureFlagIsActive = false, overrides = {}, mockedGetNetworkConfigurationByChainIdReturnValue = createMockMainnetConfiguration(), mockedGetCurrentChainIdForDomainReturnValue = NON_INFURA_CHAIN_ID, @@ -42,15 +37,11 @@ describe('switchEthereumChainHandler', () => { mockGetCaveat.mockReturnValue({ value: permissionedChainIds }); return { - getChainPermissionsFeatureFlag: () => permissionsFeatureFlagIsActive, getCurrentChainIdForDomain: jest .fn() .mockReturnValue(mockedGetCurrentChainIdForDomainReturnValue), setNetworkClientIdForDomain: jest.fn(), setActiveNetwork: jest.fn(), - requestUserApproval: jest - .fn() - .mockImplementation(mockRequestUserApproval), requestPermittedChainsPermission: jest.fn(), getCaveat: mockGetCaveat, getNetworkConfigurationByChainId: jest @@ -65,11 +56,8 @@ describe('switchEthereumChainHandler', () => { }); describe('with permittedChains permissioning inactive', () => { - const permissionsFeatureFlagIsActive = false; - it('should call setActiveNetwork when switching to a built-in infura network', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -95,7 +83,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork when switching to a built-in infura network, when chainId from request is lower case', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -121,7 +108,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork when switching to a built-in infura network, when chainId from request is upper case', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -147,7 +133,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork when switching to a custom network', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -209,14 +194,11 @@ describe('switchEthereumChainHandler', () => { }); describe('with permittedChains permissioning active', () => { - const permissionsFeatureFlagIsActive = true; - it('should call requestPermittedChainsPermission and setActiveNetwork when chainId is not in `endowment:permitted-chains`', async () => { const mockrequestPermittedChainsPermission = jest .fn() .mockResolvedValue(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { requestPermittedChainsPermission: mockrequestPermittedChainsPermission, @@ -246,7 +228,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork without calling requestPermittedChainsPermission when requested chainId is in `endowment:permitted-chains`', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, permissionedChainIds: [CHAIN_IDS.MAINNET], }); const switchEthereumChainHandler = switchEthereumChain.implementation; @@ -274,7 +255,6 @@ describe('switchEthereumChainHandler', () => { .fn() .mockRejectedValue(mockError); const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { requestPermittedChainsPermission: mockrequestPermittedChainsPermission, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a5b110fadec2..cd899c57e179 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -232,6 +232,8 @@ import { getCurrentChainId } from '../../ui/selectors'; // eslint-disable-next-line import/no-restricted-paths import { getProviderConfig } from '../../ui/ducks/metamask/metamask'; import { endTrace, trace } from '../../shared/lib/trace'; +// eslint-disable-next-line import/no-restricted-paths +import { isSnapId } from '../../ui/helpers/utils/snaps'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -5745,7 +5747,7 @@ export default class MetamaskController extends EventEmitter { { origin }, { eth_accounts: {}, - ...(process.env.CHAIN_PERMISSIONS && { + ...(!isSnapId(origin) && { [PermissionNames.permittedChains]: {}, }), }, @@ -5780,10 +5782,12 @@ export default class MetamaskController extends EventEmitter { this.permissionController.requestPermissions( { origin }, { - ...(process.env.CHAIN_PERMISSIONS && - requestedPermissions[RestrictedMethods.eth_accounts] && { - [PermissionNames.permittedChains]: {}, - }), + ...(requestedPermissions[PermissionNames.eth_accounts] && { + [PermissionNames.permittedChains]: {}, + }), + ...(requestedPermissions[PermissionNames.permittedChains] && { + [PermissionNames.eth_accounts]: {}, + }), ...requestedPermissions, }, ), @@ -5819,8 +5823,6 @@ export default class MetamaskController extends EventEmitter { return undefined; }, - getChainPermissionsFeatureFlag: () => - Boolean(process.env.CHAIN_PERMISSIONS), // network configuration-related setActiveNetwork: async (networkClientId) => { await this.networkController.setActiveNetwork(networkClientId); diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index 60e0ea378b75..eda4ef5fbf6f 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -13,7 +13,7 @@ import { regularDelayMs, } from '../helpers'; import { Driver } from '../webdriver/driver'; -import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; +import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; import { retry } from '../../../development/lib/retry'; /** @@ -201,16 +201,12 @@ export async function connectAccountToTestDapp(driver: Driver) { await driver.delay(regularDelayMs); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); + + await driver.switchToWindowWithUrl(DAPP_URL); } export async function disconnectFromTestDapp(driver: Driver) { @@ -225,7 +221,6 @@ export async function disconnectFromTestDapp(driver: Driver) { text: '127.0.0.1:8080', tag: 'p', }); - await driver.clickElement('[data-testid="account-list-item-menu-button"]'); await driver.clickElement({ text: 'Disconnect', tag: 'button' }); await driver.clickElement('[data-testid ="disconnect-all"]'); } diff --git a/test/e2e/api-specs/ConfirmationRejectionRule.ts b/test/e2e/api-specs/ConfirmationRejectionRule.ts index 503d0358c63c..3e37dcd07fd7 100644 --- a/test/e2e/api-specs/ConfirmationRejectionRule.ts +++ b/test/e2e/api-specs/ConfirmationRejectionRule.ts @@ -69,10 +69,24 @@ export class ConfirmationsRejectRule implements Rule { await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await this.driver.findClickableElements({ - text: 'Next', + text: 'Connect', tag: 'button', }); + const editButtons = await this.driver.findElements( + '[data-testid="edit"]', + ); + await editButtons[1].click(); + + await this.driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + + await this.driver.clickElement( + '[data-testid="connect-more-chains-button"]', + ); + const screenshotTwo = await this.driver.driver.takeScreenshot(); call.attachments.push({ type: 'image', @@ -80,15 +94,26 @@ export class ConfirmationsRejectRule implements Rule { }); await this.driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', }); - await this.driver.clickElement({ - text: 'Confirm', - tag: 'button', + await switchToOrOpenDapp(this.driver); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: '0x539', // 1337 + }, + ], }); + await this.driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + await switchToOrOpenDapp(this.driver); } } catch (e) { diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index cf337b84e8f5..643dcefa35ae 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -755,12 +755,19 @@ const connectToDapp = async (driver) => { }); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + const editButtons = await driver.findElements('[data-testid="edit"]'); + await editButtons[1].click(); + await driver.clickElement({ - text: 'Next', - tag: 'button', + text: 'Localhost 8545', + tag: 'p', }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); + await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); diff --git a/test/e2e/json-rpc/switchEthereumChain.spec.js b/test/e2e/json-rpc/switchEthereumChain.spec.js index 75715b6ff00b..fba06db48131 100644 --- a/test/e2e/json-rpc/switchEthereumChain.spec.js +++ b/test/e2e/json-rpc/switchEthereumChain.spec.js @@ -7,6 +7,7 @@ const { DAPP_ONE_URL, unlockWallet, switchToNotificationWindow, + WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { isManifestV3 } = require('../../../shared/modules/mv3.utils'); @@ -17,7 +18,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -74,10 +74,10 @@ describe('Switch Ethereum Chain for two dapps', function () { // Confirm switchEthereumChain await switchToNotificationWindow(driver, 4); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); // Switch to Dapp One await driver.switchToWindow(dappOne); @@ -107,7 +107,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -145,24 +144,39 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // open two dapps - const dappOne = await openDapp(driver, undefined, DAPP_URL); + await openDapp(driver, undefined, DAPP_URL); await openDapp(driver, undefined, DAPP_ONE_URL); + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + // Initiate send transaction on Dapp two await driver.clickElement('#sendButton'); - await driver.delay(2000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findClickableElements({ + text: 'Confirm', + tag: 'button', + }); + + // Switch to Dapp One + await driver.switchToWindowWithUrl(DAPP_URL); // Switch Ethereum chain request const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x53a' }], + params: [{ chainId: '0x539' }], }); - // Switch to Dapp One - await driver.switchToWindow(dappOne); - assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); - // Initiate switchEthereumChain on Dapp One await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, @@ -186,10 +200,10 @@ describe('Switch Ethereum Chain for two dapps', function () { await switchToNotificationWindow(driver, 4); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); }, ); }); @@ -199,7 +213,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -237,14 +250,43 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // open two dapps + const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); - await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect Dapp One + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + // Switch and connect Dapp Two + + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); // switchEthereumChain request const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x53a' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -253,13 +295,13 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // Switch to notification of switchEthereumChain - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - // Switch to dapp one + // Switch back to dapp one await driver.switchToWindow(dappOne); assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); @@ -268,15 +310,16 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.delay(2000); // Switch to notification that should still be switchEthereumChain request but with a warning. - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement({ - span: 'span', - text: 'Switching networks will cancel all pending confirmations', - }); + // THIS IS BROKEN + // await driver.findElement({ + // span: 'span', + // text: 'Switching networks will cancel all pending confirmations', + // }); // Confirm switchEthereumChain with queued pending tx - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); // Window handles should only be expanded mm, dapp one, dapp 2, and the offscreen document // if this is an MV3 build(3 or 4 total) @@ -294,7 +337,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -332,14 +374,42 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // open two dapps + const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); - await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect Dapp One + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + // Switch and connect Dapp Two + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); // switchEthereumChain request const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x53a' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -348,13 +418,13 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // Switch to notification of switchEthereumChain - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - // Switch to dapp one + // Switch back to dapp one await driver.switchToWindow(dappOne); assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); @@ -363,12 +433,13 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.delay(2000); // Switch to notification that should still be switchEthereumChain request but with an warning. - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement({ - span: 'span', - text: 'Switching networks will cancel all pending confirmations', - }); + // THIS IS BROKEN + // await driver.findElement({ + // span: 'span', + // text: 'Switching networks will cancel all pending confirmations', + // }); // Cancel switchEthereumChain with queued pending tx await driver.clickElement({ text: 'Cancel', tag: 'button' }); @@ -377,7 +448,7 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.delay(1000); // Switch to new pending tx notification - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Sending ETH', tag: 'span', diff --git a/test/e2e/json-rpc/wallet_requestPermissions.spec.js b/test/e2e/json-rpc/wallet_requestPermissions.spec.js index 917e30ca12fc..5484fdf73d80 100644 --- a/test/e2e/json-rpc/wallet_requestPermissions.spec.js +++ b/test/e2e/json-rpc/wallet_requestPermissions.spec.js @@ -38,12 +38,7 @@ describe('wallet_requestPermissions', function () { await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Confirm', + text: 'Connect', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-txinsights-v2.spec.js b/test/e2e/snaps/test-snap-txinsights-v2.spec.js index 0b43dca40ffc..5fb56687de96 100644 --- a/test/e2e/snaps/test-snap-txinsights-v2.spec.js +++ b/test/e2e/snaps/test-snap-txinsights-v2.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -37,22 +36,18 @@ describe('Test Snap TxInsights-v2', function () { await driver.clickElement('#connecttransaction-insights'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', }); - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); - await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -62,17 +57,9 @@ describe('Test Snap TxInsights-v2', function () { await driver.clickElement('#getAccounts'); // switch back to MetaMask window and deal with dialogs - await switchToNotificationWindow(driver); - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.waitForSelector({ - text: 'Confirm', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', }); @@ -82,7 +69,7 @@ describe('Test Snap TxInsights-v2', function () { // switch back to MetaMask window and switch to tx insights pane await driver.delay(2000); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElement({ text: 'Confirm', @@ -140,12 +127,6 @@ describe('Test Snap TxInsights-v2', function () { tag: 'button', text: 'Activity', }); - - // wait for transaction confirmation - await driver.waitForSelector({ - css: '.transaction-status-label', - text: 'Confirmed', - }); }, ); }); diff --git a/test/e2e/snaps/test-snap-txinsights.spec.js b/test/e2e/snaps/test-snap-txinsights.spec.js index ff93a2ea910b..7f6b7a3bec46 100644 --- a/test/e2e/snaps/test-snap-txinsights.spec.js +++ b/test/e2e/snaps/test-snap-txinsights.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -37,22 +36,18 @@ describe('Test Snap TxInsights', function () { await driver.clickElement('#connecttransaction-insights'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', }); - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); - await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -62,17 +57,9 @@ describe('Test Snap TxInsights', function () { await driver.clickElement('#getAccounts'); // switch back to MetaMask window and deal with dialogs - await switchToNotificationWindow(driver, 2); - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.waitForSelector({ - text: 'Confirm', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', }); @@ -82,11 +69,8 @@ describe('Test Snap TxInsights', function () { // switch back to MetaMask window and switch to tx insights pane await driver.delay(2000); - await switchToNotificationWindow(driver, 2); - await driver.waitForSelector({ - text: 'Insights Example Snap', - tag: 'button', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement({ text: 'Insights Example Snap', tag: 'button', diff --git a/test/e2e/tests/connections/connect-with-metamask.spec.js b/test/e2e/tests/connections/connect-with-metamask.spec.js new file mode 100644 index 000000000000..5611b40346db --- /dev/null +++ b/test/e2e/tests/connections/connect-with-metamask.spec.js @@ -0,0 +1,79 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + logInWithBalanceValidation, + defaultGanacheOptions, + openDapp, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Connections page', function () { + it('should render new connections flow', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await openDapp(driver); + // Connect to dapp + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // should render new connections page + const newConnectionPage = await driver.waitForSelector({ + tag: 'h2', + text: 'Connect with MetaMask', + }); + assert.ok(newConnectionPage, 'Connection Page is defined'); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + 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', + }); + const connectionsPageAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok(connectionsPageAccountInfo, 'Connections Page is defined'); + const connectionsPageNetworkInfo = await driver.isElementPresent({ + text: 'Use your enabled networks', + tag: 'p', + }); + assert.ok(connectionsPageNetworkInfo, 'Connections Page is defined'); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/edit-account-flow.spec.js b/test/e2e/tests/connections/edit-account-flow.spec.js new file mode 100644 index 000000000000..7b05f439714c --- /dev/null +++ b/test/e2e/tests/connections/edit-account-flow.spec.js @@ -0,0 +1,101 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + connectToDapp, + logInWithBalanceValidation, + locateAccountBalanceDOM, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +const accountLabel2 = '2nd custom name'; +const accountLabel3 = '3rd custom name'; +describe('Edit Accounts Flow', function () { + it('should be able to edit accounts', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-add-account"]', + ); + await driver.fill('[placeholder="Account 2"]', accountLabel2); + await driver.clickElement({ text: 'Add account', tag: 'button' }); + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-add-account"]', + ); + await driver.fill('[placeholder="Account 3"]', accountLabel3); + await driver.clickElement({ text: 'Add account', tag: 'button' }); + await locateAccountBalanceDOM(driver); + 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', + }); + const connectionsPageAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok(connectionsPageAccountInfo, 'Connections Page is defined'); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + // Ensure there are edit buttons + assert.ok(editButtons.length > 0, 'Edit buttons are available'); + + // Click the first (0th) edit button + await editButtons[0].click(); + + await driver.clickElement({ + text: '2nd custom name', + tag: 'button', + }); + await driver.clickElement({ + text: '3rd custom name', + tag: 'button', + }); + await driver.clickElement( + '[data-testid="connect-more-accounts-button"]', + ); + const updatedAccountInfo = await driver.isElementPresent({ + text: '3 accounts connected', + tag: 'span', + }); + assert.ok(updatedAccountInfo, 'Accounts List Updated'); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/edit-networks-flow.spec.js b/test/e2e/tests/connections/edit-networks-flow.spec.js new file mode 100644 index 000000000000..e14e1ae325d5 --- /dev/null +++ b/test/e2e/tests/connections/edit-networks-flow.spec.js @@ -0,0 +1,85 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + connectToDapp, + logInWithBalanceValidation, + locateAccountBalanceDOM, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +async function switchToNetworkByName(driver, networkName) { + await driver.clickElement('.mm-picker-network'); + await driver.clickElement(`[data-testid="${networkName}"]`); +} + +describe('Edit Networks Flow', function () { + it('should be able to edit networks', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement('[data-testid="network-display"]'); + await driver.clickElement('.mm-modal-content__dialog .toggle-button'); + await driver.clickElement( + '.mm-modal-content__dialog button[aria-label="Close"]', + ); + + // Switch to first network, whose send transaction was just confirmed + await switchToNetworkByName(driver, 'Localhost 8545'); + await locateAccountBalanceDOM(driver); + 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', + }); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + // Ensure there are edit buttons + assert.ok(editButtons.length > 0, 'Edit buttons are available'); + + // Click the first (0th) edit button + await editButtons[1].click(); + + // Disconnect Mainnet + await driver.clickElement({ + text: 'Ethereum Mainnet', + tag: 'p', + }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); + const updatedNetworkInfo = await driver.isElementPresent({ + text: '2 networks connected', + tag: 'span', + }); + assert.ok(updatedNetworkInfo, 'Networks List Updated'); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/review-permissions-page.spec.js b/test/e2e/tests/connections/review-permissions-page.spec.js new file mode 100644 index 000000000000..d411a343b2c9 --- /dev/null +++ b/test/e2e/tests/connections/review-permissions-page.spec.js @@ -0,0 +1,145 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + connectToDapp, + logInWithBalanceValidation, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Review Permissions page', function () { + it('should show connections page', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + 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', + }); + const reviewPermissionsAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok( + reviewPermissionsAccountInfo, + 'Review Permissions Page is defined', + ); + const reviewPermissionsNetworkInfo = await driver.isElementPresent({ + text: 'Use your enabled networks', + tag: 'p', + }); + assert.ok( + reviewPermissionsNetworkInfo, + 'Review Permissions Page is defined', + ); + }, + ); + }); + it('should disconnect when click on Disconnect button in connections page', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + 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', + }); + const reviewPermissionsAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok( + reviewPermissionsAccountInfo, + 'Accounts are defined for Review Permissions Page', + ); + const reviewPermissionsNetworkInfo = await driver.isElementPresent({ + text: 'Use your enabled networks', + tag: 'p', + }); + assert.ok( + reviewPermissionsNetworkInfo, + 'Networks are defined for Review Permissions Page', + ); + await driver.clickElement({ text: 'Disconnect', tag: 'button' }); + await driver.clickElement('[data-testid ="disconnect-all"]'); + const noAccountConnected = await driver.isElementPresent({ + text: 'MetaMask isn’t connected to this site', + tag: 'p', + }); + assert.ok( + noAccountConnected, + 'Account disconected from connections page', + ); + + // Switch back to Dapp + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Button should show Connect text if dapp is not connected + + const getConnectStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connect', + }); + + assert.ok( + getConnectStatus, + 'Account is not connected to Dapp and button has text connect', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/review-switch-permission-page.spec.js b/test/e2e/tests/connections/review-switch-permission-page.spec.js new file mode 100644 index 000000000000..5fe3d6d19526 --- /dev/null +++ b/test/e2e/tests/connections/review-switch-permission-page.spec.js @@ -0,0 +1,154 @@ +const { strict: assert } = require('assert'); +const FixtureBuilder = require('../../fixture-builder'); +const { + withFixtures, + openDapp, + unlockWallet, + DAPP_URL, + regularDelayMs, + WINDOW_TITLES, + defaultGanacheOptions, + switchToNotificationWindow, +} = require('../../helpers'); +const { PAGES } = require('../../webdriver/driver'); + +describe('Permissions Page when Dapp Switch to an enabled and non permissioned network', function () { + it('should switch to the chain when dapp tries to switch network to an enabled network after showing updated permissions page', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerDoubleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Navigate to extension home screen + await driver.navigate(PAGES.HOME); + + // Open Dapp One + await openDapp(driver, undefined, DAPP_URL); + + await driver.delay(regularDelayMs); + + const chainIdRequest = JSON.stringify({ + method: 'eth_chainId', + }); + + const chainIdBeforeConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + assert.equal(chainIdBeforeConnect, '0x539'); // 1337 + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); + + // Switch to second network + await driver.clickElement({ + text: 'Ethereum Mainnet', + css: 'p', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdBeforeConnectAfterManualSwitch = + await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // before connecting the chainId should change with the wallet + assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); + + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await switchToNotificationWindow(driver); + + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // should still be on the same chainId as the wallet after connecting + assert.equal(chainIdAfterConnect, '0x1'); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); + + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await switchToNotificationWindow(driver); + await driver.findClickableElements({ + text: 'Confirm', + tag: 'button', + }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterDappSwitch = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // should be on the new chainId that was requested + assert.equal(chainIdAfterDappSwitch, '0x539'); // 1337 + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); + + // Switch network + await driver.clickElement({ + text: 'Localhost 8546', + css: 'p', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterManualSwitch = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + assert.equal(chainIdAfterManualSwitch, '0x539'); + }, + ); + }); +}); diff --git a/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js b/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js index bd2b4a6b1aef..b992925ffc7a 100644 --- a/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js +++ b/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js @@ -65,8 +65,7 @@ describe('Dapp interactions', function () { navigate: false, }); - await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElement({ text: 'Connect', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.waitForSelector({ css: '#accounts', diff --git a/test/e2e/tests/dapp-interactions/permissions.spec.js b/test/e2e/tests/dapp-interactions/permissions.spec.js index 029a0a0661bc..adf3b809a656 100644 --- a/test/e2e/tests/dapp-interactions/permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/permissions.spec.js @@ -36,11 +36,7 @@ describe('Permissions', function () { windowHandles, ); await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + text: 'Connect', tag: 'button', }); diff --git a/test/e2e/tests/metrics/dapp-viewed.spec.js b/test/e2e/tests/metrics/dapp-viewed.spec.js index 78214685777e..668f93e65dc5 100644 --- a/test/e2e/tests/metrics/dapp-viewed.spec.js +++ b/test/e2e/tests/metrics/dapp-viewed.spec.js @@ -69,22 +69,6 @@ async function mockPermissionApprovedEndpoint(mockServer) { }); } -async function createTwoAccounts(driver) { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-account"]', - ); - await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: '2nd account', - }); -} - const waitForDappConnected = async (driver) => { await driver.waitForSelector({ css: '#accounts', @@ -273,57 +257,6 @@ describe('Dapp viewed Event @no-mmi', function () { ); }); - it('is sent when connecting dapp with two accounts', async function () { - async function mockSegment(mockServer) { - return [await mockedDappViewedEndpointFirstVisit(mockServer)]; - } - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withMetaMetricsController({ - metaMetricsId: validFakeMetricsId, - participateInMetaMetrics: true, - }) - .build(), - title: this.test.fullTitle(), - ganacheOptions: defaultGanacheOptions, - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - // create 2nd account - await createTwoAccounts(driver); - // Connect to dapp with two accounts - await openDapp(driver); - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement( - '[data-testid="choose-account-list-operate-all-check-box"]', - ); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - - const events = await getEventPayloads(driver, mockedEndpoints); - const dappViewedEventProperties = events[0].properties; - assert.equal(dappViewedEventProperties.is_first_visit, true); - assert.equal(dappViewedEventProperties.number_of_accounts, 2); - assert.equal(dappViewedEventProperties.number_of_accounts_connected, 2); - }, - ); - }); - it('is sent when reconnect to a dapp that has been connected before', async function () { async function mockSegment(mockServer) { return [ @@ -372,28 +305,20 @@ describe('Dapp viewed Event @no-mmi', function () { text: '127.0.0.1:8080', tag: 'p', }); - await driver.clickElement( - '[data-testid ="account-list-item-menu-button"]', - ); await driver.clickElement({ text: 'Disconnect', tag: 'button', }); await driver.clickElement('[data-testid ="disconnect-all"]'); - await driver.clickElement('button[aria-label="Back"]'); - await driver.clickElement('button[aria-label="Back"]'); // validate dapp is not connected - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ - text: 'All Permissions', - tag: 'div', - }); - await driver.findElement({ - text: 'Nothing to see here', + const noAccountConnected = await driver.isElementPresent({ + text: 'MetaMask isn’t connected to this site', tag: 'p', }); + assert.ok( + noAccountConnected, + 'Account disconected from connections page', + ); // reconnect again await connectToDapp(driver); const events = await getEventPayloads(driver, mockedEndpoints); diff --git a/test/e2e/tests/multichain/connection-page.spec.js b/test/e2e/tests/multichain/connection-page.spec.js deleted file mode 100644 index 122a83e718fa..000000000000 --- a/test/e2e/tests/multichain/connection-page.spec.js +++ /dev/null @@ -1,219 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - WINDOW_TITLES, - connectToDapp, - logInWithBalanceValidation, - locateAccountBalanceDOM, - defaultGanacheOptions, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); - -const accountLabel2 = '2nd custom name'; -const accountLabel3 = '3rd custom name'; - -describe('Connections page', function () { - it('should disconnect when click on Disconnect button in connections page', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - title: this.test.fullTitle(), - ganacheOptions: defaultGanacheOptions, - }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - await connectToDapp(driver); - - // It should render connected status for button if dapp is connected - const getConnectedStatus = await driver.waitForSelector({ - css: '#connectButton', - text: 'Connected', - }); - assert.ok(getConnectedStatus, 'Account is connected to Dapp'); - - // Switch to extension Tab - 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('[data-testid ="connections-page"]'); - const connectionsPage = await driver.isElementPresent({ - text: '127.0.0.1:8080', - tag: 'span', - }); - assert.ok(connectionsPage, 'Connections Page is defined'); - await driver.clickElement( - '[data-testid ="account-list-item-menu-button"]', - ); - await driver.clickElement({ text: 'Disconnect', tag: 'button' }); - await driver.clickElement('[data-testid ="disconnect-all"]'); - await driver.clickElement('button[aria-label="Back"]'); - await driver.clickElement('button[aria-label="Back"]'); - // validate dapp is not connected - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'All Permissions', tag: 'div' }); - const noAccountConnected = await driver.isElementPresent({ - text: 'Nothing to see here', - tag: 'p', - }); - assert.ok( - noAccountConnected, - 'Account disconected from connections page', - ); - - // Switch back to Dapp - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - // Button should show Connect text if dapp is not connected - - const getConnectStatus = await driver.waitForSelector({ - css: '#connectButton', - text: 'Connect', - }); - - assert.ok( - getConnectStatus, - 'Account is not connected to Dapp and button has text connect', - ); - }, - ); - }); - - it('should connect more accounts when already connected to a dapp', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - title: this.test.fullTitle(), - ganacheOptions: defaultGanacheOptions, - }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - await connectToDapp(driver); - - const account = await driver.findElement('#accounts'); - const accountAddress = await account.getText(); - - // Dapp should contain single connected account address - assert.strictEqual( - accountAddress, - '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - ); - // disconnect dapp in fullscreen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // Add two new accounts with custom label - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-account"]', - ); - await driver.fill('[placeholder="Account 2"]', accountLabel2); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-account"]', - ); - await driver.fill('[placeholder="Account 3"]', accountLabel3); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - await locateAccountBalanceDOM(driver); - 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', - }); - - // Connect only second account and keep third account unconnected - await driver.clickElement({ - text: 'Connect more accounts', - tag: 'button', - }); - await driver.clickElement({ - text: '2nd custom name', - tag: 'button', - }); - await driver.clickElement( - '[data-testid ="connect-more-accounts-button"]', - ); - const newAccountConnected = await driver.isElementPresent({ - text: '2nd custom name', - tag: 'button', - }); - - assert.ok(newAccountConnected, 'Connected More Account Successfully'); - // Switch back to Dapp - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - // Find the span element that contains the account addresses - const accounts = await driver.findElement('#accounts'); - const accountAddresses = await accounts.getText(); - - // Dapp should contain both the connected account addresses - assert.strictEqual( - accountAddresses, - '0x09781764c08de8ca82e156bbf156a3ca217c7950,0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - ); - }, - ); - }); - - // Skipped until issue where firefox connecting to dapp is resolved. - // it('shows that the account is connected to the dapp', async function () { - // await withFixtures( - // { - // dapp: true, - // fixtures: new FixtureBuilder().build(), - // title: this.test.fullTitle(), - // ganacheOptions: defaultGanacheOptions, - // }, - // async ({ driver, ganacheServer }) => { - // const ACCOUNT = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; - // const SHORTENED_ACCOUNT = shortenAddress(ACCOUNT); - // await logInWithBalanceValidation(driver, ganacheServer); - // await openDappConnectionsPage(driver); - // // Verify that there are no connected accounts - // await driver.assertElementNotPresent( - // '[data-testid="account-list-address"]', - // ); - - // await connectToDapp(driver); - // await openDappConnectionsPage(driver); - - // const account = await driver.findElement( - // '[data-testid="account-list-address"]', - // ); - // const accountAddress = await account.getText(); - - // // Dapp should contain single connected account address - // assert.strictEqual(accountAddress, SHORTENED_ACCOUNT); - // }, - // ); - // }); -}); diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index 70325cb5155b..dc8f38e1168c 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -369,13 +369,6 @@ describe('Custom network', function () { tag: 'button', text: 'Approve', }); - - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); }, ); }); diff --git a/test/e2e/tests/network/chain-interactions.spec.js b/test/e2e/tests/network/chain-interactions.spec.js index ba774ffecdb1..5b831ab1ba54 100644 --- a/test/e2e/tests/network/chain-interactions.spec.js +++ b/test/e2e/tests/network/chain-interactions.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { generateGanacheOptions, withFixtures, @@ -14,53 +13,6 @@ describe('Chain Interactions', function () { const ganacheOptions = generateGanacheOptions({ concurrent: [{ port, chainId }], }); - it('should add the Ganache test chain and not switch the network', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await logInWithBalanceValidation(driver); - - // trigger add chain confirmation - await openDapp(driver); - await driver.clickElement('#addEthereumChain'); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - // verify chain details - const [networkName, networkUrl, chainIdElement] = - await driver.findElements('.definition-list dd'); - assert.equal(await networkName.getText(), `Localhost ${port}`); - assert.equal(await networkUrl.getText(), `http://127.0.0.1:${port}`); - assert.equal(await chainIdElement.getText(), chainId.toString()); - - // approve add chain, cancel switch chain - await driver.clickElement({ text: 'Approve', tag: 'button' }); - await driver.clickElement({ text: 'Cancel', tag: 'button' }); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // verify networks - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); - - await driver.clickElement('[data-testid="network-display"]'); - const ganacheChain = await driver.findElements({ - text: `Localhost ${port}`, - tag: 'p', - }); - assert.ok(ganacheChain.length, 1); - }, - ); - }); it('should add the Ganache chain and switch the network', async function () { await withFixtures( @@ -81,7 +33,6 @@ describe('Chain Interactions', function () { // approve and switch chain await driver.clickElement({ text: 'Approve', tag: 'button' }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); // switch to extension await driver.switchToWindowWithTitle( diff --git a/test/e2e/tests/network/deprecated-networks.spec.js b/test/e2e/tests/network/deprecated-networks.spec.js index 29587f53afff..26c2388e4b51 100644 --- a/test/e2e/tests/network/deprecated-networks.spec.js +++ b/test/e2e/tests/network/deprecated-networks.spec.js @@ -92,13 +92,6 @@ describe('Deprecated networks', function () { text: 'Approve', }); - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); const deprecationWarningText = @@ -178,13 +171,6 @@ describe('Deprecated networks', function () { text: 'Approve', }); - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); const deprecationWarningText = @@ -264,13 +250,6 @@ describe('Deprecated networks', function () { text: 'Approve', }); - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); const deprecationWarningText = 'This network is deprecated'; diff --git a/test/e2e/tests/network/switch-custom-network.spec.js b/test/e2e/tests/network/switch-custom-network.spec.js index 694a8f309f01..09dedc3a62da 100644 --- a/test/e2e/tests/network/switch-custom-network.spec.js +++ b/test/e2e/tests/network/switch-custom-network.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { withFixtures, @@ -30,9 +29,6 @@ describe('Switch ethereum chain', function () { async ({ driver }) => { await unlockWallet(driver); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await openDapp(driver); await driver.clickElement({ @@ -40,62 +36,21 @@ describe('Switch ethereum chain', function () { text: 'Add Localhost 8546', }); - 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: 'Approve', }); - await driver.findElement({ - tag: 'h3', - text: 'Allow this site to switch the network?', - }); - - // Don't switch to network now, because we will click the 'Switch to Localhost 8546' button below - await driver.clickElement({ - tag: 'button', - text: 'Cancel', - }); - - await driver.waitUntilXWindowHandles(2); - - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - await driver.clickElement({ - tag: 'button', - text: 'Switch to Localhost 8546', - }); - - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, + WINDOW_TITLES.ExtensionInFullScreenView, ); - await driver.clickElement({ - tag: 'button', - text: 'Switch network', - }); - - await driver.waitUntilXWindowHandles(2); - - await driver.switchToWindow(extension); - - const currentNetworkName = await driver.findElement({ - tag: 'span', + await driver.findElement({ + css: '[data-testid="network-display"]', text: 'Localhost 8546', }); - - assert.ok( - Boolean(currentNetworkName), - 'Failed to switch to custom network', - ); }, ); }); diff --git a/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js b/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js index c2a86226d0c4..deb189404fa8 100644 --- a/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js +++ b/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js @@ -6,11 +6,9 @@ const { unlockWallet, DAPP_URL, DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -49,23 +47,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await openDapp(driver, undefined, DAPP_URL); // Connect to dapp 1 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -89,23 +77,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await openDapp(driver, undefined, DAPP_ONE_URL); // Connect to dapp 2 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp one send tx @@ -122,30 +100,29 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement( + await driver.waitForSelector( By.xpath("//div[normalize-space(.)='1 of 2']"), ); - // Check correct network on confirm tx. - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); - // Reject All Transactions await driver.clickElement('.page-container__footer-secondary a'); - await driver.clickElement({ text: 'Reject all', tag: 'button' }); // TODO: Do we want to confirm here? + // TODO: Do we want to confirm here? + await driver.clickElementAndWaitForWindowToClose({ + text: 'Reject all', + tag: 'button', + }); // Wait for confirmation to close - await driver.waitUntilXWindowHandles(3); + // TODO: find a better way to handle different dialog ids + await driver.delay(2000); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement( + await driver.waitForSelector( By.xpath("//div[normalize-space(.)='1 of 2']"), ); diff --git a/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js b/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js index 994afd5b4f31..265b28d0f56d 100644 --- a/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js +++ b/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js @@ -1,16 +1,14 @@ -const { strict: assert } = require('assert'); +const { By } = require('selenium-webdriver'); const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, - openDapp, - unlockWallet, - DAPP_URL, DAPP_ONE_URL, - regularDelayMs, - WINDOW_TITLES, + DAPP_URL, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, + openDapp, + unlockWallet, + WINDOW_TITLES, + withFixtures, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -52,39 +50,35 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + await driver.switchToWindowWithUrl(DAPP_URL); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x53a' }], }); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, + // Ensure Dapp One is on Localhost 8546 + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, ); - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); + // Should auto switch without prompt since already approved via connect - // Switch to second network - await driver.clickElement({ - text: 'Localhost 8546', - css: 'p', - }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // Wait for the first dapp's connect confirmation to disappear await driver.waitUntilXWindowHandles(2); - // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. // Open Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); @@ -92,79 +86,71 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp 1 send 2 tx await driver.switchToWindowWithUrl(DAPP_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - await driver.delay(largeDelayMs); + await driver.waitUntilXWindowHandles(4); // Dapp 2 send 2 tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - + // We cannot wait for the dialog, since it is already opened from before await driver.delay(largeDelayMs); - // Dapp 1 send 1 tx + // Dapp 1 send 1 tx await driver.switchToWindowWithUrl(DAPP_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); + // We cannot switch directly, as the dialog is sometimes closed and re-opened + await driver.delay(largeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver, 4); - - let navigationElement = await driver.findElement( - '.confirm-page-container-navigation', + await driver.waitForSelector( + By.xpath("//div[normalize-space(.)='1 of 2']"), ); - let navigationText = await navigationElement.getText(); - - assert.equal(navigationText.includes('1 of 2'), true); - - // Check correct network on confirm tx. - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); - // Reject All Transactions await driver.clickElement('.page-container__footer-secondary a'); - await driver.clickElement({ text: 'Reject all', tag: 'button' }); // TODO: Do we want to confirm here? + // TODO: Do we want to confirm here? + await driver.clickElementAndWaitForWindowToClose({ + text: 'Reject all', + tag: 'button', + }); - // Wait for confirmations to close and transactions from the second dapp to open - // Large delays to wait for confirmation spam opening/closing bug. - await driver.delay(5000); + await driver.switchToWindowWithUrl(DAPP_URL); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); + // We need a big delay to make sure dialog is not invalidated + // TODO: find a better way to handle different dialog ids + await driver.delay(2000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', + await driver.waitForSelector( + By.xpath("//div[normalize-space(.)='1 of 2']"), ); - navigationText = await navigationElement.getText(); - - assert.equal(navigationText.includes('1 of 2'), true); - // Check correct network on confirm tx. await driver.findElement({ css: '[data-testid="network-display"]', @@ -174,19 +160,17 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun // Reject All Transactions await driver.clickElement('.page-container__footer-secondary a'); - await driver.clickElement({ text: 'Reject all', tag: 'button' }); - - // Wait for confirmation to close - await driver.waitUntilXWindowHandles(3); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Reject all', + tag: 'button', + }); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); - - // Check correct network on confirm tx. - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); + // We need a big delay to make sure dialog is not invalidated + // TODO: find a better way to handle different dialog ids + await driver.delay(2000); + await driver.switchToWindowWithUrl(DAPP_URL); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); }, ); }); diff --git a/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js b/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js index d2d7cdf122c0..bd52558ec67f 100644 --- a/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js +++ b/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js @@ -22,10 +22,10 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function { dapp: true, fixtures: new FixtureBuilder() - .withNetworkControllerDoubleGanache() + .withNetworkControllerTripleGanache() .withPreferencesControllerUseRequestQueueEnabled() .build(), - dappOptions: { numberOfDapps: 2 }, + dappOptions: { numberOfDapps: 3 }, ganacheOptions: { ...defaultGanacheOptions, concurrent: [ @@ -34,6 +34,11 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function chainId, ganacheOptions2: defaultGanacheOptions, }, + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, ], }, title: this.test.fullTitle(), @@ -57,17 +62,25 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + await driver.switchToWindowWithUrl(DAPP_URL); + + let switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x3e8' }], }); + // Ensure Dapp One is on Localhost 7777 + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + // Should auto switch without prompt since already approved via connect + await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); @@ -88,18 +101,26 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await switchToNotificationWindow(driver, 4); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + + switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x53a' }], }); - // Dapp one send tx + // Ensure Dapp Two is on Localhost 8545 + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + // Should auto switch without prompt since already approved via connect + + // Dapp one send two tx await driver.switchToWindowWithUrl(DAPP_URL); await driver.delay(largeDelayMs); await driver.clickElement('#sendButton'); @@ -107,7 +128,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.delay(largeDelayMs); - // Dapp two send tx + // Dapp two send two tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.delay(largeDelayMs); await driver.clickElement('#sendButton'); @@ -126,7 +147,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function // Check correct network on confirm tx. await driver.findElement({ css: '[data-testid="network-display"]', - text: 'Localhost 8545', + text: 'Localhost 7777', }); // Reject All Transactions @@ -135,10 +156,11 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.clickElement({ text: 'Reject all', tag: 'button' }); // TODO: Do we want to confirm here? // Wait for confirmation to close - await driver.waitUntilXWindowHandles(3); + await driver.waitUntilXWindowHandles(4); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); + await driver.delay(largeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); navigationElement = await driver.findElement( '.confirm-page-container-navigation', @@ -151,7 +173,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function // Check correct network on confirm tx. await driver.findElement({ css: '[data-testid="network-display"]', - text: 'Localhost 8545', + text: 'Localhost 8546', }); }, ); diff --git a/test/e2e/tests/request-queuing/chainid-check.spec.js b/test/e2e/tests/request-queuing/chainid-check.spec.js index 850051d39c6a..1579a8ae5aa4 100644 --- a/test/e2e/tests/request-queuing/chainid-check.spec.js +++ b/test/e2e/tests/request-queuing/chainid-check.spec.js @@ -90,15 +90,8 @@ describe('Request Queueing chainId proxy sync', function () { await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -122,11 +115,11 @@ describe('Request Queueing chainId proxy sync', function () { await switchToNotificationWindow(driver); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -240,23 +233,13 @@ describe('Request Queueing chainId proxy sync', function () { assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); // Connect to dapp - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -267,6 +250,10 @@ describe('Request Queueing chainId proxy sync', function () { // should still be on the same chainId as the wallet after connecting assert.equal(chainIdAfterConnect, '0x1'); + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x1', + }); const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', @@ -278,14 +265,13 @@ describe('Request Queueing chainId proxy sync', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); - await switchToNotificationWindow(driver); - await driver.findClickableElements({ - text: 'Switch network', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const chainIdAfterDappSwitch = await driver.executeScript( @@ -295,6 +281,10 @@ describe('Request Queueing chainId proxy sync', function () { // should be on the new chainId that was requested assert.equal(chainIdAfterDappSwitch, '0x539'); // 1337 + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x539', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); diff --git a/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js index 8f6bf4c616d0..d52d45701563 100644 --- a/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js @@ -45,10 +45,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await unlockWallet(driver); await tempToggleSettingRedesignedConfirmations(driver); - // Open Dapp One + // Open and connect Dapp One await openDapp(driver, undefined, DAPP_URL); - // Connect to dapp await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); @@ -57,25 +56,14 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - // Open Dapp Two + // Open and connect to Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); - // Connect to dapp 2 await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); @@ -85,21 +73,35 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + // Switch Dapp Two to Localhost 8546 + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + let switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x53a' }], }); + // Initiate switchEthereumChain on Dapp one + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); + + // Should auto switch without prompt since already approved via connect + + // Switch back to Dapp One await driver.switchToWindowWithUrl(DAPP_URL); // switch chain for Dapp One - const switchEthereumChainRequest = JSON.stringify({ + switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', params: [{ chainId: '0x3e8' }], @@ -109,11 +111,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); - - await driver.waitUntilXWindowHandles(4); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x3e8', + }); + // Should auto switch without prompt since already approved via connect await driver.switchToWindowWithUrl(DAPP_URL); @@ -143,7 +145,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { // Check correct network on the signTypedData confirmation. await driver.findElement({ css: '[data-testid="signature-request-network-display"]', - text: 'Localhost 8545', + text: 'Localhost 8546', }); }, ); diff --git a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js index cbfb2b23a9a7..53c763d8891f 100644 --- a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js @@ -49,20 +49,10 @@ describe('Request Queueing', function () { await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - // Wait for Connecting notification to close. - await driver.waitUntilXWindowHandles(2); - // Navigate to test dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); diff --git a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js index a68884de4a4c..7a212533de4b 100644 --- a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js @@ -89,15 +89,8 @@ describe('Request Queuing Dapp 1 Send Tx -> Dapp 2 Request Accounts Tx', functio await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); diff --git a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js index 567ddf0f619d..c330596c48f3 100644 --- a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js @@ -51,16 +51,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -86,16 +79,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithUrl(DAPP_URL); @@ -104,7 +90,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x3e8' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -114,8 +100,8 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ - text: 'Allow this site to switch the network?', - tag: 'h3', + text: 'Use your enabled networks', + tag: 'p', }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); @@ -124,7 +110,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); @@ -207,16 +193,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -242,16 +221,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithUrl(DAPP_URL); @@ -260,7 +232,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x3e8' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -270,8 +242,8 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ - text: 'Allow this site to switch the network?', - tag: 'h3', + text: 'Use your enabled networks', + tag: 'p', }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); diff --git a/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js b/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js index 7821a005774d..d32e96e29571 100644 --- a/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js +++ b/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js @@ -5,11 +5,8 @@ const { unlockWallet, DAPP_URL, DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, defaultGanacheOptions, - largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -48,23 +45,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok await openDapp(driver, undefined, DAPP_URL); // Connect to dapp 1 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -88,28 +75,21 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok await openDapp(driver, undefined, DAPP_ONE_URL); // Connect to dapp 2 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver, 4); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp 1 send tx await driver.switchToWindowWithUrl(DAPP_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x1', + }); await driver.clickElement('#sendButton'); await driver.waitUntilXWindowHandles(4); @@ -117,18 +97,31 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok // Dapp 2 send tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); + await driver.waitUntilXWindowHandles(4); // Dapp 1 revokePermissions await driver.switchToWindowWithUrl(DAPP_URL); - await driver.clickElement('#revokeAccountsPermission'); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x1', + }); + await driver.assertElementNotPresent({ + css: '[id="chainId"]', + text: '0x53a', + }); // Confirmation will close then reopen - await driver.waitUntilXWindowHandles(3); + await driver.clickElement('#revokeAccountsPermission'); + // TODO: find a better way to handle different dialog ids + await driver.delay(3000); // Check correct network on confirm tx. - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ css: '[data-testid="network-display"]', diff --git a/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js b/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js index 6eb0b9d14f85..38fe1d7204d2 100644 --- a/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js +++ b/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js @@ -5,11 +5,9 @@ const { unlockWallet, DAPP_URL, DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -48,23 +46,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu await openDapp(driver, undefined, DAPP_URL); // Connect to dapp 1 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -80,31 +68,18 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu css: 'p', }); - // Wait for the first dapp's connect confirmation to disappear - await driver.waitUntilXWindowHandles(2); - // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. // Open Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); // Connect to dapp 2 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await switchToNotificationWindow(driver, 4); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp one send tx @@ -112,7 +87,7 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu await driver.delay(largeDelayMs); await driver.clickElement('#sendButton'); - await driver.delay(largeDelayMs); + await driver.waitUntilXWindowHandles(4); // Dapp two send tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); @@ -128,14 +103,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu await driver.waitUntilXWindowHandles(4); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(largeDelayMs); - - // Find correct network on confirm tx - await driver.findElement({ - text: 'Localhost 8545', - tag: 'span', - }); - // Reject Transaction await driver.findClickableElement({ text: 'Reject', tag: 'button' }); await driver.clickElement( @@ -161,6 +128,11 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu // Click Unconfirmed Tx await driver.clickElement('.transaction-list-item--unconfirmed'); + await driver.assertElementNotPresent({ + tag: 'p', + text: 'Network switched to Localhost 8546', + }); + // Confirm Tx await driver.clickElement('[data-testid="page-container-footer-next"]'); diff --git a/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js b/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js index a86229e2cdb1..df33600413e1 100644 --- a/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js @@ -3,9 +3,7 @@ const { withFixtures, openDapp, unlockWallet, - DAPP_URL, WINDOW_TITLES, - switchToNotificationWindow, defaultGanacheOptions, } = require('../../helpers'); @@ -18,7 +16,6 @@ describe('Request Queuing SwitchChain -> SendTx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPermissionControllerConnectedToTestDapp() .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { @@ -37,14 +34,30 @@ describe('Request Queuing SwitchChain -> SendTx', function () { async ({ driver }) => { await unlockWallet(driver); - await openDapp(driver, undefined, DAPP_URL); + await openDapp(driver); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // Switch Ethereum Chain - await driver.findClickableElement('#switchEthereumChain'); - await driver.clickElement('#switchEthereumChain'); + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); - // Keep notification confirmation on screen - await driver.waitUntilXWindowHandles(3); + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); // Navigate back to test dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -52,22 +65,23 @@ describe('Request Queuing SwitchChain -> SendTx', function () { // Dapp Send Button await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Persist Switch Ethereum Chain notifcation await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); + // THIS IS BROKEN // Find the cancel pending txs on the Switch Ethereum Chain notification. - await driver.findElement({ - text: 'Switching networks will cancel all pending confirmations', - tag: 'span', - }); + // await driver.findElement({ + // text: 'Switching networks will cancel all pending confirmations', + // tag: 'span', + // }); // Confirm Switch Network - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); // No confirmations, tx should be cleared await driver.waitUntilXWindowHandles(2); diff --git a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js index b84b76868303..308a9c36914b 100644 --- a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js @@ -8,6 +8,7 @@ const { withFixtures, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); +const { DAPP_URL } = require('../../constants'); describe('Request Queue SwitchChain -> WatchAsset', function () { const smartContract = SMART_CONTRACTS.HST; @@ -20,7 +21,6 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -42,17 +42,35 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { ); await logInWithBalanceValidation(driver, ganacheServer); - await openDapp(driver, contractAddress); + await openDapp(driver, contractAddress, DAPP_URL); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // Switch Ethereum Chain - await driver.clickElement('#switchEthereumChain'); + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); - await driver.waitUntilXWindowHandles(3); + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ - text: 'Allow this site to switch the network?', - tag: 'h3', + text: 'Use your enabled networks', + tag: 'p', }); // Switch back to test dapp @@ -68,10 +86,10 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { // Confirm Switch Network await driver.findClickableElement({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); }, diff --git a/test/e2e/tests/request-queuing/ui.spec.js b/test/e2e/tests/request-queuing/ui.spec.js index 482b18e0e4f5..b857d4307d5b 100644 --- a/test/e2e/tests/request-queuing/ui.spec.js +++ b/test/e2e/tests/request-queuing/ui.spec.js @@ -1,5 +1,5 @@ const { strict: assert } = require('assert'); -const { Browser, until } = require('selenium-webdriver'); +const { Browser } = require('selenium-webdriver'); const { CHAIN_IDS } = require('../../../../shared/constants/network'); const FixtureBuilder = require('../../fixture-builder'); const { @@ -16,6 +16,10 @@ const { DAPP_TWO_URL, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); +const { + PermissionNames, +} = require('../../../../app/scripts/controllers/permissions'); +const { CaveatTypes } = require('../../../../shared/constants/permissions'); // Window handle adjustments will need to be made for Non-MV3 Firefox // due to OffscreenDocument. Additionally Firefox continually bombs @@ -29,21 +33,12 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { await openDapp(driver, undefined, dappUrl); // Connect to the dapp - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - + await driver.clickElement({ text: 'Connect', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Switch back to the dapp @@ -52,6 +47,25 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { // Switch chains if necessary if (chainId) { await driver.delay(veryLargeDelayMs); + const getPermissionsRequest = JSON.stringify({ + method: 'wallet_getPermissions', + }); + const getPermissionsResult = await driver.executeScript( + `return window.ethereum.request(${getPermissionsRequest})`, + ); + + const permittedChains = + getPermissionsResult + ?.find( + (permission) => + permission.parentCapability === PermissionNames.permittedChains, + ) + ?.caveats.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value || []; + + const isAlreadyPermitted = permittedChains.includes(chainId); + const switchChainRequest = JSON.stringify({ method: 'wallet_switchEthereumChain', params: [{ chainId }], @@ -61,18 +75,20 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { `window.ethereum.request(${switchChainRequest})`, ); - await driver.delay(veryLargeDelayMs); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + if (!isAlreadyPermitted) { + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findClickableElement( - '[data-testid="confirmation-submit-button"]', - ); - await driver.clickElementAndWaitForWindowToClose( - '[data-testid="confirmation-submit-button"]', - ); + await driver.findClickableElement( + '[data-testid="page-container-footer-next"]', + ); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); - // Switch back to the dapp - await driver.switchToWindowWithUrl(dappUrl); + // Switch back to the dapp + await driver.switchToWindowWithUrl(dappUrl); + } } } @@ -183,7 +199,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -205,7 +220,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a'); @@ -249,7 +264,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -278,7 +292,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a'); @@ -377,7 +391,6 @@ describe('Request-queue UI changes', function () { preferences: { showTestNetworks: true }, }) .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -399,7 +412,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -451,7 +464,6 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), @@ -462,15 +474,13 @@ describe('Request-queue UI changes', function () { await unlockWallet(driver); // Open the first dapp which starts on chain '0x539 - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Ensure the dapp starts on the correct network - await driver.wait( - until.elementTextContains( - await driver.findElement('#chainId'), - '0x539', - ), - ); + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x539', + }); // Open the popup with shimmed activeTabOrigin await openPopupWithActiveTabOrigin(driver, DAPP_URL); @@ -482,12 +492,10 @@ describe('Request-queue UI changes', function () { await driver.switchToWindowWithUrl(DAPP_URL); // Check to make sure the dapp network changed - await driver.wait( - until.elementTextContains( - await driver.findElement('#chainId'), - '0x1', - ), - ); + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x1', + }); }, ); }); @@ -501,7 +509,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -521,7 +528,7 @@ describe('Request-queue UI changes', function () { await unlockWallet(driver); // Open the first dapp which starts on chain '0x539 - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open tab 2, switch to Ethereum Mainnet await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -554,7 +561,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -574,7 +580,7 @@ describe('Request-queue UI changes', function () { await unlockWallet(driver); // Open the first dapp which starts on chain '0x539 - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open tab 2, switch to Ethereum Mainnet await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -626,7 +632,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -652,7 +657,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -697,7 +702,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -722,7 +726,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); diff --git a/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js b/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js index 3c183b5a50a7..1c1baa17fb5a 100644 --- a/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js @@ -94,8 +94,6 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { await switchToNotificationWindow(driver); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); /** diff --git a/ui/components/app/permission-cell/permission-cell-status.js b/ui/components/app/permission-cell/permission-cell-status.js index 5b0cf8f25b56..7dcf32a3b2ee 100644 --- a/ui/components/app/permission-cell/permission-cell-status.js +++ b/ui/components/app/permission-cell/permission-cell-status.js @@ -49,7 +49,7 @@ export const PermissionCellStatus = ({ const renderAccountsGroup = () => ( <> - {process.env.CHAIN_PERMISSIONS ? ( + {networks.length > 0 ? ( <Box as="span" className="permission-cell__status__accounts-group-box" diff --git a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index 9f6637d66cf7..e5e8503e6c73 100644 --- a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -71,30 +71,18 @@ export default class PermissionPageContainerContent extends PureComponent { paddingBottom={4} > <Text variant={TextVariant.headingMd} textAlign={TextAlign.Center}> - {process.env.CHAIN_PERMISSIONS - ? t('reviewPermissions') - : t('permissions')} + {t('reviewPermissions')} </Text> <Text variant={TextVariant.bodyMd} textAlign={TextAlign.Center}> - {process.env.CHAIN_PERMISSIONS - ? t('nativeNetworkPermissionRequestDescription', [ - <Text - as="span" - key={`description_key_${subjectMetadata.origin}`} - fontWeight={FontWeight.Medium} - > - {getURLHost(subjectMetadata.origin)} - </Text>, - ]) - : t('nativePermissionRequestDescription', [ - <Text - as="span" - key={`description_key_${subjectMetadata.origin}`} - fontWeight={FontWeight.Medium} - > - {subjectMetadata.origin} - </Text>, - ])} + {t('nativeNetworkPermissionRequestDescription', [ + <Text + as="span" + key={`description_key_${subjectMetadata.origin}`} + fontWeight={FontWeight.Medium} + > + {getURLHost(subjectMetadata.origin)} + </Text>, + ])} </Text> </Box> <Box diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js index e0bed04e5429..da15d384849c 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js @@ -7,7 +7,6 @@ import { getSnapsMetadata } from '../../../selectors'; import { getSnapName } from '../../../helpers/utils/util'; import PermissionCell from '../permission-cell'; import { Box } from '../../component-library'; -import { CaveatTypes } from '../../../../shared/constants/permissions'; /** * Get one or more permission descriptions for a permission name. @@ -18,10 +17,6 @@ import { CaveatTypes } from '../../../../shared/constants/permissions'; * @returns {JSX.Element} A permission description node. */ function getDescriptionNode(permission, index, accounts) { - const permissionValue = permission?.permissionValue?.caveats?.find( - (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, - )?.value; - return ( <PermissionCell permissionName={permission.name} @@ -31,7 +26,7 @@ function getDescriptionNode(permission, index, accounts) { avatarIcon={permission.leftIcon} key={`${permission.permissionName}-${index}`} accounts={accounts} - permissionValue={permissionValue} + permissionValue={permission.permissionValue.restrictNetworkSwitching} /> ); } diff --git a/ui/components/multichain/app-header/app-header-unlocked-content.tsx b/ui/components/multichain/app-header/app-header-unlocked-content.tsx index 57e0c2f2c5fc..69ffca3f71c3 100644 --- a/ui/components/multichain/app-header/app-header-unlocked-content.tsx +++ b/ui/components/multichain/app-header/app-header-unlocked-content.tsx @@ -55,10 +55,7 @@ import { MetaMetricsContext } from '../../../contexts/metametrics'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { MINUTE } from '../../../../shared/constants/time'; import { NotificationsTagCounter } from '../notifications-tag-counter'; -import { - CONNECTIONS, - REVIEW_PERMISSIONS, -} from '../../../helpers/constants/routes'; +import { REVIEW_PERMISSIONS } from '../../../helpers/constants/routes'; import { MultichainNetwork } from '../../../selectors/multichain'; type AppHeaderUnlockedContentProps = { @@ -122,11 +119,7 @@ export const AppHeaderUnlockedContent = ({ }; const handleConnectionsRoute = () => { - if (process.env.CHAIN_PERMISSIONS) { - history.push(`${REVIEW_PERMISSIONS}/${encodeURIComponent(origin)}`); - } else { - history.push(`${CONNECTIONS}/${encodeURIComponent(origin)}`); - } + history.push(`${REVIEW_PERMISSIONS}/${encodeURIComponent(origin)}`); }; return ( diff --git a/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx b/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx index 17f5357a2fbd..62ca0ed8093a 100644 --- a/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx +++ b/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx @@ -19,7 +19,6 @@ export enum DisconnectType { } export const DisconnectAllModal = ({ - type, hostname, onClick, onClose, @@ -35,17 +34,9 @@ export const DisconnectAllModal = ({ <Modal isOpen onClose={onClose} data-testid="disconnect-all-modal"> <ModalOverlay /> <ModalContent> - <ModalHeader onClose={onClose}> - {process.env.CHAIN_PERMISSIONS - ? t('disconnect') - : t('disconnectAllTitle', [t(type)])} - </ModalHeader> + <ModalHeader onClose={onClose}>{t('disconnect')}</ModalHeader> <ModalBody> - {process.env.CHAIN_PERMISSIONS ? ( - <Text>{t('disconnectAllDescription', [hostname])}</Text> - ) : ( - <Text>{t('disconnectAllText', [t(type), hostname])}</Text> - )} + {<Text>{t('disconnectAllDescription', [hostname])}</Text>} </ModalBody> <ModalFooter> <Button diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 968025c16004..6dc4457cceb5 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -278,10 +278,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { dispatch(setActiveNetwork(networkClientId)); dispatch(toggleNetworkMenu()); - if ( - process.env.CHAIN_PERMISSIONS && - permittedAccountAddresses.length > 0 - ) { + if (permittedAccountAddresses.length > 0) { grantPermittedChain(selectedTabOrigin, network.chainId); if (!permittedChainIds.includes(network.chainId)) { dispatch(showPermittedNetworkToast()); diff --git a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap index 69054c8eaa43..66dbd90aeea2 100644 --- a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap +++ b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap @@ -55,32 +55,14 @@ exports[`All Connections render renders correctly 1`] = ` style="align-self: center;" > <div - class="mm-box mm-badge-wrapper mm-box--display-inline-block" + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-favicon mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" + data-testid="connection-list-item__avatar-favicon" > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-favicon mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" - data-testid="connection-list-item__avatar-favicon" - > - <img - alt="avatar-favicon logo" - class="mm-avatar-favicon__image" - src="https://metamask.github.io/test-dapp/metamask-fox.svg" - /> - </div> - <div - class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default mm-box--border-width-1 box--border-style-solid" - data-testid="connection-list-item__avatar-network-badge" - > - <img - alt="Ethereum Mainnet logo" - class="mm-avatar-network__network-image" - src="./images/eth_logo.svg" - /> - </div> - </div> + <img + alt="avatar-favicon logo" + class="mm-avatar-favicon__image" + src="https://metamask.github.io/test-dapp/metamask-fox.svg" + /> </div> </div> <div @@ -98,73 +80,14 @@ exports[`All Connections render renders correctly 1`] = ` <span class="mm-box mm-text mm-text--body-md mm-box--width-max mm-box--color-text-alternative" > - Connected with + 1 + + accounts +   •  + 0 + + networks </span> - <div - aria-describedby="tippy-tooltip-1" - class="" - data-original-title="This can be changed in "Settings > Alerts"" - data-tooltipped="" - style="display: inline;" - > - <div - class="mm-box multichain-avatar-group mm-box--display-flex mm-box--gap-1 mm-box--align-items-center" - data-testid="avatar-group" - > - <div - class="mm-box mm-box--display-flex" - > - <div - class="mm-box mm-box--rounded-full" - style="margin-left: 0px;" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-account mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" - > - <div - class="mm-avatar-account__jazzicon" - > - <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 16px; height: 16px; display: inline-block; background: rgb(250, 58, 0);" - > - <svg - height="16" - width="16" - x="0" - y="0" - > - <rect - fill="#18CDF2" - height="16" - transform="translate(-0.52419675189697 -1.6521420347302493) rotate(328.9 8 8)" - width="16" - x="0" - y="0" - /> - <rect - fill="#035E56" - height="16" - transform="translate(-9.149230854416022 5.2962309358743) rotate(176.2 8 8)" - width="16" - x="0" - y="0" - /> - <rect - fill="#F26602" - height="16" - transform="translate(8.333921009111961 -7.102569861498541) rotate(468.9 8 8)" - width="16" - x="0" - y="0" - /> - </svg> - </div> - </div> - </div> - </div> - </div> - </div> - </div> </div> </div> <div diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.js b/ui/components/multichain/pages/permissions-page/connection-list-item.js index 6f9a72a6ea0d..725499b30841 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.js @@ -17,9 +17,6 @@ import { import { useI18nContext } from '../../../../hooks/useI18nContext'; import { AvatarFavicon, - AvatarNetwork, - AvatarNetworkSize, - BadgeWrapper, Box, Icon, IconName, @@ -27,10 +24,8 @@ import { Text, } from '../../../component-library'; import { getURLHost } from '../../../../helpers/utils/util'; -import { getAvatarNetworkColor } from '../../../../helpers/utils/accounts'; import { SnapIcon } from '../../../app/snaps/snap-icon'; import { getPermittedChainsForSelectedTab } from '../../../../selectors'; -import { ConnectionListTooltip } from './connection-list-tooltip/connection-list-tooltip'; export const ConnectionListItem = ({ connection, onClick }) => { const t = useI18nContext(); @@ -39,32 +34,6 @@ export const ConnectionListItem = ({ connection, onClick }) => { getPermittedChainsForSelectedTab(state, connection.origin), ); - const renderListItem = process.env.CHAIN_PERMISSIONS ? ( - <AvatarFavicon - data-testid="connection-list-item__avatar-favicon" - src={connection.iconUrl} - /> - ) : ( - <BadgeWrapper - badge={ - <AvatarNetwork - data-testid="connection-list-item__avatar-network-badge" - size={AvatarNetworkSize.Xs} - name={connection.networkName} - src={connection.networkIconUrl} - borderWidth={1} - borderColor={BackgroundColor.backgroundDefault} - backgroundColor={getAvatarNetworkColor(connection.networkName)} - /> - } - > - <AvatarFavicon - data-testid="connection-list-item__avatar-favicon" - src={connection.iconUrl} - /> - </BadgeWrapper> - ); - return ( <Box data-testid="connection-list-item" @@ -91,7 +60,10 @@ export const ConnectionListItem = ({ connection, onClick }) => { avatarSize={IconSize.Md} /> ) : ( - <>{renderListItem}</> + <AvatarFavicon + data-testid="connection-list-item__avatar-favicon" + src={connection.iconUrl} + /> )} </Box> <Box @@ -110,31 +82,16 @@ export const ConnectionListItem = ({ connection, onClick }) => { alignItems={AlignItems.center} gap={1} > - {process.env.CHAIN_PERMISSIONS ? ( - <Text - as="span" - width={BlockSize.Max} - color={TextColor.textAlternative} - variant={TextVariant.bodyMd} - > - {connection.addresses.length} {t('accountsSmallCase')}  - •  - {connectedNetworks.length} {t('networksSmallCase')} - </Text> - ) : ( - <> - <Text - as="span" - width={BlockSize.Max} - color={TextColor.textAlternative} - variant={TextVariant.bodyMd} - > - {t('connectedWith')} - </Text> - - <ConnectionListTooltip connection={connection} /> - </> - )} + <Text + as="span" + width={BlockSize.Max} + color={TextColor.textAlternative} + variant={TextVariant.bodyMd} + > + {connection.addresses.length} {t('accountsSmallCase')}  + •  + {connectedNetworks.length} {t('networksSmallCase')} + </Text> </Box> )} </Box> diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.test.js b/ui/components/multichain/pages/permissions-page/connection-list-item.test.js index 7e9205517cd5..ffec0e4a3b28 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.test.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.test.js @@ -37,6 +37,10 @@ describe('ConnectionListItem', () => { iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', networkIconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', networkName: 'Test Dapp Network', + addresses: [ + '0xaaaF07C80ce267F3132cE7e6048B66E6E669365B', + '0xbbbD671F1Fcc94bCF0ebC6Ec4790Da35E8d5e1E1', + ], }; const { getByText, getByTestId } = renderWithProvider( @@ -70,36 +74,4 @@ describe('ConnectionListItem', () => { fireEvent.click(getByTestId('connection-list-item')); expect(onClickMock).toHaveBeenCalledTimes(1); }); - - it('renders badgewrapper correctly for non-Snap connection', () => { - const onClickMock = jest.fn(); - const mockConnection2 = { - extensionId: null, - iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', - name: 'MM Test Dapp', - origin: 'https://metamask.github.io', - subjectType: 'website', - addresses: ['0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da'], - addressToNameMap: { - '0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da': - 'Unreasonably long account name', - }, - networkIconUrl: './images/eth_logo.svg', - networkName: 'Ethereum Mainnet', - }; - const { getByTestId } = renderWithProvider( - <ConnectionListItem connection={mockConnection2} onClick={onClickMock} />, - store, - ); - - expect( - getByTestId('connection-list-item__avatar-network-badge'), - ).toBeInTheDocument(); - - expect( - document - .querySelector('.mm-avatar-network__network-image') - .getAttribute('src'), - ).toBe(mockConnection2.networkIconUrl); - }); }); diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.js b/ui/components/multichain/pages/permissions-page/permissions-page.js index 2b5a99fc55f5..491e041d7ac5 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.js @@ -23,7 +23,6 @@ import { TextVariant, } from '../../../../helpers/constants/design-system'; import { - CONNECTIONS, DEFAULT_ROUTE, REVIEW_PERMISSIONS, } from '../../../../helpers/constants/routes'; @@ -34,6 +33,7 @@ import { } from '../../../../selectors'; import { ProductTour } from '../../product-tour-popover'; import { hidePermissionsTour } from '../../../../store/actions'; +import { isSnapId } from '../../../../helpers/utils/snaps'; import { ConnectionListItem } from './connection-list-item'; export const PermissionsPage = () => { @@ -54,16 +54,14 @@ export const PermissionsPage = () => { const handleConnectionClick = (connection) => { const hostName = connection.origin; const safeEncodedHost = encodeURIComponent(hostName); - if (process.env.CHAIN_PERMISSIONS) { - history.push(`${REVIEW_PERMISSIONS}/${safeEncodedHost}`); - } else { - history.push(`${CONNECTIONS}/${safeEncodedHost}`); - } + + history.push(`${REVIEW_PERMISSIONS}/${safeEncodedHost}`); }; const renderConnectionsList = (connectionList) => Object.entries(connectionList).map(([itemKey, connection]) => { - return ( + const isSnap = isSnapId(connection.origin); + return isSnap ? null : ( <ConnectionListItem data-testid="connection-list-item" key={itemKey} 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 4b7da8f525fa..35e9a77656ba 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 @@ -260,6 +260,7 @@ export const ReviewPermissions = () => { startIconName={IconName.Logout} danger onClick={() => setShowDisconnectAllModal(true)} + data-test-id="disconnect-all" > {t('disconnect')} </Button> diff --git a/ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap b/ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap deleted file mode 100644 index 3115caf5af16..000000000000 --- a/ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap +++ /dev/null @@ -1,236 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PermissionApprovalContainer ConnectPath renders correctly 1`] = ` -<div> - <div - class="permissions-connect" - > - <div - class="mm-box" - > - <div - class="mm-box mm-box--padding-4 mm-box--display-flex mm-box--align-items-center mm-box--width-full mm-box--background-color-background-default" - style="box-shadow: var(--shadow-size-lg) var(--color-shadow-default);" - > - <div - class="mm-box" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-lg mm-text--body-lg-medium mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-alternative mm-box--background-color-background-alternative mm-box--rounded-full" - style="border-width: 0px;" - > - m - </div> - </div> - <div - class="mm-box mm-box--margin-right-4 mm-box--margin-left-4 mm-box--display-flex mm-box--flex-direction-column" - style="overflow: hidden;" - > - <p - class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--ellipsis mm-box--color-text-default" - > - metamask.io - </p> - <p - class="mm-box mm-text mm-text--body-sm mm-text--ellipsis mm-box--color-text-alternative" - > - https://metamask.io - </p> - </div> - </div> - </div> - <div - class="mm-box permissions-connect-choose-account__content mm-box--padding-right-6 mm-box--padding-left-6 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--height-full mm-box--background-color-background-alternative" - > - <div - class="mm-box mm-box--padding-top-4 mm-box--padding-bottom-4 mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-center mm-box--align-items-center" - > - <h3 - class="mm-box mm-text mm-text--heading-md mm-box--color-text-default" - > - Connect with MetaMask - </h3> - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" - > - Select the account(s) to use on this site - </p> - </div> - <div - class="choose-account-list" - > - <div - class="choose-account-list__header--one-item" - > - <button - class="mm-box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-info-default mm-box--background-color-transparent" - style="cursor: pointer;" - > - New account - </button> - </div> - <div - class="choose-account-list__wrapper" - > - <div - class="mm-box choose-account-list__list" - style="overflow-x: hidden;" - > - <div - class="mm-box choose-account-list__account mm-box--display-flex mm-box--width-full mm-box--background-color-primary-muted" - data-testid="choose-account-list-0" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center mm-box--width-full" - > - <label - class="mm-box mm-text mm-checkbox mm-text--body-md mm-box--display-inline-flex mm-box--align-items-center mm-box--color-text-default" - > - <span - class="mm-checkbox__input-wrapper" - > - <input - checked="" - class="mm-box mm-checkbox__input mm-checkbox__input--checked mm-box--margin-0 mm-box--margin-right-0 mm-box--display-flex mm-box--background-color-primary-default mm-box--rounded-sm mm-box--border-color-primary-default mm-box--border-width-2 box--border-style-solid" - type="checkbox" - /> - <span - class="mm-box mm-checkbox__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-primary-inverse" - style="mask-image: url('./images/icons/check-bold.svg');" - /> - </span> - </label> - <div - class="mm-box mm-box--margin-left-2" - > - <div - class="" - > - <div - class="identicon" - style="height: 34px; width: 34px; border-radius: 17px;" - > - <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 34px; height: 34px; display: inline-block; background: rgb(35, 140, 225);" - > - <svg - height="34" - width="34" - x="0" - y="0" - > - <rect - fill="#FA4300" - height="34" - transform="translate(-1.8190711089650118 -0.7352934700785319) rotate(264.3 17 17)" - width="34" - x="0" - y="0" - /> - <rect - fill="#018E77" - height="34" - transform="translate(-12.049166321096887 -15.929310915006274) rotate(296.3 17 17)" - width="34" - x="0" - y="0" - /> - <rect - fill="#F26E02" - height="34" - transform="translate(19.34570196808791 -19.44479167700129) rotate(444.6 17 17)" - width="34" - x="0" - y="0" - /> - </svg> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--padding-left-3 mm-box--display-flex mm-box--justify-content-space-between mm-box--width-full" - style="min-width: 0;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--width-full" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-text--ellipsis mm-box--color-text-default" - style="text-wrap: nowrap;" - > - Account 1 (0xd5e09...81111) - </p> - <div - class="mm-box mm-box--display-flex" - > - <div - class="mm-box currency-display-component mm-box--display-flex mm-box--flex-wrap-wrap mm-box--align-items-center" - style="flex-wrap: nowrap;" - title="0 ETH" - > - <span - class="mm-box mm-text currency-display-component__text mm-text--body-sm mm-text--ellipsis mm-box--color-text-alternative" - > - 0 - </span> - <span - class="mm-box mm-text currency-display-component__suffix mm-text--body-sm mm-box--margin-inline-start-1 mm-box--color-text-alternative" - > - ETH - </span> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - <div - class="mm-box permissions-connect-choose-account__footer mm-box--padding-top-4 mm-box--background-color-background-alternative" - > - <div - class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" - > - <span> - - Only connect with sites you trust. - <button - class="mm-box mm-text mm-button-base mm-button-link mm-button-link--size-inherit mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" - target="_blank" - > - Learn more - </button> - - - </span> - </p> - </div> - <div - class="page-container__footer" - > - <footer> - <button - class="button btn--rounded btn-default page-container__footer-button page-container__footer-button__cancel" - data-testid="page-container-footer-cancel" - > - Cancel - </button> - <button - class="button btn--rounded btn-primary page-container__footer-button" - data-testid="page-container-footer-next" - > - Next - </button> - </footer> - </div> - </div> - </div> -</div> -`; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 09befa7218cd..417a82777b36 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -19,6 +19,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { PermissionNames } from '../../../app/scripts/controllers/permissions'; +import { isSnapId } from '../../helpers/utils/snaps'; import ChooseAccount from './choose-account'; import PermissionsRedirect from './redirect'; import SnapsConnect from './snaps/snaps-connect'; @@ -328,6 +329,8 @@ export default class PermissionConnect extends Component { snapsInstallPrivacyWarningShown, } = this.state; + const isRequestingSnap = isSnapId(permissionsRequest?.metadata?.origin); + return ( <div className="permissions-connect"> {!hideTopBar && this.renderTopBar(permissionsRequestId)} @@ -339,17 +342,7 @@ export default class PermissionConnect extends Component { path={connectPath} exact render={() => - process.env.CHAIN_PERMISSIONS ? ( - <ConnectPage - rejectPermissionsRequest={(requestId) => - this.cancelPermissionsRequest(requestId) - } - activeTabOrigin={this.state.origin} - request={permissionsRequest} - permissionsRequestId={permissionsRequestId} - approveConnection={this.approveConnection} - /> - ) : ( + isRequestingSnap ? ( <ChooseAccount accounts={accounts} nativeCurrency={nativeCurrency} @@ -371,6 +364,16 @@ export default class PermissionConnect extends Component { selectedAccountAddresses={selectedAccountAddresses} targetSubjectMetadata={targetSubjectMetadata} /> + ) : ( + <ConnectPage + rejectPermissionsRequest={(requestId) => + this.cancelPermissionsRequest(requestId) + } + activeTabOrigin={this.state.origin} + request={permissionsRequest} + permissionsRequestId={permissionsRequestId} + approveConnection={this.approveConnection} + /> ) } /> diff --git a/ui/pages/permissions-connect/permissions-connect.test.tsx b/ui/pages/permissions-connect/permissions-connect.test.tsx deleted file mode 100644 index 05b1120cf5d8..000000000000 --- a/ui/pages/permissions-connect/permissions-connect.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { ApprovalType } from '@metamask/controller-utils'; -import { BtcAccountType } from '@metamask/keyring-api'; -import { fireEvent } from '@testing-library/react'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import messages from '../../../app/_locales/en/messages.json'; -import { renderWithProvider } from '../../../test/lib/render-helpers'; -import mockState from '../../../test/data/mock-state.json'; -import { CONNECT_ROUTE } from '../../helpers/constants/routes'; -import { createMockInternalAccount } from '../../../test/jest/mocks'; -import { shortenAddress } from '../../helpers/utils/util'; -import PermissionApprovalContainer from './permissions-connect.container'; - -const mockPermissionRequestId = '0cbc1f26-8772-4512-8ad7-f547d6e8b72c'; - -jest.mock('../../store/actions', () => { - return { - ...jest.requireActual('../../store/actions'), - getRequestAccountTabIds: jest.fn().mockReturnValue({ - type: 'SET_REQUEST_ACCOUNT_TABS', - payload: {}, - }), - }; -}); - -const mockAccount = createMockInternalAccount({ name: 'Account 1' }); -const mockBtcAccount = createMockInternalAccount({ - name: 'BTC Account', - address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', - type: BtcAccountType.P2wpkh, -}); - -const defaultProps = { - history: { - location: { - pathname: `${CONNECT_ROUTE}/${mockPermissionRequestId}`, - }, - }, - location: { - pathname: `${CONNECT_ROUTE}/${mockPermissionRequestId}`, - }, - match: { - params: { - id: mockPermissionRequestId, - }, - }, -}; - -const render = ( - props = defaultProps, - type: ApprovalType = ApprovalType.WalletRequestPermissions, -) => { - let pendingPermission; - if (type === ApprovalType.WalletRequestPermissions) { - pendingPermission = { - id: mockPermissionRequestId, - origin: 'https://metamask.io', - type: ApprovalType.WalletRequestPermissions, - time: 1721376328642, - requestData: { - metadata: { - id: mockPermissionRequestId, - origin: 'https://metamask.io', - }, - permissions: { - eth_accounts: {}, - }, - }, - requestState: null, - expectsResult: false, - }; - } - - const state = { - ...mockState, - metamask: { - ...mockState.metamask, - internalAccounts: { - accounts: { - [mockAccount.id]: mockAccount, - [mockBtcAccount.id]: mockBtcAccount, - }, - selectedAccount: mockAccount.id, - }, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [mockAccount.address], - }, - { - type: 'Snap Keyring', - accounts: [mockBtcAccount.address], - }, - ], - accounts: { - [mockAccount.address]: { - address: mockAccount.address, - balance: '0x0', - }, - }, - balances: { - [mockBtcAccount.id]: {}, - }, - pendingApprovals: { - [mockPermissionRequestId]: pendingPermission, - }, - }, - }; - const middlewares = [thunk]; - const mockStore = configureStore(middlewares); - const store = mockStore(state); - - return { - render: renderWithProvider( - <PermissionApprovalContainer {...props} />, - store, - `${CONNECT_ROUTE}/${mockPermissionRequestId}`, - ), - store, - }; -}; - -describe('PermissionApprovalContainer', () => { - describe('ConnectPath', () => { - it('renders correctly', () => { - const { - render: { container, getByText }, - } = render(); - expect(getByText(messages.next.message)).toBeInTheDocument(); - expect(getByText(messages.cancel.message)).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the list without BTC accounts', async () => { - const { - render: { getByText, queryByText }, - } = render(); - expect( - getByText( - `${mockAccount.metadata.name} (${shortenAddress( - mockAccount.address, - )})`, - ), - ).toBeInTheDocument(); - expect( - queryByText( - `${mockBtcAccount.metadata.name} (${shortenAddress( - mockBtcAccount.address, - )})`, - ), - ).not.toBeInTheDocument(); - }); - }); - - describe('Add new account', () => { - it('displays the correct account number', async () => { - const { - render: { getByText }, - store, - } = render(); - fireEvent.click(getByText(messages.newAccount.message)); - - const dispatchedActions = store.getActions(); - - expect(dispatchedActions).toHaveLength(2); // first action is 'SET_REQUEST_ACCOUNT_TABS' - expect(dispatchedActions[1]).toStrictEqual({ - type: 'UI_MODAL_OPEN', - payload: { - name: 'NEW_ACCOUNT', - onCreateNewAccount: expect.any(Function), - newAccountNumber: 2, - }, - }); - }); - }); -}); diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 1fdbad27ed67..25c41ca37c82 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -787,7 +787,7 @@ export default class Routes extends Component { /> ) : null} - {process.env.CHAIN_PERMISSIONS && isPermittedNetworkToastOpen ? ( + {isPermittedNetworkToastOpen ? ( <Toast key="switched-permitted-network-toast" startAdornment={ From 86525fd7cf9cf246feb408e35b71beecd03a5b2c Mon Sep 17 00:00:00 2001 From: Mathieu Artu <mathieu.artu@consensys.net> Date: Thu, 10 Oct 2024 14:16:41 +0200 Subject: [PATCH 107/226] chore: bump profile-sync-controller to 0.9.7 (#27749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/profile-sync-controller` to version `0.9.7`. This version fixes an account sync bug where we would save imported accounts in user storage. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27749?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/NOTIFY-1215 ## **Manual testing steps** 1. Create a new SRP 2. Add new accounts, rename some 3. Uninstall extension and reinstall 4. Import your previously created SRP 5. All your previously created accounts and respective names should be there! ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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. --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 416b3e1b0420..d3f7cf42a1dc 100644 --- a/package.json +++ b/package.json @@ -344,7 +344,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", "@metamask/preinstalled-example-snap": "^0.1.0", - "@metamask/profile-sync-controller": "^0.9.6", + "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 1e00e14c6cf8..4b5ad861cc3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6066,9 +6066,9 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^0.9.6": - version: 0.9.6 - resolution: "@metamask/profile-sync-controller@npm:0.9.6" +"@metamask/profile-sync-controller@npm:^0.9.7": + version: 0.9.7 + resolution: "@metamask/profile-sync-controller@npm:0.9.7" dependencies: "@metamask/base-controller": "npm:^7.0.1" "@metamask/keyring-api": "npm:^8.1.3" @@ -6084,7 +6084,7 @@ __metadata: "@metamask/accounts-controller": ^18.1.1 "@metamask/keyring-controller": ^17.2.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/102572a8805dde33eb318bf87ff2cd14cd5d5eae9139f18641c72a166ffa42dd4365d7617407d98521f3ec5e9b1d46517b283742be32825faf276141413bab51 + checksum: 10/e53888533b2aae937bbe4e385dca2617c324b34e3e60af218cd98c26d514fb725f4c67b649f126e055f6a50a554817b229d37488115b98d70e8aee7b3a910bde languageName: node linkType: hard @@ -26149,7 +26149,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" "@metamask/preinstalled-example-snap": "npm:^0.1.0" - "@metamask/profile-sync-controller": "npm:^0.9.6" + "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" From 04ba878198df5f3d0af4c6b2dc0b0ce3da1db806 Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Thu, 10 Oct 2024 14:35:42 +0200 Subject: [PATCH 108/226] fix(btc): fix jazzicons generations (#27662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The jazzicons were all the same for mainnet/testnet accounts. It was probably due to the fact that the namespace being used was `eip155` for all addresses, but Bitcoin addresses have a different format. Here's the technical details: 1. The current "icon factory" being in used is the ethereum one: - https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/ui/jazzicon/jazzicon.component.tsx#L25 - https://github.com/MetaMask/metamask-extension/blob/develop/ui/components/ui/jazzicon/jazzicon.component.tsx#L64 - `namespace` always defaults to `eip155` 2. The default constructor used for the ethereum factory uses the `jsNumberForAddress` which is ethereum-specific (or more like, "hex-specific" here): - https://github.com/MetaMask/metamask-extension/blob/develop/ui/helpers/utils/icon-factory.ts#L40 - https://github.com/MetaMask/metamask-extension/blob/develop/ui/helpers/utils/icon-factory.ts#L150-L154 - It slices the first 2 characters (probably to remove the `0x` prefix) + `parseInt(addr, 16)` will only work hex-strings, but Bitcoin is not using this format - the `parseInt` here will only consider the first valid hex-characters of its input To fix this, we check for the current address used for the jazzicon and change the namespace based on this. Ideally, we would want to use the `InternalAccount` object directly, but that would require quite a lot of changes, so for now we keep this simple. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27662?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** 1. `yarn start:flask` 2. Settings > Experimental > "Enable Bitcoin support" 3. Create a Bitcoin mainnet account 4. Create a Bitcoin testnet account 5. Check that both jazzicons are different for those 2 Bitcoin accounts 6. Remove your Bitcoin accounts 7. Re-create them 8. Re-check that jazzicons are the same than step 5 9. "Hard"-restart your extension 10. Re-check that jazzicons are the same than step 5 ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-07 at 16 17 35](https://github.com/user-attachments/assets/0a2e28e8-a81d-4468-9261-f1b0c8c3f02d) ### **After** ![Screenshot 2024-10-07 at 16 18 57](https://github.com/user-attachments/assets/81d378c9-6941-4067-9022-660e7dffb7b2) ## **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/multichain.test.ts | 105 ++++++++++++------ shared/lib/multichain.ts | 15 +++ .../account-list-item.test.js.snap | 56 +++++----- .../ui/jazzicon/jazzicon.component.tsx | 11 +- yarn.lock | 10 +- 6 files changed, 130 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index d3f7cf42a1dc..fad9fae96418 100644 --- a/package.json +++ b/package.json @@ -361,7 +361,7 @@ "@metamask/snaps-utils": "^8.1.1", "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", - "@metamask/utils": "^9.1.0", + "@metamask/utils": "^9.3.0", "@ngraveio/bc-ur": "^1.1.12", "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.4.0", diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts index 3b982ff8aff3..4c1bab12d03b 100644 --- a/shared/lib/multichain.test.ts +++ b/shared/lib/multichain.test.ts @@ -1,4 +1,9 @@ -import { isBtcMainnetAddress, isBtcTestnetAddress } from './multichain'; +import { KnownCaipNamespace } from '@metamask/utils'; +import { + getCaipNamespaceFromAddress, + isBtcMainnetAddress, + isBtcTestnetAddress, +} from './multichain'; const BTC_MAINNET_ADDRESSES = [ // P2WPKH @@ -20,35 +25,71 @@ const SOL_ADDRESSES = [ ]; describe('multichain', () => { - // @ts-expect-error This is missing from the Mocha type definitions - it.each(BTC_MAINNET_ADDRESSES)( - 'returns true if address is compatible with BTC mainnet: %s', - (address: string) => { - expect(isBtcMainnetAddress(address)).toBe(true); - }, - ); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each([...BTC_TESTNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( - 'returns false if address is not compatible with BTC mainnet: %s', - (address: string) => { - expect(isBtcMainnetAddress(address)).toBe(false); - }, - ); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each(BTC_TESTNET_ADDRESSES)( - 'returns true if address is compatible with BTC testnet: %s', - (address: string) => { - expect(isBtcTestnetAddress(address)).toBe(true); - }, - ); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each([...BTC_MAINNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( - 'returns false if address is compatible with BTC testnet: %s', - (address: string) => { - expect(isBtcTestnetAddress(address)).toBe(false); - }, - ); + describe('isBtcMainnetAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(BTC_MAINNET_ADDRESSES)( + 'returns true if address is compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...BTC_TESTNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( + 'returns false if address is not compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(false); + }, + ); + }); + + describe('isBtcTestnetAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(BTC_TESTNET_ADDRESSES)( + 'returns true if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...BTC_MAINNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( + 'returns false if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(false); + }, + ); + }); + + describe('getChainTypeFromAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...BTC_MAINNET_ADDRESSES, ...BTC_TESTNET_ADDRESSES])( + 'returns ChainType.Bitcoin for bitcoin address: %s', + (address: string) => { + expect(getCaipNamespaceFromAddress(address)).toBe( + KnownCaipNamespace.Bip122, + ); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(ETH_ADDRESSES)( + 'returns ChainType.Ethereum for ethereum address: %s', + (address: string) => { + expect(getCaipNamespaceFromAddress(address)).toBe( + KnownCaipNamespace.Eip155, + ); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(SOL_ADDRESSES)( + 'returns ChainType.Ethereum for non-supported address: %s', + (address: string) => { + expect(getCaipNamespaceFromAddress(address)).toBe( + KnownCaipNamespace.Eip155, + ); + }, + ); + }); }); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts index 8ef03509541b..942a9ce6c964 100644 --- a/shared/lib/multichain.ts +++ b/shared/lib/multichain.ts @@ -1,3 +1,4 @@ +import { CaipNamespace, KnownCaipNamespace } from '@metamask/utils'; import { validate, Network } from 'bitcoin-address-validation'; /** @@ -26,3 +27,17 @@ export function isBtcMainnetAddress(address: string): boolean { export function isBtcTestnetAddress(address: string): boolean { return validate(address, Network.testnet); } + +/** + * Returns the associated chain's type for the given address. + * + * @param address - The address to check. + * @returns The chain's type for that address. + */ +export function getCaipNamespaceFromAddress(address: string): CaipNamespace { + if (isBtcMainnetAddress(address) || isBtcTestnetAddress(address)) { + return KnownCaipNamespace.Bip122; + } + // Defaults to "Ethereum" for all other cases for now. + return KnownCaipNamespace.Eip155; +} diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap index c14fb8a0c42d..51f6f2e905f9 100644 --- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -32,7 +32,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam class="mm-avatar-account__jazzicon" > <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(24, 162, 242);" + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(200, 20, 47);" > <svg height="32" @@ -41,25 +41,25 @@ exports[`AccountListItem renders AccountListItem component and shows account nam y="0" > <rect - fill="#F29602" + fill="#F2C602" height="32" - transform="translate(0.001112151990700775 -0.0005016734084641463) rotate(358.8 16 16)" + transform="translate(5.020620447504322 0.8103948904236289) rotate(161.5 16 16)" width="32" x="0" y="0" /> <rect - fill="#FA6C00" + fill="#F5ED00" height="32" - transform="translate(7.965719514969553 10.506673824525246) rotate(69.5 16 16)" + transform="translate(-9.408121353992403 -10.305042584072448) rotate(246.0 16 16)" width="32" x="0" y="0" /> <rect - fill="#236CE1" + fill="#FB183A" height="32" - transform="translate(-19.066706584002095 16.199592375372838) rotate(260.2 16 16)" + transform="translate(9.192555269879563 26.914160820887155) rotate(215.5 16 16)" width="32" x="0" y="0" @@ -75,7 +75,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam class="mm-avatar-account__jazzicon" > <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(24, 162, 242);" + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(200, 20, 47);" > <svg height="32" @@ -84,25 +84,25 @@ exports[`AccountListItem renders AccountListItem component and shows account nam y="0" > <rect - fill="#F29602" + fill="#F2C602" height="32" - transform="translate(0.001112151990700775 -0.0005016734084641463) rotate(358.8 16 16)" + transform="translate(5.020620447504322 0.8103948904236289) rotate(161.5 16 16)" width="32" x="0" y="0" /> <rect - fill="#FA6C00" + fill="#F5ED00" height="32" - transform="translate(7.965719514969553 10.506673824525246) rotate(69.5 16 16)" + transform="translate(-9.408121353992403 -10.305042584072448) rotate(246.0 16 16)" width="32" x="0" y="0" /> <rect - fill="#236CE1" + fill="#FB183A" height="32" - transform="translate(-19.066706584002095 16.199592375372838) rotate(260.2 16 16)" + transform="translate(9.192555269879563 26.914160820887155) rotate(215.5 16 16)" width="32" x="0" y="0" @@ -134,7 +134,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam class="mm-avatar-account__jazzicon" > <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(24, 162, 242);" + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(200, 20, 47);" > <svg height="32" @@ -143,25 +143,25 @@ exports[`AccountListItem renders AccountListItem component and shows account nam y="0" > <rect - fill="#F29602" + fill="#F2C602" height="32" - transform="translate(0.001112151990700775 -0.0005016734084641463) rotate(358.8 16 16)" + transform="translate(5.020620447504322 0.8103948904236289) rotate(161.5 16 16)" width="32" x="0" y="0" /> <rect - fill="#FA6C00" + fill="#F5ED00" height="32" - transform="translate(7.965719514969553 10.506673824525246) rotate(69.5 16 16)" + transform="translate(-9.408121353992403 -10.305042584072448) rotate(246.0 16 16)" width="32" x="0" y="0" /> <rect - fill="#236CE1" + fill="#FB183A" height="32" - transform="translate(-19.066706584002095 16.199592375372838) rotate(260.2 16 16)" + transform="translate(9.192555269879563 26.914160820887155) rotate(215.5 16 16)" width="32" x="0" y="0" @@ -177,7 +177,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam class="mm-avatar-account__jazzicon" > <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(24, 162, 242);" + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(200, 20, 47);" > <svg height="32" @@ -186,25 +186,25 @@ exports[`AccountListItem renders AccountListItem component and shows account nam y="0" > <rect - fill="#F29602" + fill="#F2C602" height="32" - transform="translate(0.001112151990700775 -0.0005016734084641463) rotate(358.8 16 16)" + transform="translate(5.020620447504322 0.8103948904236289) rotate(161.5 16 16)" width="32" x="0" y="0" /> <rect - fill="#FA6C00" + fill="#F5ED00" height="32" - transform="translate(7.965719514969553 10.506673824525246) rotate(69.5 16 16)" + transform="translate(-9.408121353992403 -10.305042584072448) rotate(246.0 16 16)" width="32" x="0" y="0" /> <rect - fill="#236CE1" + fill="#FB183A" height="32" - transform="translate(-19.066706584002095 16.199592375372838) rotate(260.2 16 16)" + transform="translate(9.192555269879563 26.914160820887155) rotate(215.5 16 16)" width="32" x="0" y="0" diff --git a/ui/components/ui/jazzicon/jazzicon.component.tsx b/ui/components/ui/jazzicon/jazzicon.component.tsx index f014d321ecc4..c32789740f36 100644 --- a/ui/components/ui/jazzicon/jazzicon.component.tsx +++ b/ui/components/ui/jazzicon/jazzicon.component.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useRef } from 'react'; import jazzicon from '@metamask/jazzicon'; -import { stringToBytes } from '@metamask/utils'; +import { KnownCaipNamespace, stringToBytes } from '@metamask/utils'; import iconFactoryGenerator, { IconFactory, } from '../../../helpers/utils/icon-factory'; +import { getCaipNamespaceFromAddress } from '../../../../shared/lib/multichain'; /** * Generates a seed for Jazzicon based on the provided address. @@ -43,7 +44,7 @@ function Jazzicon({ diameter = 46, style, tokenList = {}, - namespace = 'eip155', + namespace: namespace_, }: { address: string; className?: string; @@ -60,8 +61,12 @@ function Jazzicon({ return; } + // If the address is unknown, `getCaipNamespaceFromAddress` will defaults to "eip155". + const namespace = namespace_ ?? getCaipNamespaceFromAddress(address); const iconFactory = - namespace === 'eip155' ? ethereumIconFactory : multichainIconFactory; + namespace === KnownCaipNamespace.Eip155 + ? ethereumIconFactory + : multichainIconFactory; const imageNode = iconFactory.iconForAddress( address, diff --git a/yarn.lock b/yarn.lock index 4b5ad861cc3d..f2992051fdf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6580,9 +6580,9 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1": - version: 9.2.1 - resolution: "@metamask/utils@npm:9.2.1" +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1, @metamask/utils@npm:^9.3.0": + version: 9.3.0 + resolution: "@metamask/utils@npm:9.3.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -6593,7 +6593,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/2192797afd91af19898e107afeaf63e89b61dc7285e0a75d0cc814b5b288e4cdfc856781b01904034c4d2c1efd9bdab512af24c7e4dfe7b77a03f1f3d9dec7e8 + checksum: 10/ed6648cd973bbf3b4eb0e862903b795a99d27784c820e19f62f0bc0ddf353e98c2858d7e9aaebc0249a586391b344e35b9249d13c08e3ea0c74b23dc1c6b1558 languageName: node linkType: hard @@ -26168,7 +26168,7 @@ __metadata: "@metamask/test-dapp": "npm:^8.4.0" "@metamask/transaction-controller": "npm:^37.2.0" "@metamask/user-operation-controller": "npm:^13.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^9.3.0" "@ngraveio/bc-ur": "npm:^1.1.12" "@noble/hashes": "npm:^1.3.3" "@octokit/core": "npm:^3.6.0" From 68dd6f55a9852354c1a9d8c697f43066627ca448 Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Thu, 10 Oct 2024 05:43:27 -0700 Subject: [PATCH 109/226] feat: add network picker to AssetPicker (#26559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Changes included in this PR: * Add a network picker to the AssetPicker modal component so that it can be reused within the cross-chain swaps experience * Update AssetPicker components to enable displaying network and asset data when the selected network is not the same as the wallet's active network. Example usecase: destination asset for cross-chain swaps. Specifically: - when selected `network` is not the same as wallet's network, display - selected network's icons in asset list - selected network's native token icons - add `customTokenListGenerator` prop to AssetPicker that allows upstream components to override the default displayed token list Figma design: https://www.figma.com/design/bC6RgeriyERMtMlZE8xwkm/Cross-Chain-Swaps?node-id=1490-18690&t=pnpoVVaJTqh15I0a-0 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26559?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/METABRIDGE-866 ## **Manual testing steps** 1. Swap+Send asset selection experience should not change 2. Storybook should show an AssetPicker variation that has a network picker ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> <img width="409" alt="Screenshot 2024-08-21 at 2 10 06 PM" src="https://github.com/user-attachments/assets/4a5999ae-711c-4d33-80b6-5422b5756f6c"> ### **After** <!-- [screenshots/recordings] --> ![Screenshot 2024-10-02 at 4 09 51 PM](https://github.com/user-attachments/assets/ff3f2854-7120-4f3d-9e55-88a325a5b3db)<img width="409" alt="Screenshot 2024-08-21 at 2 09 04 PM" src="https://github.com/user-attachments/assets/206367fb-3551-491c-a223-06553583c40d"> ## **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 | 3 + .../multichain/asset-picker-send.spec.ts | 4 +- .../asset-picker-modal/Asset.test.tsx | 5 +- .../asset-picker-modal/Asset.tsx | 14 +- .../asset-picker-modal/AssetList.tsx | 48 +-- .../asset-picker-modal-network.test.tsx.snap | 392 ++++++++++++++++++ .../asset-picker-modal-network.test.tsx | 132 ++++++ .../asset-picker-modal-network.tsx | 111 +++++ .../asset-picker-modal.test.tsx | 65 +++ .../asset-picker-modal/asset-picker-modal.tsx | 183 +++++--- .../asset-picker-modal/index.scss | 19 + .../__snapshots__/asset-picker.test.tsx.snap | 72 ++++ .../asset-picker/asset-picker.stories.tsx | 91 +++- .../asset-picker/asset-picker.test.tsx | 96 ++++- .../asset-picker/asset-picker.tsx | 168 +++++--- .../asset-picker/index.scss | 5 + .../token-list-item/token-list-item.tsx | 12 +- 17 files changed, 1251 insertions(+), 169 deletions(-) create mode 100644 ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal-network.test.tsx.snap create mode 100644 ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.test.tsx create mode 100644 ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ecaedb3201d0..5f6a977c1cf4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -870,6 +870,9 @@ "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeSelectNetwork": { + "message": "Select network" + }, "browserNotSupported": { "message": "Your browser is not supported..." }, diff --git a/test/e2e/tests/multichain/asset-picker-send.spec.ts b/test/e2e/tests/multichain/asset-picker-send.spec.ts index 5accb14c6074..a071bec9426d 100644 --- a/test/e2e/tests/multichain/asset-picker-send.spec.ts +++ b/test/e2e/tests/multichain/asset-picker-send.spec.ts @@ -71,7 +71,7 @@ describe('AssetPickerSendFlow @no-mmi', function () { ) ).getText(); - assert.equal(tokenListValue, '25 ETH'); + assert.equal(tokenListValue, '$250,000.00'); const tokenListSecondaryValue = await ( await driver.findElement( @@ -79,7 +79,7 @@ describe('AssetPickerSendFlow @no-mmi', function () { ) ).getText(); - assert.equal(tokenListSecondaryValue, '$250,000.00'); + assert.equal(tokenListSecondaryValue, '25 ETH'); // Search for CHZ const searchInputField = await driver.waitForSelector( diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.test.tsx index f35bc8196724..0b641101c5dd 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.test.tsx @@ -77,10 +77,11 @@ describe('Asset', () => { expect.objectContaining({ tokenSymbol: 'WETH', tokenImage: 'token-icon-url', - primary: '10', - secondary: '$10.10', + primary: '$10.10', + secondary: '10 WETH', title: 'Token', tooltipText: 'tooltip', + isPrimaryTokenSymbolHidden: true, }), {}, ); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.tsx index 83229689f055..f384ef8fd96f 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/Asset.tsx @@ -40,18 +40,22 @@ export default function Asset({ {}, true, ); + const formattedAmount = decimalTokenAmount + ? `${formatAmount( + locale, + new BigNumber(decimalTokenAmount || '0', 10), + )} ${symbol}` + : undefined; return ( <TokenListItem tokenSymbol={symbol} tokenImage={tokenImage} - primary={formatAmount( - locale, - new BigNumber(decimalTokenAmount || '0', 10), - )} - secondary={formattedFiat} + secondary={formattedAmount} + primary={formattedFiat} title={title} tooltipText={tooltipText} + isPrimaryTokenSymbolHidden /> ); } diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx index 9061592cf37c..fa071740b51d 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; -import { getSelectedAccountCachedBalance } from '../../../../selectors'; +import { + getCurrentCurrency, + getSelectedAccountCachedBalance, +} from '../../../../selectors'; import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; -import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; -import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; import { AssetType } from '../../../../../shared/constants/transaction'; import { Box } from '../../../component-library'; @@ -43,28 +44,16 @@ export default function AssetList({ const nativeCurrency = useSelector(getNativeCurrency); const balanceValue = useSelector(getSelectedAccountCachedBalance); + const currentCurrency = useSelector(getCurrentCurrency); - const { - currency: primaryCurrency, - numberOfDecimals: primaryNumberOfDecimals, - } = useUserPreferencedCurrency(PRIMARY, { ethNumberOfDecimals: 4 }); - - const { - currency: secondaryCurrency, - numberOfDecimals: secondaryNumberOfDecimals, - } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }); - - const [, primaryCurrencyProperties] = useCurrencyDisplay(balanceValue, { - numberOfDecimals: primaryNumberOfDecimals, - currency: primaryCurrency, + const [primaryCurrencyValue] = useCurrencyDisplay(balanceValue, { + currency: currentCurrency, + hideLabel: true, }); - const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = - useCurrencyDisplay(balanceValue, { - numberOfDecimals: secondaryNumberOfDecimals, - currency: secondaryCurrency, - hideLabel: true, - }); + const [secondaryCurrencyValue] = useCurrencyDisplay(balanceValue, { + currency: nativeCurrency, + }); return ( <Box className="tokens-main-view-modal"> @@ -72,6 +61,7 @@ export default function AssetList({ const tokenAddress = token.address?.toLowerCase(); const isSelected = tokenAddress === selectedToken?.toLowerCase(); const isDisabled = isTokenDisabled?.(token) ?? false; + return ( <Box padding={0} @@ -112,15 +102,13 @@ export default function AssetList({ <Box marginInlineStart={2}> {token.type === AssetType.native ? ( <TokenListItem - title={nativeCurrency} - primary={ - primaryCurrencyProperties.value ?? - secondaryCurrencyProperties.value - } - tokenSymbol={primaryCurrency} - secondary={secondaryCurrencyDisplay} + title={token.symbol} + primary={primaryCurrencyValue} + tokenSymbol={token.symbol} + secondary={secondaryCurrencyValue} tokenImage={token.image} - isOriginalTokenSymbol + isOriginalTokenSymbol={token.symbol === nativeCurrency} + isPrimaryTokenSymbolHidden /> ) : ( <AssetComponent diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal-network.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal-network.test.tsx.snap new file mode 100644 index 000000000000..c51245502300 --- /dev/null +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal-network.test.tsx.snap @@ -0,0 +1,392 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssetPickerModalNetwork renders modal with no network list by default 1`] = ` +<body> + <div + id="popover-content" + /> + <div /> + <div + class="mm-modal multichain-asset-picker__network-modal" + > + <div + aria-hidden="true" + class="mm-box mm-modal-overlay mm-box--width-full mm-box--height-full mm-box--background-color-overlay-default" + /> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + data-focus-lock-disabled="false" + > + <div + class="mm-box mm-modal-content mm-box--padding-top-4 mm-box--sm:padding-top-8 mm-box--md:padding-top-12 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--sm:padding-bottom-8 mm-box--md:padding-bottom-12 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-center mm-box--align-items-flex-start mm-box--width-screen mm-box--height-screen" + > + <section + aria-modal="true" + class="mm-box mm-modal-content__dialog mm-modal-content__dialog--size-sm mm-box--padding-0 mm-box--padding-top-4 mm-box--padding-bottom-4 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--background-color-background-default mm-box--rounded-lg" + role="dialog" + > + <header + class="mm-box mm-header-base mm-modal-header mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-space-between" + > + <div + class="mm-box" + style="min-width: 0px;" + > + <button + aria-label="Back" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/arrow-left.svg');" + /> + </button> + </div> + <div + class="mm-box mm-box--width-full" + > + <h4 + class="mm-box mm-text mm-text--heading-sm mm-text--text-align-center mm-box--color-text-default" + > + Select network + </h4> + </div> + <div + class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" + style="min-width: 0px;" + > + <button + aria-label="Close" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/close.svg');" + /> + </button> + </div> + </header> + <div + class="mm-box multichain-asset-picker__network-list" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--height-full" + style="grid-column-start: 1; grid-column-end: 3;" + /> + </div> + </section> + </div> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + </div> +</body> +`; + +exports[`AssetPickerModalNetwork should not show selected network when network prop is not passed in 1`] = ` +<body> + <div + id="popover-content" + /> + <div /> + <div + class="mm-modal multichain-asset-picker__network-modal" + > + <div + aria-hidden="true" + class="mm-box mm-modal-overlay mm-box--width-full mm-box--height-full mm-box--background-color-overlay-default" + /> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + data-focus-lock-disabled="false" + > + <div + class="mm-box mm-modal-content mm-box--padding-top-4 mm-box--sm:padding-top-8 mm-box--md:padding-top-12 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--sm:padding-bottom-8 mm-box--md:padding-bottom-12 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-center mm-box--align-items-flex-start mm-box--width-screen mm-box--height-screen" + > + <section + aria-modal="true" + class="mm-box mm-modal-content__dialog mm-modal-content__dialog--size-sm mm-box--padding-0 mm-box--padding-top-4 mm-box--padding-bottom-4 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--background-color-background-default mm-box--rounded-lg" + role="dialog" + > + <header + class="mm-box mm-header-base mm-modal-header mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-space-between" + > + <div + class="mm-box" + style="min-width: 0px;" + > + <button + aria-label="Back" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/arrow-left.svg');" + /> + </button> + </div> + <div + class="mm-box mm-box--width-full" + > + <h4 + class="mm-box mm-text mm-text--heading-sm mm-text--text-align-center mm-box--color-text-default" + > + Select network + </h4> + </div> + <div + class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" + style="min-width: 0px;" + > + <button + aria-label="Close" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/close.svg');" + /> + </button> + </div> + </header> + <div + class="mm-box multichain-asset-picker__network-list" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--height-full" + style="grid-column-start: 1; grid-column-end: 3;" + > + <div + class="mm-box multichain-network-list-item mm-box--padding-top-4 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--gap-4 mm-box--justify-content-space-between mm-box--align-items-center mm-box--width-full mm-box--background-color-transparent" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" + > + <img + alt="Network name 3 logo" + class="mm-avatar-network__network-image" + src="./images/eth_logo.svg" + /> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-flex-start mm-box--align-items-flex-start mm-box--width-full" + style="overflow: hidden;" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center mm-box--width-full" + data-testid="Network name 3" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default mm-box--background-color-transparent" + tabindex="0" + > + Network name 3 + </p> + </div> + </div> + </div> + <div + class="mm-box multichain-network-list-item mm-box--padding-top-4 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--gap-4 mm-box--justify-content-space-between mm-box--align-items-center mm-box--width-full mm-box--background-color-transparent" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" + > + <img + alt="Network name 4 logo" + class="mm-avatar-network__network-image" + src="./images/optimism.svg" + /> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-flex-start mm-box--align-items-flex-start mm-box--width-full" + style="overflow: hidden;" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center mm-box--width-full" + data-testid="Network name 4" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default mm-box--background-color-transparent" + tabindex="0" + > + Network name 4 + </p> + </div> + </div> + </div> + </div> + </div> + </section> + </div> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + </div> +</body> +`; + +exports[`AssetPickerModalNetwork should use passed in network as default when network prop is passed in 1`] = ` +<body> + <div + id="popover-content" + /> + <div /> + <div + class="mm-modal multichain-asset-picker__network-modal" + > + <div + aria-hidden="true" + class="mm-box mm-modal-overlay mm-box--width-full mm-box--height-full mm-box--background-color-overlay-default" + /> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + data-focus-lock-disabled="false" + > + <div + class="mm-box mm-modal-content mm-box--padding-top-4 mm-box--sm:padding-top-8 mm-box--md:padding-top-12 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--sm:padding-bottom-8 mm-box--md:padding-bottom-12 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-center mm-box--align-items-flex-start mm-box--width-screen mm-box--height-screen" + > + <section + aria-modal="true" + class="mm-box mm-modal-content__dialog mm-modal-content__dialog--size-sm mm-box--padding-0 mm-box--padding-top-4 mm-box--padding-bottom-4 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--background-color-background-default mm-box--rounded-lg" + role="dialog" + > + <header + class="mm-box mm-header-base mm-modal-header mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-space-between" + > + <div + class="mm-box" + style="min-width: 0px;" + > + <button + aria-label="Back" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/arrow-left.svg');" + /> + </button> + </div> + <div + class="mm-box mm-box--width-full" + > + <h4 + class="mm-box mm-text mm-text--heading-sm mm-text--text-align-center mm-box--color-text-default" + > + Select network + </h4> + </div> + <div + class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" + style="min-width: 0px;" + > + <button + aria-label="Close" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/close.svg');" + /> + </button> + </div> + </header> + <div + class="mm-box multichain-asset-picker__network-list" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--height-full" + style="grid-column-start: 1; grid-column-end: 3;" + > + <div + class="mm-box multichain-network-list-item multichain-network-list-item--selected mm-box--padding-top-4 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--gap-4 mm-box--justify-content-space-between mm-box--align-items-center mm-box--width-full mm-box--background-color-primary-muted" + > + <div + class="mm-box multichain-network-list-item__selected-indicator mm-box--background-color-primary-default mm-box--rounded-pill" + /> + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" + > + <img + alt="Network name 3 logo" + class="mm-avatar-network__network-image" + src="./images/eth_logo.svg" + /> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-flex-start mm-box--align-items-flex-start mm-box--width-full" + style="overflow: hidden;" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center mm-box--width-full" + data-testid="Network name 3" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default mm-box--background-color-transparent" + tabindex="0" + > + Network name 3 + </p> + </div> + </div> + </div> + <div + class="mm-box multichain-network-list-item mm-box--padding-top-4 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--gap-4 mm-box--justify-content-space-between mm-box--align-items-center mm-box--width-full mm-box--background-color-transparent" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" + > + <img + alt="Network name 4 logo" + class="mm-avatar-network__network-image" + src="./images/optimism.svg" + /> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-flex-start mm-box--align-items-flex-start mm-box--width-full" + style="overflow: hidden;" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center mm-box--width-full" + data-testid="Network name 4" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default mm-box--background-color-transparent" + tabindex="0" + > + Network name 4 + </p> + </div> + </div> + </div> + </div> + </div> + </section> + </div> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + </div> +</body> +`; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.test.tsx new file mode 100644 index 000000000000..3fc1e8cf7952 --- /dev/null +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.test.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { screen, fireEvent } from '@testing-library/react'; +import { RpcEndpointType } from '@metamask/network-controller'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import mockState from '../../../../../test/data/mock-send-state.json'; +import { AssetPickerModalNetwork } from './asset-picker-modal-network'; + +const mockOnClose = jest.fn(); +const mockOnNetworkChange = jest.fn(); +const mockOnBack = jest.fn(); + +describe('AssetPickerModalNetwork', () => { + const mockStore = configureStore([thunk]); + const store = mockStore(mockState); + + const defaultProps = { + isOpen: true, + onClose: mockOnClose, + onBack: mockOnBack, + network: undefined, + networks: [], + onNetworkChange: mockOnNetworkChange, + }; + + const networkProps = { + network: { + chainId: '0x1', + nativeCurrency: 'ETH', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'network', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + networks: [ + { + chainId: '0x1', + nativeCurrency: 'ETH', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'Network name 3', + }, + { + chainId: '0xa', + nativeCurrency: 'ETH', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test2', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'Network name 4', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders modal with no network list by default', () => { + const { baseElement } = renderWithProvider( + <AssetPickerModalNetwork {...defaultProps} />, + store, + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it('should not show selected network when network prop is not passed in', () => { + const { baseElement } = renderWithProvider( + <AssetPickerModalNetwork + {...defaultProps} + networks={networkProps.networks} + />, + store, + ); + expect(baseElement).toMatchSnapshot(); + }); + + it('should use passed in network as default when network prop is passed in', () => { + const { baseElement } = renderWithProvider( + <AssetPickerModalNetwork {...defaultProps} {...networkProps} />, + store, + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it('should call onClose and onBack when header buttons are clicked', () => { + renderWithProvider(<AssetPickerModalNetwork {...defaultProps} />, store); + + fireEvent.click(screen.getByLabelText('Close')); + expect(mockOnClose).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByLabelText('Back')); + expect(mockOnBack).toHaveBeenCalledTimes(1); + }); + + it('should call onBack and onClickHandler when network is selected', () => { + renderWithProvider( + <AssetPickerModalNetwork {...defaultProps} {...networkProps} />, + store, + ); + + fireEvent.click(screen.getByText('Network name 3')); + expect(mockOnBack).toHaveBeenCalledTimes(1); + expect(mockOnNetworkChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx new file mode 100644 index 000000000000..d674fbef528e --- /dev/null +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx @@ -0,0 +1,111 @@ +import React from 'react'; + +import { useSelector } from 'react-redux'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { + Display, + FlexDirection, + BlockSize, +} from '../../../../helpers/constants/design-system'; +import { + ModalOverlay, + ModalContent, + ModalHeader, + Modal, + Box, +} from '../../../component-library'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { useI18nContext } from '../../../../hooks/useI18nContext'; +///: END:ONLY_INCLUDE_IF +import { NetworkListItem } from '../../network-list-item'; +import { getNetworkConfigurationsByChainId } from '../../../../selectors'; +import { getProviderConfig } from '../../../../ducks/metamask/metamask'; + +/** + * AssetPickerModalNetwork component displays a modal for selecting a network in the asset picker. + * + * @param props + * @param props.isOpen - Determines whether the modal is open or not. + * @param props.network - The currently selected network, not necessarily the active wallet network. + * @param props.networks - The list of selectable networks. + * @param props.onNetworkChange - The callback function to handle network change. + * @param props.onClose - The callback function to handle modal close. + * @param props.onBack - The callback function to handle going back in the modal. + * @returns A modal with a list of selectable networks. + */ +export const AssetPickerModalNetwork = ({ + isOpen, + onClose, + onBack, + network, + networks, + onNetworkChange, +}: { + isOpen: boolean; + network?: NetworkConfiguration; + networks?: NetworkConfiguration[]; + onNetworkChange: (network: NetworkConfiguration) => void; + onClose: () => void; + onBack: () => void; +}) => { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const t = useI18nContext(); + ///: END:ONLY_INCLUDE_IF + + const currentNetwork = useSelector(getProviderConfig); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + + const selectedNetwork = + network ?? (currentNetwork?.chainId && allNetworks[currentNetwork.chainId]); + + const networksList: NetworkConfiguration[] = + networks ?? Object.values(allNetworks) ?? []; + + return ( + <Modal + isOpen={isOpen} + onClose={onClose} + className="multichain-asset-picker__network-modal" + > + <ModalOverlay /> + <ModalContent modalDialogProps={{ padding: 0 }}> + <ModalHeader onBack={onBack} onClose={onClose}> + {t('bridgeSelectNetwork')} + </ModalHeader> + <Box className="multichain-asset-picker__network-list"> + <Box + style={{ + gridColumnStart: 1, + gridColumnEnd: 3, + }} + display={Display.Flex} + flexDirection={FlexDirection.Column} + height={BlockSize.Full} + > + {networksList.map((networkConfig) => { + const { name, chainId } = networkConfig; + return ( + <NetworkListItem + key={chainId} + name={name} + selected={selectedNetwork?.chainId === chainId} + onClick={() => { + onNetworkChange(networkConfig); + onBack(); + }} + iconSrc={ + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ] + } + focus={false} + /> + ); + })} + </Box> + </Box> + </ModalContent> + </Modal> + ); +}; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index 15cc339775c9..566783abed11 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -4,6 +4,7 @@ import configureStore from 'redux-mock-store'; import { useSelector } from 'react-redux'; import thunk from 'redux-thunk'; import sinon from 'sinon'; +import { RpcEndpointType } from '@metamask/network-controller'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useNftsCollections } from '../../../../hooks/useNftsCollections'; import { useTokenTracker } from '../../../../hooks/useTokenTracker'; @@ -69,6 +70,7 @@ describe('AssetPickerModal', () => { const defaultProps = { header: 'sendSelectReceiveAsset', + onNetworkPickerClick: jest.fn(), isOpen: true, onClose: onCloseMock, asset: { @@ -291,4 +293,67 @@ describe('AssetPickerModal', () => { }), ).toBe(true); }); + + it('should render network picker when onNetworkPickerClick prop is defined', () => { + const { getByText, getAllByRole } = renderWithProvider( + <AssetPickerModal + {...defaultProps} + header="selectNetworkHeader" + network={{ + nativeCurrency: 'ETH', + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'Network name', + }} + />, + store, + ); + + const modalTitle = getByText('selectNetworkHeader'); + expect(modalTitle).toBeInTheDocument(); + + expect(getAllByRole('img')).toHaveLength(2); + const modalContent = getByText('Network name'); + expect(modalContent).toBeInTheDocument(); + }); + + it('should not render network picker when onNetworkPickerClick prop is not defined', () => { + const { getByText, getAllByRole } = renderWithProvider( + <AssetPickerModal + {...defaultProps} + onNetworkPickerClick={undefined} + header="selectNetworkHeader" + network={{ + nativeCurrency: 'ETH', + chainId: '0x1', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'Network name', + }} + />, + store, + ); + + const modalTitle = getByText('selectNetworkHeader'); + expect(modalTitle).toBeInTheDocument(); + + expect(getAllByRole('img')).toHaveLength(1); + }); }); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index 0d9e01627878..d9a1a3a08588 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -17,6 +17,7 @@ import { AvatarTokenSize, AvatarToken, Text, + PickerNetwork, } from '../../../component-library'; import { BorderRadius, @@ -47,9 +48,9 @@ import { import { useTokenTracker } from '../../../../hooks/useTokenTracker'; import { getTopAssets } from '../../../../ducks/swaps/swaps'; import { getRenderableTokenData } from '../../../../hooks/useTokensToSearch'; -import { useEqualityCheck } from '../../../../hooks/useEqualityCheck'; import { getSwapsBlockedTokens } from '../../../../ducks/send'; import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network'; import { ERC20Asset, NativeAsset, @@ -61,6 +62,7 @@ import { AssetPickerModalTabs, TabName } from './asset-picker-modal-tabs'; import { AssetPickerModalNftTab } from './asset-picker-modal-nft-tab'; import AssetList from './AssetList'; import { Search } from './asset-picker-modal-search'; +import { AssetPickerModalNetwork } from './asset-picker-modal-network'; type AssetPickerModalProps = { header: JSX.Element | string | null; @@ -74,10 +76,21 @@ type AssetPickerModalProps = { * Sending asset for UI treatments; only for dest component */ sendingAsset?: { image: string; symbol: string } | undefined; + onNetworkPickerClick?: () => void; + /** + * Generator function that returns a list of tokens filtered by a predicate and sorted + * by a custom order. + */ + customTokenListGenerator?: ( + filterPredicate: (symbol: string, address?: string) => boolean, + ) => Generator< + AssetWithDisplayData<NativeAsset> | AssetWithDisplayData<ERC20Asset> + >; } & Pick< React.ComponentProps<typeof AssetPickerModalTabs>, 'visibleTabs' | 'defaultActiveTabKey' ->; +> & + Pick<React.ComponentProps<typeof AssetPickerModalNetwork>, 'network'>; const MAX_UNOWNED_TOKENS_RENDERED = 30; @@ -88,6 +101,9 @@ export function AssetPickerModal({ asset, onAssetChange, sendingAsset, + network, + onNetworkPickerClick, + customTokenListGenerator, ...tabProps }: AssetPickerModalProps) { const t = useI18nContext(); @@ -132,13 +148,6 @@ export function AssetPickerModal({ const tokenList = useSelector(getTokenList) as TokenListMap; const topTokens = useSelector(getTopAssets, isEqual); - const usersTokens = uniqBy<TokenWithBalance>( - [...tokensWithBalances, ...tokens], - 'address', - ); - - const memoizedUsersTokens: TokenWithBalance[] = useEqualityCheck(usersTokens); - const getIsDisabled = useCallback( ({ address, @@ -157,40 +166,49 @@ export function AssetPickerModal({ [sendingAsset?.symbol, memoizedSwapsBlockedTokens], ); - const filteredTokenList = useMemo(() => { - const nativeToken: AssetWithDisplayData<NativeAsset> = { - address: null, - symbol: nativeCurrency, - decimals: 18, - image: nativeCurrencyImage, - balance: balanceValue, - string: undefined, - type: AssetType.native, - }; - - const filteredTokens: AssetWithDisplayData<ERC20Asset | NativeAsset>[] = []; - // undefined would be the native token address - const filteredTokensAddresses = new Set<string | undefined>(); - - function* tokenGenerator(): Generator< + const memoizedUsersTokens: TokenWithBalance[] = useMemo(() => { + return uniqBy<TokenWithBalance>( + [...tokensWithBalances, ...tokens], + 'address', + ); + }, [tokensWithBalances, tokens]); + + const tokenListGenerator = useCallback( + function* ( + shouldAddToken: (symbol: string, address?: null | string) => boolean, + ): Generator< | AssetWithDisplayData<NativeAsset> | ((Token | TokenListToken) & { balance?: string; string?: string; }) > { - yield nativeToken; + const nativeToken: AssetWithDisplayData<NativeAsset> = { + address: null, + symbol: nativeCurrency, + decimals: 18, + image: nativeCurrencyImage, + balance: balanceValue, + string: undefined, + type: AssetType.native, + }; + + if (shouldAddToken(nativeToken.symbol, nativeToken.address)) { + yield nativeToken; + } const blockedTokens = []; for (const token of memoizedUsersTokens) { - yield token; + if (shouldAddToken(token.symbol, token.address)) { + yield token; + } } // topTokens should already be sorted by popularity for (const address of Object.keys(topTokens)) { const token = tokenList?.[address]; - if (token) { + if (token && shouldAddToken(token.symbol, token.address)) { if (getIsDisabled(token)) { blockedTokens.push(token); continue; @@ -201,37 +219,68 @@ export function AssetPickerModal({ } for (const token of Object.values(tokenList)) { - yield token; + if (shouldAddToken(token.symbol, token.address)) { + yield token; + } } for (const token of blockedTokens) { yield token; } - } + }, + [ + nativeCurrency, + nativeCurrencyImage, + balanceValue, + memoizedUsersTokens, + topTokens, + tokenList, + getIsDisabled, + ], + ); - for (const token of tokenGenerator()) { - if ( - token.symbol?.toLowerCase().includes(searchQuery.toLowerCase()) && - !filteredTokensAddresses.has(token.address?.toLowerCase()) - ) { - filteredTokensAddresses.add(token.address?.toLowerCase()); - filteredTokens.push( - getRenderableTokenData( - token.address - ? ({ - ...token, - ...tokenList[token.address.toLowerCase()], - type: AssetType.token, - } as AssetWithDisplayData<ERC20Asset>) - : token, - tokenConversionRates, - conversionRate, - currentCurrency, - chainId, - tokenList, - ), - ); - } + const filteredTokenList = useMemo(() => { + const filteredTokens: ( + | AssetWithDisplayData<ERC20Asset> + | AssetWithDisplayData<NativeAsset> + )[] = []; + // undefined would be the native token address + const filteredTokensAddresses = new Set<string | undefined>(); + + // Default filter predicate for whether a token should be included in displayed list + const shouldAddToken = (symbol: string, address?: string | null) => { + const trimmedSearchQuery = searchQuery.trim(); + return ( + (!trimmedSearchQuery || + symbol?.toLowerCase().includes(trimmedSearchQuery.toLowerCase())) && + !filteredTokensAddresses.has(address?.toLowerCase()) + ); + }; + + // If filteredTokensGenerator is passed in, use it to generate the filtered tokens + // Otherwise use the default tokenGenerator + for (const token of (customTokenListGenerator ?? tokenListGenerator)( + shouldAddToken, + )) { + filteredTokensAddresses.add(token.address?.toLowerCase()); + filteredTokens.push( + customTokenListGenerator + ? token + : getRenderableTokenData( + token.address + ? ({ + ...token, + ...tokenList[token.address.toLowerCase()], + type: AssetType.token, + } as AssetWithDisplayData<ERC20Asset>) + : token, + tokenConversionRates, + conversionRate, + currentCurrency, + chainId, + tokenList, + ), + ); if (filteredTokens.length > MAX_UNOWNED_TOKENS_RENDERED) { break; @@ -240,22 +289,13 @@ export function AssetPickerModal({ return filteredTokens; }, [ - memoizedUsersTokens, - topTokens, - searchQuery, - nativeCurrency, - nativeCurrencyImage, - balanceValue, - memoizedUsersTokens, - topTokens, - tokenList, - getIsDisabled, searchQuery, tokenConversionRates, conversionRate, currentCurrency, chainId, - tokenList, + tokenListGenerator, + customTokenListGenerator, ]); return ( @@ -289,6 +329,21 @@ export function AssetPickerModal({ </Text> </Box> )} + {onNetworkPickerClick && ( + <Box className="network-picker"> + <PickerNetwork + label={network?.name ?? 'Select network'} + src={ + network?.chainId && + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + network.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ] + } + onClick={onNetworkPickerClick} + data-testid="multichain-asset-picker__network" + /> + </Box> + )} <Box className="modal-tab__wrapper"> <AssetPickerModalTabs {...tabProps}> <React.Fragment key={TabName.TOKENS}> diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss b/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss index bf231003a671..be1e27dc1416 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss @@ -1,5 +1,13 @@ @use "design-system"; +.multichain-asset-picker__network-modal { + overflow-y: auto; + + .mm-modal-content__dialog { + overflow-y: scroll; + } +} + .asset-picker-modal { $self: &; @@ -56,6 +64,17 @@ max-height: 100%; } + .network-picker { + display: flex; + justify-content: center; + align-items: center; + padding-top: 4px; + + button: { + background: var(--color-background-alternative); + } + } + .modal-tab { &__main-view { max-height: 100%; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap index d2748ce758bf..42c7b204d708 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap @@ -131,3 +131,75 @@ exports[`AssetPicker render if disabled 1`] = ` </button> </div> `; + +exports[`AssetPicker should render network picker when networks prop is defined 1`] = ` +<DocumentFragment> + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--disabled asset-picker mm-text--body-md-medium mm-box--padding-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--gap-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-transparent mm-box--rounded-pill" + data-testid="asset-picker-button" + disabled="" + title="[swapTokenNotAvailable]" + > + <span + class="mm-box mm-text mm-text--inherit mm-box--color-text-default" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex" + > + <div + class="mm-box mm-badge-wrapper mm-box--display-inline-block" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-token mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full" + > + <img + alt="NATIVE TICKER logo" + class="mm-avatar-token__token-image" + src="./images/eth_logo.svg" + /> + </div> + <div + class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-muted box--border-style-solid box--border-width-1" + > + <img + alt="network logo" + class="mm-avatar-network__network-image" + src="./images/eth_logo.svg" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box" + > + <div + aria-describedby="tippy-tooltip-4" + class="" + data-original-title="NATIVE TICKER" + data-tooltipped="" + style="display: inline;" + tabindex="0" + > + <p + class="mm-box mm-text asset-picker__symbol mm-text--body-md mm-box--color-text-default" + > + NATIVE... + </p> + </div> + </div> + </div> + </span> + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-start-0 mm-box--display-none mm-box--color-icon-default" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> + </button> +</DocumentFragment> +`; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.stories.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.stories.tsx index 753ee2644f5a..0572e2d237c2 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.stories.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.stories.tsx @@ -5,8 +5,14 @@ import mockState from '../../../../../test/data/mock-state.json'; import { AssetType } from '../../../../../shared/constants/transaction'; import { AssetPicker } from './asset-picker'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../../../shared/constants/network'; +import { TabName } from '../asset-picker-modal/asset-picker-modal-tabs'; +import { + CHAIN_ID_TOKEN_IMAGE_MAP, + CHAIN_IDS, +} from '../../../../../shared/constants/network'; import { ERC20Asset } from '../asset-picker-modal/types'; +import { mockNetworkState } from '../../../../../test/stub/networks'; +import { RpcEndpointType } from '@metamask/network-controller'; const storybook = { title: 'Components/Multichain/AssetPicker', @@ -61,7 +67,7 @@ export const SendDestStory = () => { type: AssetType.native, }} sendingAsset={{ - image: 'token image', + image: CHAIN_ID_TOKEN_IMAGE_MAP['0x1'], symbol: 'ETH', }} /> @@ -72,12 +78,11 @@ function store() { const defaultMockState = { ...mockState }; defaultMockState.metamask = { ...defaultMockState.metamask, - providerConfig: { - ...defaultMockState.metamask.providerConfig, - chainId: '0x1', - ticker: 'ETH', - nickname: 'Ethereum Mainnet', - }, + ...(mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.LINEA_MAINNET }, + { chainId: CHAIN_IDS.GOERLI }, + ) as any), }; return configureStore(defaultMockState); } @@ -88,4 +93,74 @@ SendDestStory.decorators = [ SendDestStory.storyName = 'With Sending Asset'; +export const NetworksStory = ({ isOpen }: { isOpen: boolean }) => { + const t = useI18nContext(); + return ( + <AssetPicker + header={'Bridge from'} + onAssetChange={() => ({})} + {...props} + asset={{ + symbol: 'ETH', + image: CHAIN_ID_TOKEN_IMAGE_MAP['0x1'], + type: AssetType.native, + }} + networkProps={{ + network: { + chainId: '0x1', + name: 'Mainnet', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://mainnet.infura.io/v3/', + type: RpcEndpointType.Custom, + }, + ], + nativeCurrency: 'ETH', + }, + networks: [ + { + chainId: '0x1', + name: 'Mainnet', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://mainnet.infura.io/v3/', + type: RpcEndpointType.Custom, + }, + ], + nativeCurrency: 'ETH', + }, + { + chainId: '0x10', + name: 'Optimism', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test2', + url: 'https://optimism.infura.io/v3/', + type: RpcEndpointType.Custom, + }, + ], + nativeCurrency: 'ETH', + }, + ], + onNetworkChange: () => ({}), + }} + visibleTabs={[TabName.TOKENS]} + /> + ); +}; + +NetworksStory.decorators = [ + (story) => <Provider store={store()}>{story()}</Provider>, +]; + +NetworksStory.storyName = 'With Network Picker'; + export default storybook; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx index 262016ce4e69..5481a2b0c5de 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { Hex } from '@metamask/utils'; +import { RpcEndpointType } from '@metamask/network-controller'; import { AssetType } from '../../../../../shared/constants/transaction'; import mockSendState from '../../../../../test/data/mock-send-state.json'; import configureStore from '../../../../store/store'; @@ -113,6 +114,10 @@ describe('AssetPicker', () => { const img = getByAltText('Ethereum Mainnet logo'); expect(img).toBeInTheDocument(); expect(img).toHaveAttribute('src', './images/eth_logo.svg'); + expect(getByAltText('NATIVE logo')).toHaveAttribute( + 'src', + CHAIN_ID_TOKEN_IMAGE_MAP['0x1'], + ); }); it('native: renders overflowing symbol and image', () => { @@ -136,6 +141,10 @@ describe('AssetPicker', () => { const img = getByAltText('Ethereum Mainnet logo'); expect(img).toBeInTheDocument(); expect(img).toHaveAttribute('src', './images/eth_logo.svg'); + expect(getByAltText('NATIVE TICKER logo')).toHaveAttribute( + 'src', + CHAIN_ID_TOKEN_IMAGE_MAP['0x1'], + ); }); it('token: renders symbol and image', () => { @@ -160,6 +169,10 @@ describe('AssetPicker', () => { const img = getByAltText('symbol logo'); expect(img).toBeInTheDocument(); expect(img).toHaveAttribute('src', 'token icon url'); + expect(getByAltText('symbol logo')).toHaveAttribute( + 'src', + 'token icon url', + ); }); it('token: renders symbol and image overflowing', () => { @@ -172,11 +185,7 @@ describe('AssetPicker', () => { const mockAssetChange = jest.fn(); const { getByText, getByAltText } = render( - <Provider - store={store("SHOULDN'T MATTER", { - 'token address': { iconUrl: 'token icon url' }, - })} - > + <Provider store={store("SHOULDN'T MATTER")}> <AssetPicker header={'testHeader'} asset={asset} @@ -188,6 +197,10 @@ describe('AssetPicker', () => { const img = getByAltText('symbol overflow logo'); expect(img).toBeInTheDocument(); expect(img).toHaveAttribute('src', 'token icon url'); + expect(getByAltText('symbol overflow logo')).toHaveAttribute( + 'src', + 'token icon url', + ); }); it('token: renders symbol and image falls back', () => { @@ -284,4 +297,77 @@ describe('AssetPicker', () => { expect(container).toMatchSnapshot(); }); + + it('should render network picker when networks prop is defined', () => { + const asset = { + type: AssetType.native, + image: CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP['0x1'], + symbol: NATIVE_TICKER, + } as NativeAsset; + + const mockAssetChange = jest.fn(); + + const { asFragment } = render( + <Provider store={store(NATIVE_TICKER)}> + <AssetPicker + header={'testHeader'} + asset={asset} + onAssetChange={() => mockAssetChange()} + isDisabled + networkProps={{ + network: { + chainId: '0x1', + nativeCurrency: 'ETH', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'network', + }, + networks: [ + { + chainId: '0x1', + nativeCurrency: 'ETH', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test1', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'Network name 3', + }, + { + chainId: '0xa', + nativeCurrency: 'ETH', + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: ['https://explorerurl'], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + networkClientId: 'test2', + url: 'https://rpcurl', + type: RpcEndpointType.Custom, + }, + ], + name: 'Network name 4', + }, + ], + onNetworkChange: jest.fn(), + }} + /> + </Provider>, + ); + + expect(asFragment()).toMatchSnapshot(); + }); }); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx index 3b1526e4af62..e2965687fed5 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx @@ -26,7 +26,7 @@ import { AssetType } from '../../../../../shared/constants/transaction'; import { AssetPickerModal } from '../asset-picker-modal/asset-picker-modal'; import { getCurrentNetwork, - getTestNetworkBackgroundColor, + getNetworkConfigurationsByChainId, } from '../../../../selectors'; import Tooltip from '../../../ui/tooltip'; import { LARGE_SYMBOL_LENGTH } from '../constants'; @@ -41,6 +41,12 @@ import { NFT, } from '../asset-picker-modal/types'; import { TabName } from '../asset-picker-modal/asset-picker-modal-tabs'; +import { AssetPickerModalNetwork } from '../asset-picker-modal/asset-picker-modal-network'; +import { + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, + GOERLI_DISPLAY_NAME, + SEPOLIA_DISPLAY_NAME, +} from '../../../../../shared/constants/network'; const ELLIPSIFY_LENGTH = 13; // 6 (start) + 4 (end) + 3 (...) @@ -60,9 +66,13 @@ export type AssetPickerProps = { ) => void; onClick?: () => void; isDisabled?: boolean; + networkProps?: Pick< + React.ComponentProps<typeof AssetPickerModalNetwork>, + 'network' | 'networks' | 'onNetworkChange' + >; } & Pick< React.ComponentProps<typeof AssetPickerModal>, - 'visibleTabs' | 'header' | 'sendingAsset' + 'visibleTabs' | 'header' | 'sendingAsset' | 'customTokenListGenerator' >; // A component that lets the user pick from a list of assets. @@ -70,10 +80,12 @@ export function AssetPicker({ header, asset, onAssetChange, + networkProps, sendingAsset, onClick, isDisabled = false, visibleTabs, + customTokenListGenerator, }: AssetPickerProps) { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const t = useI18nContext(); @@ -95,7 +107,10 @@ export function AssetPicker({ // Badge details const currentNetwork = useSelector(getCurrentNetwork); - const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const selectedNetwork = + networkProps?.network ?? + (currentNetwork?.chainId && allNetworks[currentNetwork.chainId]); const handleAssetPickerTitle = (): string | undefined => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -107,8 +122,23 @@ export function AssetPicker({ return undefined; }; + const [isSelectingNetwork, setIsSelectingNetwork] = useState(false); + return ( <> + {networkProps && ( + <AssetPickerModalNetwork + isOpen={isSelectingNetwork} + onClose={() => { + setIsSelectingNetwork(false); + }} + onBack={() => { + setIsSelectingNetwork(false); + setShowAssetPickerModal(true); + }} + {...networkProps} + /> + )} {/* This is the Modal that ask to choose token to send */} <AssetPickerModal visibleTabs={visibleTabs} @@ -125,9 +155,19 @@ export function AssetPicker({ setShowAssetPickerModal(false); }} sendingAsset={sendingAsset} + network={networkProps?.network ? networkProps.network : undefined} + onNetworkPickerClick={ + networkProps + ? () => { + setShowAssetPickerModal(false); + setIsSelectingNetwork(true); + } + : undefined + } defaultActiveTabKey={ asset?.type === AssetType.NFT ? TabName.NFTS : TabName.TOKENS } + customTokenListGenerator={customTokenListGenerator} /> <ButtonBase @@ -143,7 +183,11 @@ export function AssetPicker({ justifyContent={isNFT ? JustifyContent.spaceBetween : undefined} backgroundColor={BackgroundColor.transparent} onClick={() => { - setShowAssetPickerModal(true); + if (networkProps && !networkProps.network) { + setIsSelectingNetwork(true); + } else { + setShowAssetPickerModal(true); + } onClick?.(); }} endIconName={IconName.ArrowDown} @@ -154,59 +198,81 @@ export function AssetPicker({ }} title={handleAssetPickerTitle()} > - <Box display={Display.Flex} alignItems={AlignItems.center} gap={3}> - <Box display={Display.Flex}> - <BadgeWrapper - badge={ - <AvatarNetwork - size={AvatarNetworkSize.Xs} - name={currentNetwork?.nickname ?? ''} - src={currentNetwork?.rpcPrefs?.imageUrl} - backgroundColor={testNetworkBackgroundColor} - borderColor={ - primaryTokenImage - ? BorderColor.borderMuted - : BorderColor.borderDefault - } + {asset ? ( + <Box display={Display.Flex} alignItems={AlignItems.center} gap={3}> + <Box display={Display.Flex}> + <BadgeWrapper + badge={ + <AvatarNetwork + size={AvatarNetworkSize.Xs} + name={selectedNetwork?.name ?? ''} + src={ + selectedNetwork?.chainId && + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + selectedNetwork.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ] + } + backgroundColor={ + Object.entries({ + [GOERLI_DISPLAY_NAME]: BackgroundColor.goerli, + [SEPOLIA_DISPLAY_NAME]: BackgroundColor.sepolia, + }).find(([tickerSubstring]) => + selectedNetwork?.nativeCurrency?.includes( + tickerSubstring, + ), + )?.[1] + } + borderColor={ + primaryTokenImage + ? BorderColor.borderMuted + : BorderColor.borderDefault + } + /> + } + > + <AvatarToken + borderRadius={isNFT ? BorderRadius.LG : BorderRadius.full} + src={primaryTokenImage ?? undefined} + size={AvatarTokenSize.Md} + name={symbol} + {...(isNFT && { + backgroundColor: BackgroundColor.transparent, + })} /> - } - > - <AvatarToken - borderRadius={isNFT ? BorderRadius.LG : BorderRadius.full} - src={primaryTokenImage ?? undefined} - size={AvatarTokenSize.Md} - name={symbol} - {...(isNFT && { backgroundColor: BackgroundColor.transparent })} - /> - </BadgeWrapper> - </Box> + </BadgeWrapper> + </Box> - <Tooltip - disabled={!isSymbolLong} - title={symbol} - position="bottom" - wrapperClassName="mm-box" - > - <Text - className="asset-picker__symbol" - variant={TextVariant.bodyMd} - color={TextColor.textDefault} + <Tooltip + disabled={!isSymbolLong} + title={symbol} + position="bottom" + wrapperClassName="mm-box" > - {formattedSymbol} - </Text> - {isNFT && asset?.tokenId && ( <Text - variant={TextVariant.bodySm} - color={TextColor.textAlternative} + className="asset-picker__symbol" + variant={TextVariant.bodyMd} + color={TextColor.textDefault} > - # - {String(asset.tokenId).length < ELLIPSIFY_LENGTH - ? asset.tokenId - : ellipsify(String(asset.tokenId), 6, 4)} + {formattedSymbol} </Text> - )} - </Tooltip> - </Box> + {isNFT && asset?.tokenId && ( + <Text + variant={TextVariant.bodySm} + color={TextColor.textAlternative} + > + # + {String(asset.tokenId).length < ELLIPSIFY_LENGTH + ? asset.tokenId + : ellipsify(String(asset.tokenId), 6, 4)} + </Text> + )} + </Tooltip> + </Box> + ) : ( + <Text className="asset-picker__fallback" variant={TextVariant.bodyMd}> + {t('swapSelectToken')} + </Text> + )} </ButtonBase> </> ); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/index.scss b/ui/components/multichain/asset-picker-amount/asset-picker/index.scss index d30ce8c016d7..6cfaf877efbd 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/index.scss +++ b/ui/components/multichain/asset-picker-amount/asset-picker/index.scss @@ -25,4 +25,9 @@ opacity: 1; cursor: not-allowed; } + + &__fallback { + text-wrap: nowrap; + padding-left: 8px; + } } diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index a5d6cf385e36..0c3c46114541 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -81,6 +81,7 @@ type TokenListItemProps = { isStakeable?: boolean; address?: string | null; showPercentage?: boolean; + isPrimaryTokenSymbolHidden?: boolean; }; export const TokenListItem = ({ @@ -93,6 +94,7 @@ export const TokenListItem = ({ title, tooltipText, isOriginalTokenSymbol, + isPrimaryTokenSymbolHidden = false, isNativeCurrency = false, isStakeable = false, address = null, @@ -379,7 +381,10 @@ export const TokenListItem = ({ variant={TextVariant.bodyMd} textAlign={TextAlign.End} > - {primary} {isNativeCurrency ? '' : tokenSymbol} + {primary}{' '} + {isNativeCurrency || isPrimaryTokenSymbolHidden + ? '' + : tokenSymbol} </Text> </Box> ) : ( @@ -405,7 +410,10 @@ export const TokenListItem = ({ variant={TextVariant.bodySmMedium} textAlign={TextAlign.End} > - {primary} {isNativeCurrency ? '' : tokenSymbol} + {primary}{' '} + {isNativeCurrency || isPrimaryTokenSymbolHidden + ? '' + : tokenSymbol} </Text> </Box> )} From 875ab21f3f69cf785faac83a949125c170cb0e82 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari <nidhi.kumari@consensys.net> Date: Thu, 10 Oct 2024 13:43:35 +0100 Subject: [PATCH 110/226] fix: updated toasts component and copy (#27656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to update the toast component and update the copy changes for the toast ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/3389](https://github.com/MetaMask/MetaMask-planning/issues/3389 ) ## **Manual testing steps** 1. Run extension with CHAIN_PERMISSIONS=1 yarn start 2. Check the toast component, it has border radius, box shadow and other fixes as described in ticket ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-07 at 12 33 08 PM](https://github.com/user-attachments/assets/5a2c27ec-a972-4d50-a80d-a2a69838c932) ### **After** ![Screenshot 2024-10-07 at 12 33 52 PM](https://github.com/user-attachments/assets/8831f9bd-095c-4917-8486-37992913f906) ## **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 | 8 +++----- .../review-permissions-page.tsx | 9 ++++----- .../toast/__snapshots__/toast.test.tsx.snap | 2 +- ui/components/multichain/toast/index.scss | 12 ++++++++---- ui/components/multichain/toast/toast.tsx | 3 +++ .../__snapshots__/survey-toast.test.tsx.snap | 2 +- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5f6a977c1cf4..4cd6b48566df 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -163,8 +163,7 @@ "message": "Account options" }, "accountPermissionToast": { - "message": "Account permissions updated for $1", - "description": "$1 represents connected dapp" + "message": "Account permissions updated" }, "accountSelectionRequired": { "message": "You need to select an account!" @@ -3151,8 +3150,7 @@ "message": "Network options" }, "networkPermissionToast": { - "message": "Network permissions updated for $1", - "description": "$1 represents connected dapp" + "message": "Network permissions updated" }, "networkProvider": { "message": "Network provider" @@ -4100,7 +4098,7 @@ "message": "You're giving the spender permission to spend this many tokens from your account." }, "permittedChainToastUpdate": { - "message": "$1 has been given access to $2." + "message": "$1 has access to $2." }, "personalAddressDetected": { "message": "Personal address detected. Input the token contract address." 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 35e9a77656ba..022b508984cd 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 @@ -5,11 +5,11 @@ import { NonEmptyArray } from '@metamask/utils'; import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { NetworkConfiguration } from '@metamask/network-controller'; import { + AlignItems, BlockSize, Display, FlexDirection, } from '../../../../helpers/constants/design-system'; -import { getURLHost } from '../../../../helpers/utils/util'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { getConnectedSitesList, @@ -175,8 +175,6 @@ export const ReviewPermissions = () => { setShowAccountToast(true); }; - const hostName = getURLHost(securedOrigin); - return ( <Page data-testid="connections-page" @@ -222,11 +220,12 @@ export const ReviewPermissions = () => { flexDirection={FlexDirection.Column} width={BlockSize.Full} gap={2} + alignItems={AlignItems.center} > {showAccountToast ? ( <ToastContainer> <Toast - text={t('accountPermissionToast', [hostName])} + text={t('accountPermissionToast')} onClose={() => setShowAccountToast(false)} startAdornment={ <AvatarFavicon @@ -241,7 +240,7 @@ export const ReviewPermissions = () => { {showNetworkToast ? ( <ToastContainer> <Toast - text={t('networkPermissionToast', [hostName])} + text={t('networkPermissionToast')} onClose={() => setShowNetworkToast(false)} startAdornment={ <AvatarFavicon diff --git a/ui/components/multichain/toast/__snapshots__/toast.test.tsx.snap b/ui/components/multichain/toast/__snapshots__/toast.test.tsx.snap index 1e009735c1e1..8afaf9082776 100644 --- a/ui/components/multichain/toast/__snapshots__/toast.test.tsx.snap +++ b/ui/components/multichain/toast/__snapshots__/toast.test.tsx.snap @@ -3,7 +3,7 @@ exports[`Toast should render Toast component 1`] = ` <div> <div - class="mm-box mm-banner-base mm-box--padding-3 mm-box--display-flex mm-box--gap-2 mm-box--background-color-background-default" + class="mm-box mm-banner-base toasts-container__banner-base undefined mm-box--padding-3 mm-box--display-flex mm-box--gap-2 mm-box--background-color-background-default" data-theme="light" > <div diff --git a/ui/components/multichain/toast/index.scss b/ui/components/multichain/toast/index.scss index ef351fec8d67..8bb290a777ce 100644 --- a/ui/components/multichain/toast/index.scss +++ b/ui/components/multichain/toast/index.scss @@ -1,14 +1,18 @@ .toasts-container { position: sticky; - bottom: 10px; - margin-inline-start: 10px; - margin-inline-end: 10px; + bottom: 16px; + margin-inline-start: 16px; + margin-inline-end: 16px; z-index: 200; display: flex; gap: 10px; width: 90%; flex-direction: column; - max-width: 600px; + + &__banner-base { + border-radius: 8px; + box-shadow: var(--shadow-size-md); + } } .toast-text { diff --git a/ui/components/multichain/toast/toast.tsx b/ui/components/multichain/toast/toast.tsx index b69d099a9034..1d62a50f75db 100644 --- a/ui/components/multichain/toast/toast.tsx +++ b/ui/components/multichain/toast/toast.tsx @@ -24,6 +24,7 @@ export const Toast = ({ autoHideTime, onAutoHideToast, dataTestId, + className, }: { startAdornment: React.ReactNode | React.ReactNode[]; text: string; @@ -35,6 +36,7 @@ export const Toast = ({ autoHideTime?: number; onAutoHideToast?: () => void; dataTestId?: string; + className?: string; }) => { const { theme } = document.documentElement.dataset; const [shouldDisplay, setShouldDisplay] = useState(true); @@ -66,6 +68,7 @@ export const Toast = ({ onClose={onClose} borderRadius={borderRadius} data-testid={dataTestId ? `${dataTestId}-banner-base` : undefined} + className={`toasts-container__banner-base ${className}`} > <Box display={Display.Flex} gap={4} data-testid={dataTestId}> {startAdornment} diff --git a/ui/components/ui/survey-toast/__snapshots__/survey-toast.test.tsx.snap b/ui/components/ui/survey-toast/__snapshots__/survey-toast.test.tsx.snap index 04ddae84b4b1..a6e8f296b697 100644 --- a/ui/components/ui/survey-toast/__snapshots__/survey-toast.test.tsx.snap +++ b/ui/components/ui/survey-toast/__snapshots__/survey-toast.test.tsx.snap @@ -3,7 +3,7 @@ exports[`SurveyToast should match snapshot 1`] = ` <div> <div - class="mm-box mm-banner-base mm-box--padding-3 mm-box--display-flex mm-box--gap-2 mm-box--background-color-background-default" + class="mm-box mm-banner-base toasts-container__banner-base undefined mm-box--padding-3 mm-box--display-flex mm-box--gap-2 mm-box--background-color-background-default" data-testid="survey-toast-banner-base" data-theme="light" > From bfde1da99612a31efd6186004239ade5a715cca3 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 10 Oct 2024 14:35:21 +0100 Subject: [PATCH 111/226] fix: SIWE signature page displays parsed URI instead of domain (#27754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR modifies the SIWE signature page only, by changing the URL field to display the parsed `uri` instead of `domain`. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27754?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27609 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../siwe-sign/__snapshots__/siwe-sign.test.tsx.snap | 4 ++-- .../confirm/info/personal-sign/siwe-sign/siwe-sign.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap index 69f98d84213d..56fba6cdaa0c 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap @@ -54,7 +54,7 @@ exports[`SIWESignInfo renders correctly for SIWE signature request 1`] = ` class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - metamask.github.io + https://metamask.github.io </p> </div> </div> @@ -358,7 +358,7 @@ exports[`SIWESignInfo renders correctly for SIWE signature request with resource class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - metamask.github.io + https://metamask.github.io </p> </div> </div> diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx index 404390a03754..383bf182ac8c 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx @@ -26,12 +26,12 @@ const SIWESignInfo: React.FC = () => { const { address, chainId, - domain, issuedAt, nonce, requestId, statement, resources, + uri, version, } = siweMessage; const hexChainId = toHex(chainId); @@ -44,7 +44,7 @@ const SIWESignInfo: React.FC = () => { <ConfirmInfoRowText text={statement || ''} /> </ConfirmInfoRow> <ConfirmInfoRow label={t('siweURI')}> - <ConfirmInfoRowText text={domain} /> + <ConfirmInfoRowText text={uri} /> </ConfirmInfoRow> <ConfirmInfoRow label={t('siweNetwork')}> <ConfirmInfoRowText text={network} /> From b08b374df1c5a1fa3945f22f48ab7a1fdc4e1558 Mon Sep 17 00:00:00 2001 From: David Drazic <david@timechaser.org> Date: Thu, 10 Oct 2024 15:43:23 +0200 Subject: [PATCH 112/226] fix: issue with Snap title in Snap Authorship Header (#27752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix issue with Snap name in Snap Authorship Header. ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27752?quickstart=1) ## **Related issues** Fixes: n/a ## **Manual testing steps** 1. Try to install a Snap with long name (e.g. Ethereum Provider Example Snap) 2. Make sure that the title is properly truncated and all elements visible ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-10 at 11 38 11](https://github.com/user-attachments/assets/6919c8f9-ce3f-4c27-97f5-d4087c0d87ee) ![Screenshot 2024-10-10 at 11 38 28](https://github.com/user-attachments/assets/03bbae28-b859-4e85-8abf-e858e4793d81) ### **After** ![Screenshot 2024-10-10 at 12 08 44](https://github.com/user-attachments/assets/2486f3f5-8485-4342-ad42-d9eba9d4e1ed) ![Screenshot 2024-10-10 at 12 08 57](https://github.com/user-attachments/assets/1857085f-6868-4eb8-98b9-041c4d985256) ## **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/snaps/snap-authorship-header/snap-authorship-header.js | 2 ++ .../templates/__snapshots__/create-snap-account.test.js.snap | 2 ++ .../templates/__snapshots__/remove-snap-account.test.js.snap | 2 ++ .../templates/__snapshots__/snap-account-redirect.test.js.snap | 1 + .../__snapshots__/create-snap-redirect.test.tsx.snap | 2 ++ 5 files changed, 9 insertions(+) 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 a2561d92e48d..0cb0a48dd2d6 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 @@ -91,12 +91,14 @@ const SnapAuthorshipHeader = ({ display={Display.Flex} justifyContent={JustifyContent.center} alignItems={AlignItems.center} + style={{ overflow: 'hidden' }} > <SnapIcon snapId={snapId} avatarSize={IconSize.Sm} /> <Text color={TextColor.textDefault} variant={TextVariant.bodyMdMedium} marginLeft={2} + title={snapName} ellipsis > {snapName} diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap index d85bbe7bb4ed..114355592125 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap @@ -31,6 +31,7 @@ exports[`create-snap-account confirmation should match snapshot 1`] = ` > <div class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + style="overflow: hidden;" > <div class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-alternative mm-box--background-color-background-alternative mm-box--rounded-full" @@ -40,6 +41,7 @@ exports[`create-snap-account confirmation should match snapshot 1`] = ` </div> <p class="mm-box mm-text mm-text--body-md-medium mm-text--ellipsis mm-box--margin-left-2 mm-box--color-text-default" + title="Test Snap" > Test Snap </p> diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap index 3acaa31478e7..db600570c273 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap @@ -31,6 +31,7 @@ exports[`remove-snap-account confirmation should match snapshot 1`] = ` > <div class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + style="overflow: hidden;" > <div class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-alternative mm-box--background-color-background-alternative mm-box--rounded-full" @@ -40,6 +41,7 @@ exports[`remove-snap-account confirmation should match snapshot 1`] = ` </div> <p class="mm-box mm-text mm-text--body-md-medium mm-text--ellipsis mm-box--margin-left-2 mm-box--color-text-default" + title="Test Snap" > Test Snap </p> diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap index 11ac26234265..d7731522c967 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap @@ -22,6 +22,7 @@ exports[`snap-account-redirect confirmation should match snapshot 1`] = ` > <div class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + style="overflow: hidden;" > <div class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-alternative mm-box--background-color-background-alternative mm-box--rounded-full" diff --git a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap index e6bb4ba7579c..29fefa61b305 100644 --- a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap +++ b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap @@ -15,6 +15,7 @@ exports[`<SnapAccountRedirect /> renders the url and message when provided and i > <div class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + style="overflow: hidden;" > <div class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-alternative mm-box--background-color-background-alternative mm-box--rounded-full" @@ -24,6 +25,7 @@ exports[`<SnapAccountRedirect /> renders the url and message when provided and i </div> <p class="mm-box mm-text mm-text--body-md-medium mm-text--ellipsis mm-box--margin-left-2 mm-box--color-text-default" + title="@metamask/snap-simple-keyring" > @metamask/snap-simple-keyring </p> From 7d7f01754d94b7ba3d21c47878455625b982e8fe Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 10 Oct 2024 15:34:19 +0100 Subject: [PATCH 113/226] =?UTF-8?q?fix:=20Replace=20'transaction=20fees'?= =?UTF-8?q?=20with=20'network=20fees'=20in=20the=20insufficie=E2=80=A6=20(?= =?UTF-8?q?#27762)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …nt funds alert copy <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> A copy change in the insufficient funds alert. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27762?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/de/messages.json | 3 --- app/_locales/el/messages.json | 3 --- app/_locales/en/messages.json | 4 ++-- app/_locales/en_GB/messages.json | 3 --- app/_locales/es/messages.json | 3 --- app/_locales/fr/messages.json | 3 --- app/_locales/hi/messages.json | 3 --- app/_locales/id/messages.json | 3 --- app/_locales/ja/messages.json | 3 --- app/_locales/ko/messages.json | 3 --- app/_locales/pt/messages.json | 3 --- app/_locales/ru/messages.json | 3 --- app/_locales/tl/messages.json | 3 --- app/_locales/tr/messages.json | 3 --- app/_locales/vi/messages.json | 3 --- app/_locales/zh_CN/messages.json | 3 --- .../e2e/tests/confirmations/alerts/insufficient-funds.spec.ts | 2 +- .../alerts/transactions/useInsufficientBalanceAlerts.test.ts | 2 +- .../hooks/alerts/transactions/useInsufficientBalanceAlerts.ts | 2 +- 19 files changed, 5 insertions(+), 50 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 8c91aec52887..26a5018b00ad 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Um mit dieser Transaktion fortzufahren, müssen Sie das Gas-Limit auf 21.000 oder mehr erhöhen." }, - "alertMessageInsufficientBalance": { - "message": "Sie haben nicht genug ETH auf Ihrem Konto, um die Transaktionsgebühren zu bezahlen." - }, "alertMessageNetworkBusy": { "message": "Die Gas-Preise sind hoch und die Schätzungen sind weniger genau." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 4f29362124bd..afeba57dfb6f 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Για να συνεχίσετε με αυτή τη συναλλαγή, θα πρέπει να αυξήσετε το όριο των τελών συναλλαγών σε 21000 ή περισσότερο." }, - "alertMessageInsufficientBalance": { - "message": "Δεν έχετε αρκετά ETH στον λογαριασμό σας για να πληρώσετε τα τέλη συναλλαγών." - }, "alertMessageNetworkBusy": { "message": "Οι τιμές των τελών συναλλαγών είναι υψηλές και οι εκτιμήσεις είναι λιγότερο ακριβείς." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 4cd6b48566df..a25b94722ea7 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -442,8 +442,8 @@ "alertMessageGasTooLow": { "message": "To continue with this transaction, you’ll need to increase the gas limit to 21000 or higher." }, - "alertMessageInsufficientBalance": { - "message": "You do not have enough ETH in your account to pay for transaction fees." + "alertMessageInsufficientBalance2": { + "message": "You do not have enough ETH in your account to pay for network fees." }, "alertMessageNetworkBusy": { "message": "Gas prices are high and estimates are less accurate." diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 3c8962e7f7c9..153655e55f24 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -412,9 +412,6 @@ "alertMessageGasTooLow": { "message": "To continue with this transaction, you’ll need to increase the gas limit to 21000 or higher." }, - "alertMessageInsufficientBalance": { - "message": "You do not have enough ETH in your account to pay for transaction fees." - }, "alertMessageNetworkBusy": { "message": "Gas prices are high and estimates are less accurate." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 49c523b184f6..6c1792ebc84d 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Para continuar con esta transacción, deberá aumentar el límite de gas a 21000 o más." }, - "alertMessageInsufficientBalance": { - "message": "No tiene suficiente ETH en su cuenta para pagar las tarifas de transacción." - }, "alertMessageNetworkBusy": { "message": "Los precios del gas son altos y las estimaciones son menos precisas." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 0c5015f67665..247bdaa359e0 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Pour effectuer cette transaction, vous devez augmenter la limite de gaz à 21 000 ou plus." }, - "alertMessageInsufficientBalance": { - "message": "Vous n’avez pas assez d’ETH sur votre compte pour payer les frais de transaction." - }, "alertMessageNetworkBusy": { "message": "Les prix du gaz sont élevés et les estimations sont moins précises." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 274aae47e2e3..bca5168630b8 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "इस ट्रांसेक्शन को जारी रखने के लिए, आपको गैस लिमिट को 21000 या अधिक तक बढ़ाना होगा।" }, - "alertMessageInsufficientBalance": { - "message": "ट्रांसेक्शन फीस का भुगतान करने के लिए आपके अकाउंट में पर्याप्त ETH नहीं है।" - }, "alertMessageNetworkBusy": { "message": "गैस प्राइसें अधिक हैं और अनुमान कम सटीक हैं।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 5f36af7a382d..6e0d950450e9 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Untuk melanjutkan transaksi ini, Anda perlu meningkatkan batas gas menjadi 21000 atau lebih tinggi." }, - "alertMessageInsufficientBalance": { - "message": "Anda tidak memiliki cukup ETH di akun untuk membayar biaya transaksi." - }, "alertMessageNetworkBusy": { "message": "Harga gas tinggi dan estimasinya kurang akurat." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index c8adf1ff5af9..412708c194a3 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "このトランザクションを続行するには、ガスリミットを21000以上に上げる必要があります。" }, - "alertMessageInsufficientBalance": { - "message": "アカウントにトランザクション手数料を支払うのに十分なETHがありません。" - }, "alertMessageNetworkBusy": { "message": "ガス価格が高く、見積もりはあまり正確ではありません。" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 5868672bce32..6d47331f15c9 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "이 트랜잭션을 계속 진행하려면, 가스 한도를 21000 이상으로 늘려야 합니다." }, - "alertMessageInsufficientBalance": { - "message": "계정에 트랜잭션 수수료를 지불할 수 있는 이더리움이 충분하지 않습니다." - }, "alertMessageNetworkBusy": { "message": "가스비가 높고 견적의 정확도도 떨어집니다." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 298f4b8b8d70..23d1bae93726 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Para continuar com essa transação, você precisará aumentar o limite de gás para 21000 ou mais." }, - "alertMessageInsufficientBalance": { - "message": "Você não tem ETH suficiente em sua conta para pagar as taxas de transação." - }, "alertMessageNetworkBusy": { "message": "Os preços do gás são altos e as estimativas são menos precisas." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 999f237f73ea..f6a532043195 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Чтобы продолжить эту транзакцию, вам необходимо увеличить лимит газа до 21 000 или выше." }, - "alertMessageInsufficientBalance": { - "message": "На вашем счету недостаточно ETH для оплаты комиссий за транзакцию." - }, "alertMessageNetworkBusy": { "message": "Цены газа высоки, а оценки менее точны." }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index df021e9dfdad..1d9fdc20819e 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Para magpatuloy sa transaksyong ito, kakailanganin mong dagdagan ang gas limit sa 21000 o mas mataas." }, - "alertMessageInsufficientBalance": { - "message": "Wala kang sapat na ETH sa iyong account para bayaran ang mga bayad sa transaksyon." - }, "alertMessageNetworkBusy": { "message": "Ang mga presyo ng gas ay mataas at ang pagtantiya ay hindi gaanong tumpak." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index ce36a61ca716..35ac8fa29c7d 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Bu işlemle devam etmek için gaz limitini 21000 veya üzeri olacak şekilde artırmanız gerekecek." }, - "alertMessageInsufficientBalance": { - "message": "Hesabınızda işlem ücretlerini ödemek için yeterli ETH yok." - }, "alertMessageNetworkBusy": { "message": "Gaz fiyatları yüksektir ve tahmin daha az kesindir." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 5766a1789d24..29f9017003a9 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "Để tiếp tục giao dịch này, bạn cần tăng giới hạn phí gas lên 21000 hoặc cao hơn." }, - "alertMessageInsufficientBalance": { - "message": "Bạn không có đủ ETH trong tài khoản để thanh toán phí giao dịch." - }, "alertMessageNetworkBusy": { "message": "Phí gas cao và ước tính kém chính xác hơn." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index a5e2b1175862..0bed963cf99f 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -393,9 +393,6 @@ "alertMessageGasTooLow": { "message": "要继续此交易,您需要将燃料限制提高到 21000 或更高。" }, - "alertMessageInsufficientBalance": { - "message": "您的账户中没有足够的 ETH 来支付交易费用。" - }, "alertMessageNetworkBusy": { "message": "燃料价格很高,估算不太准确。" }, diff --git a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts index 59618596c344..3aa8ecc88ebc 100644 --- a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts +++ b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts @@ -73,6 +73,6 @@ async function mintNft(driver: Driver) { async function displayAlertForInsufficientBalance(driver: Driver) { await driver.waitForSelector({ css: '[data-testid="alert-modal__selected-alert"]', - text: 'You do not have enough ETH in your account to pay for transaction fees.', + text: 'You do not have enough ETH in your account to pay for network fees.', }); } diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts index 60531b5680d7..e8cfc802ee99 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts @@ -142,7 +142,7 @@ describe('useInsufficientBalanceAlerts', () => { isBlocking: true, key: 'insufficientBalance', message: - 'You do not have enough ETH in your account to pay for transaction fees.', + 'You do not have enough ETH in your account to pay for network fees.', reason: 'Insufficient funds', severity: Severity.Danger, }, diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts index ac2732d21688..55b0b0d8d94a 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts @@ -55,7 +55,7 @@ export function useInsufficientBalanceAlerts(): Alert[] { field: RowAlertKey.EstimatedFee, isBlocking: true, key: 'insufficientBalance', - message: t('alertMessageInsufficientBalance'), + message: t('alertMessageInsufficientBalance2'), reason: t('alertReasonInsufficientBalance'), severity: Severity.Danger, }, From dc3fa104d2c74f718f3d73854f392d4b857aa7e4 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:47:51 -0400 Subject: [PATCH 114/226] docs: remove outdated Medium link, update "Twitter" to "X" (#26692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our up-to-the-minute Medium link was last updated in 2022. And Twitter hasn't been called Twitter for a while now. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. - ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26692?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. --> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d70eb03a32b2..4f15e138be56 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ For [general questions](https://community.metamask.io/c/learn/26), [feature requ MetaMask supports Firefox, Google Chrome, and Chromium-based browsers. We recommend using the latest available browser version. -For up to the minute news, follow our [Twitter](https://twitter.com/metamask) or [Medium](https://medium.com/metamask) pages. +For up to the minute news, follow us on [X](https://x.com/MetaMask). To learn how to develop MetaMask-compatible applications, visit our [Developer Docs](https://metamask.github.io/metamask-docs/). From 0798717a81a98e4f1283ec8f9e664bd835b855c5 Mon Sep 17 00:00:00 2001 From: Vince Howard <vincenguyenhoward@gmail.com> Date: Thu, 10 Oct 2024 09:05:24 -0600 Subject: [PATCH 115/226] chore(3212): remove alert settings (#27709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR removes the "Alerts" section from the Extension's Settings menu, preparing for future updates. The two settings previously offered here, `unconnectedAccount` and `web3ShimUsage`, are no longer configurable and will remain toggled on by default. All related UI elements and tests have been removed. Rationale for removal: - `unconnectedAccount`: This feature, which alerts users when browsing a website with an unconnected account selected, is now considered a core functionality that doesn't require user configuration. - `web3ShimUsage`: This setting was introduced to inform users why a dapp might be broken due to using the deprecated window.web3 API. However, current data shows limited usage, primarily from non-crypto sites, and the warning only displays for connected dapps, limiting its effectiveness. This change paves the way for repurposing the bell icon for a new Notifications feature. Testing confirms that this removal doesn't affect other parts of the extension, and all remaining tests pass successfully. **Note:** Controller/state related removal will be addressed in a separate pull request and [tracked in a separate issue](https://github.com/MetaMask/MetaMask-planning/issues/3480). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27230?quickstart=1) ## **Related issues** Fixes: [#3212](https://github.com/MetaMask/MetaMask-planning/issues/3212) ## **Manual testing steps** 1. Goto settings page by clicking on the more icon which are the three dots in the top right corner, and then click on settings. 2. Check to see if the "Alerts" settings are gone 3. Search for "Alerts" in the settings search and make sure nothing appears other than "Security & Privacy > Security Alerts" which is unrelated to the "Alerts" section being removed ## **Screenshots/Recordings** NA ### **Before** #### Menu <img width="1840" alt="before_menu" src="https://github.com/user-attachments/assets/7f6d3a74-d312-4776-8840-9f1d68395bd0"> #### Search <img width="1840" alt="before_search" src="https://github.com/user-attachments/assets/ba65f255-5851-4b59-a798-f59368133658"> ### **After** #### Menu <img width="1840" alt="after_menu" src="https://github.com/user-attachments/assets/bbb23efc-98c2-4bb0-8e47-92af144dc670"> #### Search <img width="1840" alt="after_search" src="https://github.com/user-attachments/assets/5c5c4a34-a1c6-4642-9906-c8be5c7bd496"> ## **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 - [ ] 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/_locales/de/messages.json | 12 --- app/_locales/el/messages.json | 12 --- app/_locales/en/messages.json | 12 --- app/_locales/en_GB/messages.json | 12 --- app/_locales/es/messages.json | 12 --- app/_locales/es_419/messages.json | 12 --- app/_locales/fr/messages.json | 12 --- app/_locales/hi/messages.json | 12 --- app/_locales/id/messages.json | 12 --- app/_locales/it/messages.json | 12 --- app/_locales/ja/messages.json | 12 --- app/_locales/ko/messages.json | 12 --- app/_locales/ph/messages.json | 12 --- app/_locales/pt/messages.json | 12 --- app/_locales/pt_BR/messages.json | 12 --- app/_locales/ru/messages.json | 12 --- app/_locales/tl/messages.json | 12 --- app/_locales/tr/messages.json | 12 --- app/_locales/vi/messages.json | 12 --- app/_locales/zh_CN/messages.json | 12 --- app/_locales/zh_TW/messages.json | 12 --- .../files-to-convert.json | 3 - .../tests/settings/settings-search.spec.js | 26 ------ ui/helpers/constants/routes.ts | 3 - ui/helpers/constants/settings.js | 15 ---- ui/helpers/utils/settings-search.test.js | 8 -- ui/pages/settings/alerts-tab/alerts-tab.js | 87 ------------------- ui/pages/settings/alerts-tab/alerts-tab.scss | 38 -------- .../settings/alerts-tab/alerts-tab.stories.js | 14 --- .../settings/alerts-tab/alerts-tab.test.js | 42 --------- ui/pages/settings/alerts-tab/index.js | 1 - ui/pages/settings/index.scss | 1 - ui/pages/settings/settings.component.js | 8 -- ui/pages/settings/settings.container.js | 2 - ui/pages/settings/settings.stories.js | 2 - 35 files changed, 502 deletions(-) delete mode 100644 ui/pages/settings/alerts-tab/alerts-tab.js delete mode 100644 ui/pages/settings/alerts-tab/alerts-tab.scss delete mode 100644 ui/pages/settings/alerts-tab/alerts-tab.stories.js delete mode 100644 ui/pages/settings/alerts-tab/alerts-tab.test.js delete mode 100644 ui/pages/settings/alerts-tab/index.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 26a5018b00ad..b744300f8656 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Falsches Konto" }, - "alertSettingsUnconnectedAccount": { - "message": "Eine Webseite mit einem nicht verknüpften Konto durchsuchen" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Diese Warnung wird im Popup angezeigt, wenn Sie eine verbundene Webseite durchsuchen, aber das aktuell ausgewählte Konto ist nicht verbunden." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Wenn eine Webseite versucht, die entfernte window.web3 API zu verwenden" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Diese Benachrichtigung wird in einem Popup-Fenster angezeigt, wenn Sie eine Website besuchen, die versucht, die entfernte window.web3-API zu verwenden, und die dadurch möglicherweise beschädigt wird." - }, "alerts": { "message": "Benachrichtigungen" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index afeba57dfb6f..acafb24be5b6 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Λάθος λογαριασμός" }, - "alertSettingsUnconnectedAccount": { - "message": "Περιήγηση σε έναν ιστότοπο με έναν μη συνδεδεμένο επιλέγμενο λογαριασμό" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Αυτή η ειδοποίηση εμφανίζεται στο αναδυόμενο παράθυρο κατά την περιήγηση σε μια συνδεδεμένη web3 ιστοσελίδα, αλλά ο τρέχων επιλεγμένος λογαριασμός δεν είναι συνδεδεμένος." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Όταν μια ιστοσελίδα προσπαθεί να χρησιμοποιήσει το window.web3 API που έχει αφαιρεθεί" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Αυτή η ειδοποίηση εμφανίζεται στο αναδυόμενο παράθυρο όταν περιηγείστε σε μια ιστοσελίδα που προσπαθεί να χρησιμοποιήσει το window.web3 API που έχει αφαιρεθεί, και μπορεί, ως αποτέλεσμα, να μη λειτουργεί." - }, "alerts": { "message": "Ειδοποιήσεις" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a25b94722ea7..2924499a236d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -499,18 +499,6 @@ "alertReasonWrongAccount": { "message": "Wrong account" }, - "alertSettingsUnconnectedAccount": { - "message": "Browsing a website with an unconnected account selected" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." - }, - "alertSettingsWeb3ShimUsage": { - "message": "When a website tries to use the removed window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." - }, "alerts": { "message": "Alerts" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 153655e55f24..25cb6cd3df29 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -466,18 +466,6 @@ "alertReasonWrongAccount": { "message": "Wrong account" }, - "alertSettingsUnconnectedAccount": { - "message": "Browsing a website with an unconnected account selected" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." - }, - "alertSettingsWeb3ShimUsage": { - "message": "When a website tries to use the removed window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." - }, "alerts": { "message": "Alerts" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 6c1792ebc84d..a22d35c294f1 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Cuenta incorrecta" }, - "alertSettingsUnconnectedAccount": { - "message": "Explorando un sitio web con una cuenta no conectada seleccionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio conectado de Web3, pero la cuenta actualmente seleccionada no está conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Cuando un sitio web intenta utilizar la API de window.web3 que se eliminó" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio que intenta utilizar la API de window.web3 que se eliminó y que puede que no funcione." - }, "alerts": { "message": "Alertas" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 0bba1bd69551..4de37dd09e43 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -157,18 +157,6 @@ "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Explorando un sitio web con una cuenta no conectada seleccionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio conectado de Web3, pero la cuenta actualmente seleccionada no está conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Cuando un sitio web intenta utilizar la API de window.web3 que se eliminó" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio que intenta utilizar la API de window.web3 que se eliminó y que puede que no funcione." - }, "alerts": { "message": "Alertas" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 247bdaa359e0..9a1fbb3b8fb7 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Mauvais compte" }, - "alertSettingsUnconnectedAccount": { - "message": "Navigation sur un site Web avec un compte non connecté sélectionné" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Cette alerte s’affiche dans le pop-up lorsque vous naviguez sur un site web3 connecté, mais que le compte actuellement sélectionné n’est pas connecté." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Lorsqu’un site Web tente d’utiliser l’API window.web3 supprimée" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Cette alerte s’affiche dans le pop-up lorsque vous naviguez sur un site qui tente d’utiliser l’API window.web3 supprimée, et qui peut par conséquent être défaillant." - }, "alerts": { "message": "Alertes" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index bca5168630b8..1d7dcc92b7ed 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "गलत अकाउंट" }, - "alertSettingsUnconnectedAccount": { - "message": "जो कनेक्टेड नहीं है वह अकाउंट चुनकर कोई वेबसाइट ब्राउज़ करना" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "यह चेतावनी पॉपअप में तब दिखाई जाती है, जब आप कनेक्टेड web3 साइट ब्राउज़ कर रहे होते हैं, लेकिन वर्तमान में चुना गया अकाउंट कनेक्ट नहीं होता है।" - }, - "alertSettingsWeb3ShimUsage": { - "message": "जब कोई वेबसाइट हटाए गए window.web3 API का इस्तेमाल करने की कोशिश करती है" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "यह एलर्ट पॉपअप में तब दिखाया जाता है, जब आप ऐसी साइट ब्राउज़ कर रहे होते हैं, जो हटाए गए window.web3 API का इस्तेमाल करने की कोशिश करती है और परिणामस्वरूप उसमें गड़बड़ी आ सकती है।" - }, "alerts": { "message": "चेतावनियां" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 6e0d950450e9..77c7b8f5286a 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Akun salah" }, - "alertSettingsUnconnectedAccount": { - "message": "Memilih untuk menjelajahi situs web dengan akun yang tidak terhubung" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Peringatan ini ditampilkan dalam sembulan saat Anda menelusuri situs web3 yang terhubung, tetapi akun yang baru saja dipilih tidak terhubung." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Saat situs web mencoba menggunakan API window.web3 yang dihapus" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Peringatan ini ditampilkan dalam sembulan saat Anda menelusuri situs yang mencoba menggunakan API window.web3 yang dihapus, dan bisa mengakibatkan kerusakan." - }, "alerts": { "message": "Peringatan" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 71b07590d6d1..70e81c595852 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -209,18 +209,6 @@ "alertDisableTooltip": { "message": "Può essere cambiato in \"Impostazioni > Avvisi\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Navigazione su un sito con un account non connesso" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Questo avviso è mostrato nel popup quando stai visitando un sito Web3, ma l'account selezionato non è connesso al sito." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Quando un sito prova a usare la API window.web3 rimossa" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "L'avviso che viene mostrato nel popup quando stai visitando un sito che prova a usare la API window.web3 rimossa e che potrebbe non funzionare." - }, "alerts": { "message": "Avvisi" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 412708c194a3..c258d0947266 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "正しくないアカウント" }, - "alertSettingsUnconnectedAccount": { - "message": "選択した未接続のアカウントを使用してWebサイトをブラウズしています" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "このアラートは、選択中のアカウントが未接続のままweb3サイトを閲覧しているときにポップアップ表示されます。" - }, - "alertSettingsWeb3ShimUsage": { - "message": "Webサイトが削除済みのwindow.web3 APIを使用しようとした場合" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "このアラートは、削除されたwindow.web3 APIを使用しようとし、その結果破損している可能性があるサイトをブラウズした際、ポップアップに表示されます。" - }, "alerts": { "message": "アラート" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6d47331f15c9..6eaa179492f3 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "잘못된 계정" }, - "alertSettingsUnconnectedAccount": { - "message": "연결되지 않은 계정을 선택하여 웹사이트 탐색" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "이 경고는 연결된 web3 사이트를 탐색하고 있지만 현재 선택한 계정이 연결되지 않은 경우 팝업에 표시됩니다." - }, - "alertSettingsWeb3ShimUsage": { - "message": "웹사이트가 제거된 window.web3 API를 이용하는 경우" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "이 경고는 제거된 window.web3 API를 이용하려다가 작동이 정지된 사이트를 탐색할 때 팝업으로 표시됩니다." - }, "alerts": { "message": "경고" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 454facde8524..e12eb4379cf1 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -81,18 +81,6 @@ "alertDisableTooltip": { "message": "Mababago ito sa \"Mga Setting > Mga Alerto\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Napili ang pag-browse ng website nang may hindi nakakonektang account" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Makikita ang alertong ito sa popup kapag nagba-browse ka sa isang nakakonektang web3 site, pero hindi nakakonekta ang kasalukuyang napiling account." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Kapag sinubukan ng isang website na gamitin ang inalis na window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Makikita ang alertong ito sa popup kapag nagba-browse ka sa isang site na sumusubok na gamitin ang inalis na window.web3 API, at posibleng sira bilang resulta." - }, "alerts": { "message": "Mga Alerto" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 23d1bae93726..ca4f9a643a36 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Conta incorreta" }, - "alertSettingsUnconnectedAccount": { - "message": "Navegando em um site com uma conta não conectada selecionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site conectado da web3, mas a conta atualmente selecionada não estiver conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Quando um site tenta usar a API window.web3 removida" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site que tente usar a API window.web3 removida, e que consequentemente possa apresentar problemas." - }, "alerts": { "message": "Alertas" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 0f6efb88d348..b37bbb8af658 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -157,18 +157,6 @@ "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Navegando em um site com uma conta não conectada selecionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site conectado da web3, mas a conta atualmente selecionada não estiver conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Quando um site tenta usar a API window.web3 removida" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site que tente usar a API window.web3 removida, e que consequentemente possa apresentar problemas." - }, "alerts": { "message": "Alertas" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index f6a532043195..b06caab95019 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Неверный счет" }, - "alertSettingsUnconnectedAccount": { - "message": "Просмотр веб-сайта с выбранным неподключенным счетом" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Это предупреждение отображается во всплывающем окне, когда вы просматриваете подключенный сайт web3, но текущий выбранный счет не подключен." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Когда веб-сайт пытается использовать удаленный API window.web3" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Это предупреждение отображается во всплывающем окне, когда вы просматриваете сайт, который пытается использовать удаленный API window.web3 и из-за этого может не работать." - }, "alerts": { "message": "Оповещения" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 1d9fdc20819e..42ad155e2931 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Maling account" }, - "alertSettingsUnconnectedAccount": { - "message": "Nagba-browse sa isang website na may napiling hindi konektadong account" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Ang alertong ito ay ipinapakita sa popup kapag nagba-browse ka ng konektadong web3 site, ngunit ang kasalukuyang napiling account ay hindi nakakonekta." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Kapag sinubukan ng isang website na gamitin ang inalis na window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Ang alertong ito ay ipinapakita sa popup kapag nagba-browse ka sa isang site na sumusubok na gamitin ang inalis na window.web3 API, at maaaring masira bilang resulta." - }, "alerts": { "message": "Mga Alerto" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 35ac8fa29c7d..db5dab6482f3 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Yanlış hesap" }, - "alertSettingsUnconnectedAccount": { - "message": "Bağlı olmayan bir hesap ile bir web sitesine göz atma seçildi" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Bu uyarı, bağlı bir web3 sitesinde gezdiğinizde gösterilir ancak şu anda seçili hesap bağlı değildir." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Bir web sitesi kaldırılmış window.web3 API'sini kullanmaya çalıştığında" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Bu uyarı, kaldırılmış window.web3 API kullanmaya çalışan bir ve bunun sonucu olarak bozulmuş olabilen bir sitede gezindiğinizde açılır pencerede gösterilir." - }, "alerts": { "message": "Uyarılar" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 29f9017003a9..3623c8b86a2e 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "Tài khoản không đúng" }, - "alertSettingsUnconnectedAccount": { - "message": "Đang duyệt trang web khi chọn một tài khoản không được kết nối" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Cảnh báo này hiển thị trong cửa sổ bật lên khi bạn đang duyệt một trang web đã được kết nối trên web3, nhưng tài khoản đang chọn không được kết nối." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Khi một trang web cố dùng API window.web3 đã bị xóa" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Cảnh báo này hiển thị trong cửa sổ bật lên khi bạn đang duyệt một trang web cố sử dụng API window.web3 đã bị xóa nên có thể bị lỗi." - }, "alerts": { "message": "Cảnh báo" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 0bed963cf99f..3b78771d5823 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -447,18 +447,6 @@ "alertReasonWrongAccount": { "message": "错误账户" }, - "alertSettingsUnconnectedAccount": { - "message": "浏览网站时选择的账户未连接" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "当您在浏览已连接的Web3网站,但当前所选择的账户没有连接时,此提醒会在弹出的窗口中显示。" - }, - "alertSettingsWeb3ShimUsage": { - "message": "当网站尝试使用已经删除的 window.web3 API 时" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "当您浏览尝试使用已删除的 window.web3 API 并因此可能出现故障的网站时,此警报会显示在弹出窗口中。" - }, "alerts": { "message": "提醒" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index dee06a7aef16..32e98ed12288 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -77,18 +77,6 @@ "alertDisableTooltip": { "message": "這可以在「設定 > 提醒」裡變更" }, - "alertSettingsUnconnectedAccount": { - "message": "選擇尚未連結的帳戶瀏覽一個網站時" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "當您瀏覽一個使用 web3 的網站,但目前選擇的帳戶沒有連結時,這個提醒會顯示在一個彈跳視窗。" - }, - "alertSettingsWeb3ShimUsage": { - "message": "當一個網站試著使用已經移除的 window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "當您瀏覽一個嘗試使用已經移除的 window.web3 API 的網站,可能會因此故障時,這個提醒會顯示在一個彈跳視窗。" - }, "alerts": { "message": "提醒" }, diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index ea3015d4c1ba..7ffbd68472d1 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -1415,9 +1415,6 @@ "ui/pages/settings/advanced-tab/advanced-tab.container.js", "ui/pages/settings/advanced-tab/advanced-tab.stories.js", "ui/pages/settings/advanced-tab/index.js", - "ui/pages/settings/alerts-tab/alerts-tab.js", - "ui/pages/settings/alerts-tab/alerts-tab.test.js", - "ui/pages/settings/alerts-tab/index.js", "ui/pages/settings/contact-list-tab/add-contact/add-contact.component.js", "ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js", "ui/pages/settings/contact-list-tab/add-contact/index.js", diff --git a/test/e2e/tests/settings/settings-search.spec.js b/test/e2e/tests/settings/settings-search.spec.js index fb67fbffd23b..bf27c591bc29 100644 --- a/test/e2e/tests/settings/settings-search.spec.js +++ b/test/e2e/tests/settings/settings-search.spec.js @@ -122,32 +122,6 @@ describe('Settings Search', function () { }, ); }); - it('should find element inside the Alerts tab', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - await openMenuSafe(driver); - - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.fill('#search-settings', settingsSearch.alerts); - - // Check if element redirects to the correct page - const page = 'Alerts'; - await driver.clickElement({ text: page, tag: 'span' }); - assert.equal( - await driver.isElementPresent({ text: page, tag: 'div' }), - true, - `${settingsSearch.alerts} item does not redirect to ${page} view`, - ); - }, - ); - }); it('should find element inside the Experimental tab', async function () { await withFixtures( { diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index eec9075a64d8..8052ad867084 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -36,9 +36,6 @@ PATH_NAME_MAP[SECURITY_ROUTE] = 'Security Settings Page'; export const ABOUT_US_ROUTE = '/settings/about-us'; PATH_NAME_MAP[ABOUT_US_ROUTE] = 'About Us Page'; -export const ALERTS_ROUTE = '/settings/alerts'; -PATH_NAME_MAP[ALERTS_ROUTE] = 'Alerts Settings Page'; - export const NETWORKS_ROUTE = '/settings/networks'; PATH_NAME_MAP[NETWORKS_ROUTE] = 'Network Settings Page'; diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index 89cca83f27cf..c22b0cbcf183 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,7 +1,6 @@ /* eslint-disable @metamask/design-tokens/color-no-hex*/ import { IconName } from '../../components/component-library'; import { - ALERTS_ROUTE, ADVANCED_ROUTE, SECURITY_ROUTE, GENERAL_ROUTE, @@ -331,20 +330,6 @@ const SETTINGS_CONSTANTS = [ route: `${SECURITY_ROUTE}#delete-metametrics-data`, icon: 'fa fa-lock', }, - { - tabMessage: (t) => t('alerts'), - sectionMessage: (t) => t('alertSettingsUnconnectedAccount'), - descriptionMessage: (t) => t('alertSettingsUnconnectedAccount'), - route: `${ALERTS_ROUTE}#unconnected-account`, - iconName: IconName.Notification, - }, - { - tabMessage: (t) => t('alerts'), - sectionMessage: (t) => t('alertSettingsWeb3ShimUsage'), - descriptionMessage: (t) => t('alertSettingsWeb3ShimUsage'), - route: `${ALERTS_ROUTE}#web3-shimusage`, - icon: 'fa fa-bell', - }, { tabMessage: (t) => t('networks'), sectionMessage: (t) => t('mainnet'), diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index c3d07073a7d3..cc7b875d8c5e 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -86,10 +86,6 @@ const t = (key) => { return 'Participate in MetaMetrics to help us make MetaMask better'; case 'alerts': return 'Alerts'; - case 'alertSettingsUnconnectedAccount': - return 'Browsing a website with an unconnected account selected'; - case 'alertSettingsWeb3ShimUsage': - return 'When a website tries to use the removed window.web3 API'; case 'networks': return 'Networks'; case 'mainnet': @@ -177,10 +173,6 @@ describe('Settings Search Utils', () => { ).toStrictEqual(21); }); - it('returns "Alerts" section count', () => { - expect(getNumberOfSettingRoutesInTab(t, t('alerts'))).toStrictEqual(2); - }); - it('returns "Network" section count', () => { expect(getNumberOfSettingRoutesInTab(t, t('networks'))).toStrictEqual(7); }); diff --git a/ui/pages/settings/alerts-tab/alerts-tab.js b/ui/pages/settings/alerts-tab/alerts-tab.js deleted file mode 100644 index 7523c68f6fa1..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; - -import { AlertTypes } from '../../../../shared/constants/alerts'; -import Tooltip from '../../../components/ui/tooltip'; -import ToggleButton from '../../../components/ui/toggle-button'; -import { setAlertEnabledness } from '../../../store/actions'; -import { getAlertEnabledness } from '../../../ducks/metamask/metamask'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { handleSettingsRefs } from '../../../helpers/utils/settings-search'; -import { Icon, IconName } from '../../../components/component-library'; - -const AlertSettingsEntry = ({ alertId, description, title }) => { - const t = useI18nContext(); - const settingsRefs = useRef(); - - useEffect(() => { - handleSettingsRefs(t, t('alerts'), settingsRefs); - }, [settingsRefs, t]); - - const isEnabled = useSelector((state) => getAlertEnabledness(state)[alertId]); - - return ( - <> - <div ref={settingsRefs} className="alerts-tab__item"> - <span>{title}</span> - <div className="alerts-tab__description-container"> - <Tooltip - position="top" - title={description} - wrapperClassName="alerts-tab__description" - > - <Icon - name={IconName.Info} - className="alerts-tab__description__icon" - /> - </Tooltip> - <ToggleButton - offLabel={t('off')} - onLabel={t('on')} - onToggle={() => setAlertEnabledness(alertId, !isEnabled)} - value={isEnabled} - /> - </div> - </div> - </> - ); -}; - -AlertSettingsEntry.propTypes = { - alertId: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, -}; - -const AlertsTab = () => { - const t = useI18nContext(); - - const alertConfig = { - [AlertTypes.unconnectedAccount]: { - title: t('alertSettingsUnconnectedAccount'), - description: t('alertSettingsUnconnectedAccountDescription'), - }, - [AlertTypes.web3ShimUsage]: { - title: t('alertSettingsWeb3ShimUsage'), - description: t('alertSettingsWeb3ShimUsageDescription'), - }, - }; - - return ( - <div className="alerts-tab__body"> - {Object.entries(alertConfig).map( - ([alertId, { title, description }], _) => ( - <AlertSettingsEntry - alertId={alertId} - description={description} - key={alertId} - title={title} - /> - ), - )} - </div> - ); -}; - -export default AlertsTab; diff --git a/ui/pages/settings/alerts-tab/alerts-tab.scss b/ui/pages/settings/alerts-tab/alerts-tab.scss deleted file mode 100644 index 0e8ee8f83983..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.scss +++ /dev/null @@ -1,38 +0,0 @@ -@use "design-system"; - -.alerts-tab { - &__body { - @include design-system.H6; - - display: grid; - grid-template-columns: 8fr 30px max-content; - grid-template-rows: 1fr 1fr; - align-items: center; - display: block; - } - - &__description-container { - display: flex; - } - - &__description-container > * { - padding: 0 8px; - } - - &__description { - display: flex; - align-items: center; - - &__icon { - color: var(--color-icon-alternative); - } - } - - &__item { - border-bottom: 1px solid var(--color-border-muted); - padding: 16px 32px; - display: flex; - justify-content: space-between; - align-items: center; - } -} diff --git a/ui/pages/settings/alerts-tab/alerts-tab.stories.js b/ui/pages/settings/alerts-tab/alerts-tab.stories.js deleted file mode 100644 index 65b9dfd12d11..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.stories.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import AlertsTab from './alerts-tab'; - -export default { - title: 'Components/UI/Pages/AlertsTab ', - - component: AlertsTab, -}; - -export const DefaultAlertsTab = () => { - return <AlertsTab />; -}; - -DefaultAlertsTab.storyName = 'Default'; diff --git a/ui/pages/settings/alerts-tab/alerts-tab.test.js b/ui/pages/settings/alerts-tab/alerts-tab.test.js deleted file mode 100644 index 8750c4a9a3ec..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../test/jest'; -import { AlertTypes } from '../../../../shared/constants/alerts'; -import AlertsTab from '.'; - -const mockSetAlertEnabledness = jest.fn(); - -jest.mock('../../../store/actions', () => ({ - setAlertEnabledness: (...args) => mockSetAlertEnabledness(...args), -})); - -describe('Alerts Tab', () => { - const store = configureMockStore([])({ - metamask: { - alertEnabledness: { - unconnectedAccount: false, - web3ShimUsage: false, - }, - }, - }); - - it('calls setAlertEnabledness with the correct params method when the toggles are clicked', () => { - renderWithProvider(<AlertsTab />, store); - - expect(mockSetAlertEnabledness.mock.calls).toHaveLength(0); - fireEvent.click(screen.getAllByRole('checkbox')[0]); - expect(mockSetAlertEnabledness.mock.calls).toHaveLength(1); - expect(mockSetAlertEnabledness.mock.calls[0][0]).toBe( - AlertTypes.unconnectedAccount, - ); - expect(mockSetAlertEnabledness.mock.calls[0][1]).toBe(true); - - fireEvent.click(screen.getAllByRole('checkbox')[1]); - expect(mockSetAlertEnabledness.mock.calls).toHaveLength(2); - expect(mockSetAlertEnabledness.mock.calls[1][0]).toBe( - AlertTypes.web3ShimUsage, - ); - expect(mockSetAlertEnabledness.mock.calls[1][1]).toBe(true); - }); -}); diff --git a/ui/pages/settings/alerts-tab/index.js b/ui/pages/settings/alerts-tab/index.js deleted file mode 100644 index f6aa526da73e..000000000000 --- a/ui/pages/settings/alerts-tab/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './alerts-tab'; diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index b4cea4bef630..f57e1c310998 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -1,7 +1,6 @@ @use "design-system"; @import 'info-tab/index'; -@import 'alerts-tab/alerts-tab'; @import 'developer-options-tab/index'; @import 'networks-tab/index'; @import 'settings-tab/index'; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index 72a4141512ee..2f33cd78f2e1 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -5,7 +5,6 @@ import classnames from 'classnames'; import TabBar from '../../components/app/tab-bar'; import { - ALERTS_ROUTE, ADVANCED_ROUTE, SECURITY_ROUTE, GENERAL_ROUTE, @@ -45,7 +44,6 @@ import MetafoxLogo from '../../components/ui/metafox-logo'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; import SettingsTab from './settings-tab'; -import AlertsTab from './alerts-tab'; import AdvancedTab from './advanced-tab'; import InfoTab from './info-tab'; import SecurityTab from './security-tab'; @@ -316,11 +314,6 @@ class SettingsPage extends PureComponent { icon: <i className="fa fa-lock" />, key: SECURITY_ROUTE, }, - { - content: t('alerts'), - icon: <Icon name={IconName.Notification} />, - key: ALERTS_ROUTE, - }, { content: t('experimental'), icon: <Icon name={IconName.Flask} />, @@ -376,7 +369,6 @@ class SettingsPage extends PureComponent { /> <Route exact path={ABOUT_US_ROUTE} component={InfoTab} /> <Route exact path={ADVANCED_ROUTE} component={AdvancedTab} /> - <Route exact path={ALERTS_ROUTE} component={AlertsTab} /> <Route exact path={ADD_NETWORK_ROUTE} diff --git a/ui/pages/settings/settings.container.js b/ui/pages/settings/settings.container.js index 638b6aea23af..1ee2bfb83941 100644 --- a/ui/pages/settings/settings.container.js +++ b/ui/pages/settings/settings.container.js @@ -15,7 +15,6 @@ import { import { ABOUT_US_ROUTE, ADVANCED_ROUTE, - ALERTS_ROUTE, CONTACT_LIST_ROUTE, CONTACT_ADD_ROUTE, CONTACT_EDIT_ROUTE, @@ -39,7 +38,6 @@ const ROUTES_TO_I18N_KEYS = { [ADD_NETWORK_ROUTE]: 'networks', [ADD_POPULAR_CUSTOM_NETWORK]: 'addNetwork', [ADVANCED_ROUTE]: 'advanced', - [ALERTS_ROUTE]: 'alerts', [CONTACT_ADD_ROUTE]: 'newContact', [CONTACT_EDIT_ROUTE]: 'editContact', [CONTACT_LIST_ROUTE]: 'contacts', diff --git a/ui/pages/settings/settings.stories.js b/ui/pages/settings/settings.stories.js index f33cd3bba41f..a23cd3c1e1cf 100644 --- a/ui/pages/settings/settings.stories.js +++ b/ui/pages/settings/settings.stories.js @@ -5,7 +5,6 @@ import { MemoryRouter, withRouter } from 'react-router-dom'; import { ABOUT_US_ROUTE, ADVANCED_ROUTE, - ALERTS_ROUTE, CONTACT_ADD_ROUTE, CONTACT_EDIT_ROUTE, CONTACT_LIST_ROUTE, @@ -33,7 +32,6 @@ export default { const ROUTES_TO_I18N_KEYS = { [ABOUT_US_ROUTE]: 'about', [ADVANCED_ROUTE]: 'advanced', - [ALERTS_ROUTE]: 'alerts', [CONTACT_ADD_ROUTE]: 'newContact', [CONTACT_EDIT_ROUTE]: 'editContact', [CONTACT_LIST_ROUTE]: 'contacts', From 61adc783c21f141ec99da86fa62af94d18e976e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= <apregadas@gmail.com> Date: Thu, 10 Oct 2024 16:28:33 +0100 Subject: [PATCH 116/226] feat: adds the new default settings view to onboarding (#24562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: MetaMask/MetaMask-planning#2080 -- <br class="Apple-interchange-newline"> ## **Description** Introduces the new Default Settings view and Congratulations views depending on the fact if user imported a wallet or created a new one. **User imported a wallet:** ![Screenshot 2024-06-03 at 16 08 44](https://github.com/MetaMask/metamask-extension/assets/1125631/c9784cbb-e4e2-4557-b6f6-527d9df91fa5) **User created a new wallet and backed up the seed phrase:** ![Screenshot 2024-06-03 at 16 11 13](https://github.com/MetaMask/metamask-extension/assets/1125631/8045e007-63bb-4aac-915c-d908d03b52a1) **User created a new wallet and didn’t back up the seed phrase:** ![Screenshot 2024-06-03 at 16 09 37](https://github.com/MetaMask/metamask-extension/assets/1125631/005d34ec-587a-4978-92d3-0ed14d64f9c8) **Inside look on the Default Settings:** ![Screenshot 2024-06-03 at 16 13 49](https://github.com/MetaMask/metamask-extension/assets/1125631/c573c7ba-36f4-4b46-8124-9c2091018356) ![Screenshot 2024-06-03 at 16 14 00](https://github.com/MetaMask/metamask-extension/assets/1125631/733ab5db-6aec-43a9-8da8-f1d543e5d3a1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask 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: vinnyhoward <vincenguyenhoward@gmail.com> Co-authored-by: David Walsh <davidwalsh83@gmail.com> --- app/_locales/de/messages.json | 42 - app/_locales/el/messages.json | 42 - app/_locales/en/messages.json | 95 +- app/_locales/es/messages.json | 42 - app/_locales/es_419/messages.json | 29 - app/_locales/fr/messages.json | 42 - app/_locales/hi/messages.json | 42 - app/_locales/id/messages.json | 42 - app/_locales/ja/messages.json | 42 - app/_locales/ko/messages.json | 42 - app/_locales/pt/messages.json | 42 - app/_locales/pt_BR/messages.json | 33 - app/_locales/ru/messages.json | 42 - app/_locales/tl/messages.json | 42 - app/_locales/tr/messages.json | 42 - app/_locales/vi/messages.json | 42 - app/_locales/zh_CN/messages.json | 42 - shared/constants/metametrics.ts | 1 + test/e2e/helpers.js | 4 +- test/e2e/tests/network/multi-rpc.spec.ts | 31 +- test/e2e/tests/onboarding/onboarding.spec.js | 183 ++-- .../tests/privacy/basic-functionality.spec.js | 47 +- .../onboarding/wallet-created.test.tsx | 16 +- .../incoming-transaction-toggle.test.js.snap | 2 +- .../incoming-transaction-toggle.tsx | 4 +- .../creation-successful.js | 221 +++-- .../creation-successful.test.js | 94 +- .../creation-successful/index.scss | 37 - .../pin-extension/pin-extension.js | 48 +- .../pin-extension/pin-extension.test.js | 16 +- .../privacy-settings/index.scss | 49 +- .../privacy-settings/privacy-settings.js | 842 +++++++++++------- .../privacy-settings/privacy-settings.test.js | 110 ++- .../privacy-settings/setting.js | 9 +- .../__snapshots__/security-tab.test.js.snap | 2 +- 35 files changed, 1174 insertions(+), 1287 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index b744300f8656..6177fe229bfb 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Wenn Ihre Transaktion in den Block aufgenommen wird, wird die Differenz zwischen Ihrer maximalen Grundgebühr und der tatsächlichen Grundgebühr erstattet. Der Gesamtbetrag wird berechnet als maximale Grundgebühr (in GWEI) * Gas-Limit." }, - "advancedConfiguration": { - "message": "Erweiterte Einstellungen" - }, "advancedDetailsDataDesc": { "message": "Daten" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Beta-Nutzungsbedingungen" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta kann Ihre geheime Wiederherstellungsphrase nicht wiederherstellen." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta wird Sie nie nach Ihrer geheimen Wiederherstellungsphrase fragen." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON Datei", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional pinnen" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Phishing-Warnungen basieren auf der Kommunikation mit $1. jsDeliver hat Zugriff auf Ihre IP-Adresse. $2 ansehen.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 T", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Abgelehnt" }, - "remember": { - "message": "Erinnern:" - }, "remove": { "message": "Entfernen" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepolia-Testnetzwerk" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask nutzt diese vertrauenswürdigen Dienstleistungen von Drittanbietern, um die Benutzerfreundlichkeit und Sicherheit der Produkte zu verbessern." - }, "setApprovalForAll": { "message": "Erlaubnis für alle erteilen" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "unsere Hardware-Wallet-Verbindungsanleitung" }, - "walletCreationSuccessDetail": { - "message": "Sie haben Ihre Wallet erfolgreich geschützt. Halten Sie Ihre geheime Wiederherstellungsphrase sicher und geheim -- es liegt in Ihrer Verantwortung!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask kann Ihre geheime Wiederherstellungsphrase nicht wiederherstellen." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask-Team wird nie nach Ihrer geheimen Wiederherstellungsphrase fragen." - }, - "walletCreationSuccessReminder3": { - "message": "$1 mit jemandem oder riskieren Sie, dass Ihre Gelder gestohlen werden.", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Geben Sie niemals Ihre geheime Wiederherstellungsphrase an andere weiter", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Wallet-Erstellung erfolgreich" - }, "wantToAddThisNetwork": { "message": "Möchten Sie dieses Netzwerk hinzufügen?" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index acafb24be5b6..c7ce18893b4d 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Όταν η συναλλαγή σας συμπεριληφθεί στο μπλοκ, οποιαδήποτε διαφορά μεταξύ της μέγιστης βασικής χρέωσής σας και της πραγματικής βασικής χρέωσής θα επιστραφεί. Το συνολικό ποσό υπολογίζεται ως μέγιστο βασικό τέλος (σε GWEI) * όριο τελών συναλλαγής." }, - "advancedConfiguration": { - "message": "Προηγμένη ρύθμιση παραμέτρων" - }, "advancedDetailsDataDesc": { "message": "Δεδομένα" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Όροι Χρήσης της Δοκιμαστικής Έκδοσης" }, - "betaWalletCreationSuccessReminder1": { - "message": "Η δοκιμαστική έκδοση του MetaMask δεν μπορεί να ανακτήσει τη Μυστική Φράση Ανάκτησής σας." - }, - "betaWalletCreationSuccessReminder2": { - "message": "Η δοκιμαστική έκδοση του MetaMask δεν θα σας ζητήσει ποτέ τη Μυστική Φράση Ανάκτησής σας." - }, "billionAbbreviation": { "message": "Δ", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Αρχείο JSON", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Καρφιτσώστε το MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Οι ειδοποιήσεις ανίχνευσης για phishing βασίζονται στην επικοινωνία με το $1. Το jsDeliver θα έχει πρόσβαση στη διεύθυνση IP σας. Δείτε $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1Η", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Απορρίφθηκε" }, - "remember": { - "message": "Να θυμάστε:" - }, "remove": { "message": "Κατάργηση" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Δίκτυο δοκιμών Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "Το MetaMask χρησιμοποιεί αυτές τις αξιόπιστες υπηρεσίες τρίτων για να ενισχύσει τη χρηστικότητα και την ασφάλεια των προϊόντων." - }, "setApprovalForAll": { "message": "Ρύθμιση έγκρισης για όλους" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "ο οδηγός μας σύνδεσης πορτοφολιού υλικού" }, - "walletCreationSuccessDetail": { - "message": "Προστατεύσατε με επιτυχία το πορτοφόλι σας. Διατηρήστε τη Μυστική Φράση Ανάκτησης ασφαλής και μυστική - είναι δική σας ευθύνη!" - }, - "walletCreationSuccessReminder1": { - "message": "Το MetaMask δεν μπορεί να ανακτήσει τη Μυστική Φράση Ανάκτησής σας." - }, - "walletCreationSuccessReminder2": { - "message": "Το MetaMask δεν θα σας ζητήσει ποτέ τη Μυστική Φράση Ανάκτησής σας." - }, - "walletCreationSuccessReminder3": { - "message": "$1 με οποιονδήποτε ή να διακινδυνεύστε τα χρήματά σας να κλαπούν", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Ποτέ μην μοιράζεστε τη Μυστική Φράση Ανάκτησης σας", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Επιτυχής δημιουργία πορτοφολιού" - }, "wantToAddThisNetwork": { "message": "Θέλετε να προσθέσετε αυτό το δίκτυο;" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2924499a236d..e8b2625103d3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -373,9 +373,6 @@ "advancedBaseGasFeeToolTip": { "message": "When your transaction gets included in the block, any difference between your max base fee and the actual base fee will be refunded. Total amount is calculated as max base fee (in GWEI) * gas limit." }, - "advancedConfiguration": { - "message": "Advanced configuration" - }, "advancedDetailsDataDesc": { "message": "Data" }, @@ -638,6 +635,12 @@ "assetOptions": { "message": "Asset options" }, + "assets": { + "message": "Assets" + }, + "assetsDescription": { + "message": "Autodetect tokens in your wallet, display NFTs, and get batched account balance updates" + }, "attemptSendingAssets": { "message": "You may lose your assets if you try to send them from another network. Transfer funds safely between networks by using a bridge." }, @@ -752,12 +755,6 @@ "betaTerms": { "message": "Beta Terms of use" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta can’t recover your Secret Recovery Phrase." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta will never ask you for your Secret Recovery Phrase." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -1093,6 +1090,9 @@ "confusingEnsDomain": { "message": "We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam." }, + "congratulations": { + "message": "Congratulations!" + }, "connect": { "message": "Connect" }, @@ -1544,6 +1544,12 @@ "defaultRpcUrl": { "message": "Default RPC URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMask uses default settings to best balance safety and ease of use. Change these settings to further increase your privacy." + }, + "defaultSettingsTitle": { + "message": "Default settings" + }, "delete": { "message": "Delete" }, @@ -2193,6 +2199,9 @@ "generalCameraErrorTitle": { "message": "Something went wrong...." }, + "generalDescription": { + "message": "Sync settings across devices, select network preferences, and track token data" + }, "genericExplorerView": { "message": "View account on $1" }, @@ -2356,6 +2365,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "If you get locked out of the app or get a new device, you will lose your funds. Be sure to back up your Secret Recovery Phrase in $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Ignore all" }, @@ -2606,13 +2619,14 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON File", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Keep a reminder of your Secret Recovery Phrase somewhere safe. If you lose it, no one can help you get it back. Even worse, you won’t be able access to your wallet ever again. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Account name" }, @@ -2671,6 +2685,9 @@ "message": "Learn how to $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Learn how" + }, "learnMore": { "message": "learn more" }, @@ -2678,6 +2695,9 @@ "message": "Want to $1 about gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Learn more about privacy best practices." + }, "learnMoreKeystone": { "message": "Learn More" }, @@ -2829,6 +2849,9 @@ "message": "Make sure nobody is looking", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Manage default settings" + }, "marketCap": { "message": "Market cap" }, @@ -3702,10 +3725,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Pin MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -4369,8 +4388,11 @@ "rejected": { "message": "Rejected" }, - "remember": { - "message": "Remember:" + "rememberSRPIfYouLooseAccess": { + "message": "Remember, if you lose your Secret Recovery Phrase, you lose access to your wallet. $1 to keep this set of words safe so you can always access your funds." + }, + "reminderSet": { + "message": "Reminder set!" }, "remove": { "message": "Remove" @@ -4642,6 +4664,12 @@ "securityAndPrivacy": { "message": "Security & privacy" }, + "securityDescription": { + "message": "Reduce your chances of joining unsafe networks and protect your accounts" + }, + "securityPrivacyPath": { + "message": "Settings > Security & Privacy." + }, "securityProviderPoweredBy": { "message": "Powered by $1", "description": "The security provider that is providing data" @@ -4808,9 +4836,6 @@ "sepolia": { "message": "Sepolia test network" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask uses these trusted third-party services to enhance product usability and safety." - }, "setApprovalForAll": { "message": "Set approval for all" }, @@ -4827,6 +4852,9 @@ "settings": { "message": "Settings" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Settings are optimised for ease of use and security. Change these any time." + }, "settingsSearchMatchingNotFound": { "message": "No matching results found." }, @@ -6545,25 +6573,9 @@ "walletConnectionGuide": { "message": "our hardware wallet connection guide" }, - "walletCreationSuccessDetail": { - "message": "You’ve successfully protected your wallet. Keep your Secret Recovery Phrase safe and secret -- it’s your responsibility!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask can’t recover your Secret Recovery Phrase." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask will never ask you for your Secret Recovery Phrase." - }, - "walletCreationSuccessReminder3": { - "message": "$1 with anyone or risk your funds being stolen", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Never share your Secret Recovery Phrase", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Wallet creation successful" + "walletProtectedAndReadyToUse": { + "message": "Your wallet is protected and ready to use. You can find your Secret Recovery Phrase in $1 ", + "description": "$1 is the menu path to be shown with font weight bold" }, "wantToAddThisNetwork": { "message": "Want to add this network?" @@ -6681,6 +6693,9 @@ "yourTransactionJustConfirmed": { "message": "We weren't able to cancel your transaction before it was confirmed on the blockchain." }, + "yourWalletIsReady": { + "message": "Your wallet is ready" + }, "zeroGasPriceOnSpeedUpError": { "message": "Zero gas price on speed up" } diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index a22d35c294f1..0b4dc1432ac8 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Cuando su transacción se incluya en el bloque, se reembolsará cualquier diferencia entre su tarifa base máxima y la tarifa base real. El importe total se calcula como tarifa base máxima (en GWEI) * límite de gas." }, - "advancedConfiguration": { - "message": "Configuración avanzada" - }, "advancedDetailsDataDesc": { "message": "Datos" }, @@ -697,12 +694,6 @@ "betaTerms": { "message": "Términos de uso de beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask beta no puede recuperar su frase secreta de recuperación." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask beta nunca le pedirá su frase secreta de recuperación." - }, "billionAbbreviation": { "message": "mm", "description": "Shortened form of 'billion'" @@ -2397,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Archivo JSON", "description": "format for importing an account" @@ -3449,10 +3437,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Fijar MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Las alertas de detección de phishing se basan en la comunicación con $1. jsDeliver tendrá acceso a su dirección IP. Ver 2$.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 d", "description": "Shortened form of '1 day'" @@ -4093,9 +4077,6 @@ "rejected": { "message": "Rechazado" }, - "remember": { - "message": "Recuerde:" - }, "remove": { "message": "Quitar" }, @@ -4503,9 +4484,6 @@ "sepolia": { "message": "Red de prueba Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask utiliza estos servicios de terceros de confianza para mejorar la usabilidad y la seguridad de los productos." - }, "setApprovalForAll": { "message": "Establecer aprobación para todos" }, @@ -6157,26 +6135,6 @@ "walletConnectionGuide": { "message": "nuestra guía de conexión del monedero físico" }, - "walletCreationSuccessDetail": { - "message": "Ha protegido con éxito su monedero. Mantenga su frase secreta de recuperación a salvo y en secreto: ¡es su responsabilidad!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask no puede recuperar su frase secreta de recuperación." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask nunca le pedirá su frase secreta de recuperación." - }, - "walletCreationSuccessReminder3": { - "message": "$1 con nadie o se arriesga a que le roben los fondos", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca comparta su frase secreta de recuperación", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Creación exitosa del monedero" - }, "wantToAddThisNetwork": { "message": "¿Desea añadir esta red?" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 4de37dd09e43..cd980aaa99c2 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1033,9 +1033,6 @@ "invalidSeedPhrase": { "message": "Frase secreta de recuperación no válida" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Archivo JSON", "description": "format for importing an account" @@ -1555,9 +1552,6 @@ "rejected": { "message": "Rechazado" }, - "remember": { - "message": "Recuerde:" - }, "remove": { "message": "Quitar" }, @@ -1720,9 +1714,6 @@ "message": "Enviando $1", "description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask utiliza estos servicios de terceros de confianza para mejorar la usabilidad y la seguridad de los productos." - }, "settings": { "message": "Configuración" }, @@ -2442,26 +2433,6 @@ "walletConnectionGuide": { "message": "nuestra guía de conexión de la cartera de hardware" }, - "walletCreationSuccessDetail": { - "message": "Ha protegido con éxito su cartera. Mantenga su frase secreta de recuperación a salvo y en secreto: ¡es su responsabilidad!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask no puede recuperar su frase secreta de recuperación." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask nunca le pedirá su frase secreta de recuperación." - }, - "walletCreationSuccessReminder3": { - "message": "$1 con nadie o se arriesga a que le roben los fondos", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca comparta su frase secreta de recuperación", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Creación exitosa de la cartera" - }, "web3ShimUsageNotification": { "message": "Parece que el sitio web actual intentó utilizar la API de window.web3 que se eliminó. Si el sitio no funciona, haga clic en $1 para obtener más información.", "description": "$1 is a clickable link." diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 9a1fbb3b8fb7..780300aebe08 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Lorsque votre transaction est intégrée au bloc, toute différence entre vos frais de base maximaux et les frais de base réels vous sera remboursée. Le montant total est calculé comme suit : frais de base maximaux (en GWEI) × limite de carburant." }, - "advancedConfiguration": { - "message": "Configuration avancée" - }, "advancedDetailsDataDesc": { "message": "Données" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Conditions d’utilisation de la version bêta" }, - "betaWalletCreationSuccessReminder1": { - "message": "La version bêta de MetaMask ne peut pas retrouver votre phrase secrète de récupération." - }, - "betaWalletCreationSuccessReminder2": { - "message": "La version bêta de MetaMask ne vous demandera jamais votre phrase secrète de récupération." - }, "billionAbbreviation": { "message": "Mrd", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Fichier JSON", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Épingler MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Les alertes de détection d’hameçonnage reposent sur la communication avec $1. jsDeliver aura accès à votre adresse IP. Voir $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 j", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Rejeté" }, - "remember": { - "message": "Rappel :" - }, "remove": { "message": "Supprimer" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Réseau de test Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask utilise ces services tiers de confiance pour améliorer la convivialité et la sécurité des produits." - }, "setApprovalForAll": { "message": "Définir l’approbation pour tous" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "notre guide de connexion des portefeuilles matériels" }, - "walletCreationSuccessDetail": { - "message": "Votre portefeuille est bien protégé. Conservez votre phrase secrète de récupération en sécurité et en toute discrétion. C’est votre responsabilité !" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask ne peut pas restaurer votre phrase secrète de récupération." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask ne vous demandera jamais votre phrase secrète de récupération." - }, - "walletCreationSuccessReminder3": { - "message": "$1 avec n’importe qui, sinon vous risquez de voir vos fonds subtilisés", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Ne partagez jamais votre phrase secrète de récupération", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Portefeuille créé avec succès" - }, "wantToAddThisNetwork": { "message": "Voulez-vous ajouter ce réseau ?" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 1d7dcc92b7ed..8a3744a255f5 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "जब आपका ट्रांसेक्शन ब्लॉक में शामिल हो जाता है, तो आपके अधिकतम बेस फ़ीस और वास्तविक बेस फ़ीस के बीच का कोई भी अंतर वापस कर दिया जाता है। कुल अमाउंट को अधिकतम बेस फ़ीस (GWEI में) * गैस लिमिट के रुप में कैलकुलेट किया जाता है।" }, - "advancedConfiguration": { - "message": "एडवांस्ड कॉन्फ़िगरेशन" - }, "advancedDetailsDataDesc": { "message": "डेटा" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "बीटा के इस्तेमाल की शर्तें" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask बीटा आपका सीक्रेट रिकवरी फ्रेज़ रिकवर नहीं कर सकता।" - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask बीटा आपसे आपका गुप्त रिकवरी वाक्यांश कभी नहीं मांगेगा।" - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "जैज़आइकन्स" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON फाइल", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional को पिन करें" }, - "onboardingUsePhishingDetectionDescription": { - "message": "फिशिंग डिटेक्शन अलर्ट $1 के साथ संचार पर निर्भर करते हैं। jsDeliver की पहुंच आपके IP एड्रेस तक होगी। $2 देखें।", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "रिजेक्ट" }, - "remember": { - "message": "याद रखें:" - }, "remove": { "message": "हटाएं" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepolia टेस्ट नेटवर्क" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask उत्पाद की उपयोगिता और सुरक्षा को बढ़ाने के लिए इन विश्वसनीय तीसरे-पक्ष की सेवाओं का इस्तेमाल करता है।" - }, "setApprovalForAll": { "message": "सभी के लिए स्वीकृति सेट करें" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "हमारी hardware wallet कनेक्शन गाइड" }, - "walletCreationSuccessDetail": { - "message": "आपने अपने वॉलेट को सफलतापूर्वक सुरक्षित कर लिया है। अपने सीक्रेट रिकवरी फ्रेज को सुरक्षित और गुप्त रखें -- यह आपकी जिम्मेदारी है!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask आपके सीक्रेट रिकवरी फ्रेज को फिर से प्राप्त नहीं कर सकता है।" - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask कभी भी आपके सीक्रेट रिकवरी फ्रेज के बारे में नहीं पूछेगा।" - }, - "walletCreationSuccessReminder3": { - "message": "$1 किसी के साथ या आपके फंड के चोरी होने का खतरा", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "अपने सीक्रेट रिकवरी फ्रेज को कभी शेयर ना करें", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "वॉलेट का निर्माण सफल हुआ" - }, "wantToAddThisNetwork": { "message": "इस नेटवर्क को जोड़ना चाहते हैं?" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 77c7b8f5286a..39d64cc98618 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Saat transaksi Anda dimasukkan ke dalam blok, selisih antara biaya dasar maks dan biaya dasar aktual akan dikembalikan. Jumlah total dihitung sebagai biaya dasar maks (dalam GWEI) * batas gas." }, - "advancedConfiguration": { - "message": "Konfigurasi lanjutan" - }, "advancedDetailsDataDesc": { "message": "Data" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Ketentuan penggunaan Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta tidak dapat memulihkan Frasa Pemulihan Rahasia Anda." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta tidak akan pernah menanyakan Frasa Pemulihan Rahasia Anda." - }, "billionAbbreviation": { "message": "M", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "File JSON", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Sematkan MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Peringatan deteksi pengelabuan bergantung pada komunikasi dengan $1. jsDeliver akan mendapat akses ke alamat IP Anda. Lihat $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1H", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Ditolak" }, - "remember": { - "message": "Ingatlah:" - }, "remove": { "message": "Hapus" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Jaringan uji Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask menggunakan layanan pihak ketiga tepercaya ini untuk meningkatkan kegunaan dan keamanan produk." - }, "setApprovalForAll": { "message": "Atur persetujuan untuk semua" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "panduan koneksi dompet perangkat keras kami" }, - "walletCreationSuccessDetail": { - "message": "Anda telah berhasil melindungi dompet Anda. Jaga agar Frasa Pemulihan Rahasia tetap aman dan terlindungi. Ini merupakan tanggung jawab Anda!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask tidak dapat memulihkan Frasa Pemulihan Rahasia Anda." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask tidak akan pernah menanyakan Frasa Pemulihan Rahasia Anda." - }, - "walletCreationSuccessReminder3": { - "message": "$1 dengan siapa pun atau dana Anda berisiko dicuri", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Jangan pernah membagikan Frasa Pemulihan Rahasia Anda", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Dompet berhasil dibuat" - }, "wantToAddThisNetwork": { "message": "Ingin menambahkan jaringan ini?" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index c258d0947266..ca1e76018a81 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "トランザクションがブロックに含まれた場合、最大基本料金と実際の基本料金の差が返金されます。合計金額は、最大基本料金 (Gwei単位) * ガスリミットで計算されます。" }, - "advancedConfiguration": { - "message": "詳細設定" - }, "advancedDetailsDataDesc": { "message": "データ" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "ベータ版利用規約" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMaskベータ版はシークレットリカバリーフレーズを復元できません。" - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMaskベータ版がユーザーのシークレットリカバリーフレーズを求めることは絶対にありません。" - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicon" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSONファイル", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutionalをピン留めする" }, - "onboardingUsePhishingDetectionDescription": { - "message": "フィッシング検出アラートには$1との通信が必要です。jsDeliverはユーザーのIPアドレスにアクセスします。$2をご覧ください。", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1日", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "拒否されました" }, - "remember": { - "message": "ご注意:" - }, "remove": { "message": "削除" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepoliaテストネットワーク" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMaskはこれらの信頼できるサードパーティサービスを使用して、製品の使いやすさと安全性を向上させています。" - }, "setApprovalForAll": { "message": "すべてを承認に設定" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "弊社のハードウェアウォレット接続ガイド" }, - "walletCreationSuccessDetail": { - "message": "ウォレットが正常に保護されました。シークレットリカバリーフレーズを安全かつ機密に保管してください。これはユーザーの責任です!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMaskはシークレットリカバリーフレーズを復元できません。" - }, - "walletCreationSuccessReminder2": { - "message": "MetaMaskがユーザーのシークレットリカバリーフレーズを確認することは絶対にありません。" - }, - "walletCreationSuccessReminder3": { - "message": "誰に対しても$1。資金が盗まれる恐れがあります", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "シークレットリカバリーフレーズは決して教えないでください", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "ウォレットが作成されました" - }, "wantToAddThisNetwork": { "message": "このネットワークを追加しますか?" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6eaa179492f3..051a19589005 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "트랜잭션이 블록에 포함되면 최대 기본 요금과 실제 기본 요금 간의 차액이 환불됩니다. 총 금액은 최대 기본 요금(GWEI 단위) 곱하기 가스 한도로 계산합니다." }, - "advancedConfiguration": { - "message": "고급 옵션" - }, "advancedDetailsDataDesc": { "message": "데이터" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "베타 이용약관" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask 베타는 비밀복구구문을 복구할 수 없습니다." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask 베타는 비밀복구구문을 절대 묻지 않습니다." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON 파일", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional 고정" }, - "onboardingUsePhishingDetectionDescription": { - "message": "피싱 감지 경고는 $1과(와)의 통신에 의존합니다. jsDeliver는 회원님의 IP 주소에 액세스할 수 있습니다. $2 보기.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1일", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "거부됨" }, - "remember": { - "message": "참고:" - }, "remove": { "message": "제거" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepolia 테스트 네트워크" }, - "setAdvancedPrivacySettingsDetails": { - "message": "이와 같이 MetaMask는 신용있는 타사의 서비스를 사용하여 제품 가용성과 안전성을 향상합니다." - }, "setApprovalForAll": { "message": "모두 승인 설정" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "당사의 하드웨어 지갑 연결 가이드" }, - "walletCreationSuccessDetail": { - "message": "지갑을 성공적으로 보호했습니다. 비밀복구구문을 안전하게 비밀로 유지하세요. 이는 회원님의 책임입니다!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask는 비밀복구구문을 복구할 수 없습니다." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask는 비밀복구구문을 절대 묻지 않습니다." - }, - "walletCreationSuccessReminder3": { - "message": "누군가와 $1 또는 회원님의 자금을 도난당할 위험이 있습니다.", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "비밀복구구문을 절대 공유하지 마세요.", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "지갑 생성 성공" - }, "wantToAddThisNetwork": { "message": "이 네트워크를 추가하시겠습니까?" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index ca4f9a643a36..8d686eaee7c2 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Quando a sua transação for incluída no bloco, qualquer diferença entre a sua taxa-base máxima e a taxa-base real será reembolsada. O cálculo do valor total é feito da seguinte forma: taxa-base máxima (em GWEI) * limite de gás." }, - "advancedConfiguration": { - "message": "Configurações avançadas" - }, "advancedDetailsDataDesc": { "message": "Dados" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Termos de uso do Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "O MetaMask Beta não pode recuperar a sua Frase de Recuperação Secreta." - }, - "betaWalletCreationSuccessReminder2": { - "message": "O MetaMask Beta nunca pedirá sua Frase de Recuperação Secreta." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Arquivo JSON", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Fixar MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Recusada" }, - "remember": { - "message": "Lembre-se:" - }, "remove": { "message": "Remover" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Rede de teste Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "A MetaMask utiliza esses serviços terceirizados de confiança para aumentar a usabilidade e a segurança dos produtos." - }, "setApprovalForAll": { "message": "Definir aprovação para todos" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "nosso guia de conexão com a carteira de hardware" }, - "walletCreationSuccessDetail": { - "message": "Você protegeu sua carteira com sucesso. Guarde sua Frase de Recuperação Secreta em segredo e em segurança — é sua responsabilidade!" - }, - "walletCreationSuccessReminder1": { - "message": "A MetaMask não é capaz de recuperar sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder2": { - "message": "A equipe da MetaMask jamais pedirá sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder3": { - "message": "$1 com ninguém, senão seus fundos poderão ser roubados", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca compartilhe a sua Frase de Recuperação Secreta", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Carteira criada com sucesso" - }, "wantToAddThisNetwork": { "message": "Desejar adicionar esta rede?" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index b37bbb8af658..2becf1c495a1 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1033,9 +1033,6 @@ "invalidSeedPhrase": { "message": "Frase de Recuperação Secreta inválida" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Arquivo JSON", "description": "format for importing an account" @@ -1392,10 +1389,6 @@ "onboardingPinExtensionTitle": { "message": "Sua instalação da MetaMask está concluída!" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "onlyConnectTrust": { "message": "Conecte-se somente com sites em que você confia.", "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." @@ -1559,9 +1552,6 @@ "rejected": { "message": "Rejeitada" }, - "remember": { - "message": "Lembre-se:" - }, "remove": { "message": "Remover" }, @@ -1724,9 +1714,6 @@ "message": "Enviando $1", "description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)" }, - "setAdvancedPrivacySettingsDetails": { - "message": "A MetaMask utiliza esses serviços terceirizados de confiança para aumentar a usabilidade e a segurança dos produtos." - }, "settings": { "message": "Configurações" }, @@ -2446,26 +2433,6 @@ "walletConnectionGuide": { "message": "nosso guia de conexão com a carteira de hardware" }, - "walletCreationSuccessDetail": { - "message": "Você protegeu sua carteira com sucesso. Guarde sua Frase de Recuperação Secreta em segredo e em segurança — é sua responsabilidade!" - }, - "walletCreationSuccessReminder1": { - "message": "A MetaMask não é capaz de recuperar sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder2": { - "message": "A equipe da MetaMask jamais pedirá sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder3": { - "message": "$1 com ninguém, senão seus fundos poderão ser roubados", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca compartilhe a sua Frase de Recuperação Secreta", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Carteira criada com sucesso" - }, "web3ShimUsageNotification": { "message": "Percebemos que o site atual tentou usar a API window.web3 removida. Se o site parecer estar corrompido, clique em $1 para obter mais informações.", "description": "$1 is a clickable link." diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b06caab95019..b36ac87f7dbe 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "После включения вашей транзакции в блок возмещается любая разница между вашей максимальной базовой комиссией и фактической базовой комиссией. Общая сумма рассчитывается следующим образом: максимальная базовая комиссия (в Гвей) x лимит газа." }, - "advancedConfiguration": { - "message": "Дополнительная конфигурация" - }, "advancedDetailsDataDesc": { "message": "Данные" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Условия использования бета-версии" }, - "betaWalletCreationSuccessReminder1": { - "message": "Бета-версия MetaMask не сможет восстановить вашу секретную фразу для восстановления." - }, - "betaWalletCreationSuccessReminder2": { - "message": "Бета-версия MetaMask никогда не запрашивает у вас секретную фразу для восстановления." - }, "billionAbbreviation": { "message": "Б", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON-файл", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Закрепить MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Оповещения об обнаружении фишинга зависят от связи с $1. jsDeliver получит доступ к вашему IP-адресу. Посмотрите $ 2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 Д", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Отклонено" }, - "remember": { - "message": "Помните:" - }, "remove": { "message": "Удалить" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Тестовая сеть Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask использует эти доверенные сторонние сервисы для повышения удобства использования и безопасности продукта." - }, "setApprovalForAll": { "message": "Установить одобрение для всех" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "наше руководство по подключению аппаратного кошелька" }, - "walletCreationSuccessDetail": { - "message": "Вы успешно защитили свой кошелек. Сохраните секретную фразу для восстановления в тайне — вы отвечаете за ее сохранность!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask не сможет восстановить вашу секретную фразу для восстановления." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask никогда не запрашивает у вас секретную фразу для восстановления." - }, - "walletCreationSuccessReminder3": { - "message": "$1, чтобы предотвратить кражу ваших средств", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Никогда не сообщайте никому свою секретную фразу для восстановления", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Кошелек создан" - }, "wantToAddThisNetwork": { "message": "Хотите добавить эту сеть?" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 42ad155e2931..e076ac55176b 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Kapag nakasama ang iyong transaksyon sa block, ire-refund ang anumang difference sa pagitan ng iyong pinakamataas na batayang bayad at ang aktwal na batayang bayad. Ang kabuuang halaga ay kinakalkula bilang pinakamataas na batayang bayad (sa GWEI) * ng limitasyon ng gas." }, - "advancedConfiguration": { - "message": "Advanced na pagsasaayos" - }, "advancedDetailsDataDesc": { "message": "Data" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Mga tuntunin sa paggamit ng Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "Hindi mabawi ng MetaMask Beta ang iyong Lihim na Parirala sa Pagbawi." - }, - "betaWalletCreationSuccessReminder2": { - "message": "Hindi kailanman hihingiin sa iyo ng MetaMask Beta ang iyong Lihim na Parirala sa Pagbawi." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Mga Jazzicon" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "File na JSON", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "I-pin ang MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Ang mga alerto sa pagtuklas ng phishing ay umaasa sa komunikasyon sa $1. Ang jsDeliver ay magkakaroon ng access sa iyong IP address. Tingnan ang $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Tinanggihan" }, - "remember": { - "message": "Tandaan:" - }, "remove": { "message": "Alisin" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepolia test network" }, - "setAdvancedPrivacySettingsDetails": { - "message": "Ginagamit ng MetaMask ang mga pinagkakatiwalaang serbisyo ng third-party na ito para mapahusay ang kakayahang magamit at kaligtasan ng produkto." - }, "setApprovalForAll": { "message": "Itakda ang Pag-apruba para sa Lahat" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "ang aming gabay sa pagkonekta ng wallet na hardware" }, - "walletCreationSuccessDetail": { - "message": "Matagumpay mong naprotektahan ang iyong wallet. Panatilihing ligtas at sikreto ang iyong Lihim na Parirala sa Pagbawi - pananagutan mo ito!" - }, - "walletCreationSuccessReminder1": { - "message": "Di mababawi ng MetaMask ang iyong Lihim na Parirala sa Pagbawi." - }, - "walletCreationSuccessReminder2": { - "message": "Kailanman ay hindi hihingin ng MetaMask ang iyong Lihim na Parirala sa Pagbawi." - }, - "walletCreationSuccessReminder3": { - "message": "$1 sa sinuman o panganib na manakaw ang iyong pondo", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Huwag kailanman ibahagi ang iyong Lihim na Parirala sa Pagbawi", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Matagumpay ang paggawa ng wallet" - }, "wantToAddThisNetwork": { "message": "Gusto mo bang idagdag ang network na ito?" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index db5dab6482f3..4465fd7c0d78 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "İşleminiz bloka dahil edildiğinde maks. baz ücretiniz ile gerçek paz ücret arasındaki fark iade edilecektir. Toplam miktar, maks. baz ücret (GWEI'de) * gaz limiti olarak hesaplanacaktır." }, - "advancedConfiguration": { - "message": "Gelişmiş yapılandırma" - }, "advancedDetailsDataDesc": { "message": "Veri" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Beta Kullanım koşulları" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta Gizli Kurtarma İfadenizi kurtaramaz." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask hiçbir zaman Gizli Kurtarma İfadenizi istemez." - }, "billionAbbreviation": { "message": "MR", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON Dosyası", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional'ı sabitle" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Kimlik avı tespiti uyarıları $1 ile iletişime bağlıdır. jsDeliver IP adresinize erişim sağlayacaktır. Şunu görüntüleyin: $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1G", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Reddedildi" }, - "remember": { - "message": "Unutmayın:" - }, "remove": { "message": "Kaldır" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepolia test ağı" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask, ürünün kullanılabilirliğini ve güvenliğini iyileştirmek amacıyla bu güvenilir üçüncü taraf hizmetlerini kullanır." - }, "setApprovalForAll": { "message": "Tümüne onay ver" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "donanım cüzdanı bağlantı kılavuzumuz" }, - "walletCreationSuccessDetail": { - "message": "Cüzdanınızı başarılı bir şekilde korudunuz. Gizli Kurtarma İfadenizi güvenli ve gizli tutun -- bunun sorumluluğu size aittir!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask Gizli Kurtarma İfadenizi kurtaramıyor." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask hiçbir zaman Gizli Kurtarma İfadenizi istemeyecektir." - }, - "walletCreationSuccessReminder3": { - "message": "$1 hiç kimseyle başkasıyla paylaşmayın, aksi halde çalınma riskiyle karşı karşıya kalırsınız", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Gizli Kurtarma İfadenizi", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Cüzdan oluşturma başarılı" - }, "wantToAddThisNetwork": { "message": "Bu ağı eklemek istiyor musunuz?" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 3623c8b86a2e..2d8d89a25ee7 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Khi các giao dịch của bạn được đưa vào khối, mọi phần chênh lệch giữa phí cơ sở tối đa và phí cơ sở thực tế đều sẽ được hoàn lại. Tổng số tiền sẽ được tính bằng phí cơ sở tối đa (theo GWEI) * hạn mức phí gas." }, - "advancedConfiguration": { - "message": "Cấu hình nâng cao" - }, "advancedDetailsDataDesc": { "message": "Dữ liệu" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "Điều khoản sử dụng phiên bản Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta không thể khôi phục Cụm từ khôi phục bí mật của bạn." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta sẽ không bao giờ hỏi về Cụm từ khôi phục bí mật của bạn." - }, "billionAbbreviation": { "message": "Tỷ", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Tập tin JSON", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Ghim MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Thông báo phát hiện dấu hiệu lừa đảo tùy thuộc vào quá trình truyền tin với $1. jsDeliver sẽ có quyền truy cập vào địa chỉ IP của bạn. Xem $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 Ngày", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "Đã từ chối" }, - "remember": { - "message": "Ghi nhớ:" - }, "remove": { "message": "Xóa" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Mạng thử nghiệm Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask sử dụng các dịch vụ của bên thứ ba đáng tin cậy này để nâng cao sự hữu ích và an toàn của sản phẩm." - }, "setApprovalForAll": { "message": "Thiết lập chấp thuận tất cả" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "hướng dẫn của chúng tôi về cách kết nối ví cứng" }, - "walletCreationSuccessDetail": { - "message": "Bạn đã bảo vệ thành công ví của mình. Hãy đảm bảo an toàn và bí mật cho Cụm từ khôi phục bí mật của bạn -- đây là trách nhiệm của bạn!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask không thể khôi phục Cụm từ khôi phục bí mật của bạn." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask sẽ không bao giờ hỏi về Cụm từ khôi phục bí mật của bạn." - }, - "walletCreationSuccessReminder3": { - "message": "$1 với bất kỳ ai, nếu không bạn sẽ có nguy cơ bị mất tiền", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Không bao giờ chia sẻ Cụm từ khôi phục bí mật của bạn", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Tạo ví thành công" - }, "wantToAddThisNetwork": { "message": "Bạn muốn thêm mạng này?" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 3b78771d5823..37836c219ccd 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "当您的交易被包含在区块中时,您的最大基础费用与实际基础费用之间的任何差额将被退还。总金额按最大基础费用(以GWEI为单位)*燃料限制计算。" }, - "advancedConfiguration": { - "message": "高级配置" - }, "advancedDetailsDataDesc": { "message": "数据" }, @@ -700,12 +697,6 @@ "betaTerms": { "message": "测试版使用条款" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask 测试版无法恢复您的账户私钥助记词。" - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask 测试版绝对不会向您索要账户私钥助记词。" - }, "billionAbbreviation": { "message": "十亿", "description": "Shortened form of 'billion'" @@ -2400,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON 文件", "description": "format for importing an account" @@ -3452,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "将MetaMask Institutional置顶" }, - "onboardingUsePhishingDetectionDescription": { - "message": "网络钓鱼检测提醒依赖于与 $1 的通信。jsDeliver 将有权访问您的 IP 地址。查看 $2。", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 天", "description": "Shortened form of '1 day'" @@ -4096,9 +4080,6 @@ "rejected": { "message": "已拒绝" }, - "remember": { - "message": "记住:" - }, "remove": { "message": "删除" }, @@ -4506,9 +4487,6 @@ "sepolia": { "message": "Sepolia测试网络" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask 使用这些可信的第三方服务来提高产品可用性和安全性。" - }, "setApprovalForAll": { "message": "设置批准所有" }, @@ -6160,26 +6138,6 @@ "walletConnectionGuide": { "message": "我们的硬件钱包连接指南" }, - "walletCreationSuccessDetail": { - "message": "您已经成功地保护了您的钱包。请确保您的账户私钥助记词安全和秘密——这是您的责任!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask 无法恢复您的账户私钥助记词。" - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask 绝对不会索要您的账户私钥助记词。" - }, - "walletCreationSuccessReminder3": { - "message": "对任何人 $1,否则您的资金有被盗风险", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "切勿分享您的账户私钥助记词", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "钱包创建成功" - }, "wantToAddThisNetwork": { "message": "想要添加此网络吗?" }, diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 8faf7c7bfb79..b3e6f252d23d 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -586,6 +586,7 @@ export enum MetaMetricsEventName { OnboardingWalletImportAttempted = 'Wallet Import Attempted', OnboardingWalletVideoPlay = 'SRP Intro Video Played', OnboardingTwitterClick = 'External Link Clicked', + OnboardingWalletSetupComplete = 'Wallet Setup Complete', OnrampProviderSelected = 'On-ramp Provider Selected', PermissionsApproved = 'Permissions Approved', PermissionsRejected = 'Permissions Rejected', diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 643dcefa35ae..21cc84a6fcb9 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -546,7 +546,7 @@ const onboardingRevealAndConfirmSRP = async (driver) => { */ const onboardingCompleteWalletCreation = async (driver) => { // complete - await driver.findElement({ text: 'Wallet creation successful', tag: 'h2' }); + await driver.findElement({ text: 'Congratulations', tag: 'h2' }); await driver.clickElement('[data-testid="onboarding-complete-done"]'); }; @@ -554,7 +554,7 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { // wait for h2 to appear await driver.findElement({ text: 'Wallet creation successful', tag: 'h2' }); // opt-out from third party API - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ text: 'Manage default settings', tag: 'a' }); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index 7b03d411d6ec..c9fa95f986e9 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -397,7 +397,13 @@ describe('MultiRpc:', function (this: Suite) { // go to advanced settigns await driver.clickElement({ - text: 'Advanced configuration', + text: 'Manage default settings', + }); + + await driver.delay(regularDelayMs); + + await driver.clickElement({ + text: 'General', }); // open edit modal @@ -419,6 +425,27 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); + await driver.delay(regularDelayMs); + await driver.waitForSelector('[data-testid="category-back-button"]'); + await driver.clickElement('[data-testid="category-back-button"]'); + + await driver.waitForSelector( + '[data-testid="privacy-settings-back-button"]', + ); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + + await driver.clickElement({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + await driver.clickElement({ text: 'Done', tag: 'button', @@ -433,7 +460,7 @@ describe('MultiRpc:', function (this: Suite) { true, '“Arbitrum One” was successfully edited!', ); - + // Ensures popover backround doesn't kill test await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="network-display"]'); diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 1aa716953703..1b15dba5ddd7 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -21,6 +21,7 @@ const { regularDelayMs, unlockWallet, tinyDelayMs, + largeDelayMs, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { @@ -204,7 +205,7 @@ describe('MetaMask onboarding @no-mmi', function () { // Verify site assert.equal( await driver.isElementPresent({ - text: 'Wallet creation successful', + text: 'Your wallet is ready', tag: 'h2', }), true, @@ -270,76 +271,122 @@ describe('MetaMask onboarding @no-mmi', function () { }, async ({ driver, secondaryGanacheServer }) => { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); + try { + await driver.navigate(); + await importSRPOnboardingFlow( + driver, + TEST_SEED_PHRASE, + WALLET_PASSWORD, + ); - // Add custom network localhost 8546 during onboarding - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); - await driver.clickElement({ text: 'Add a network' }); - await driver.waitForSelector( - '.multichain-network-list-menu-content-wrapper__dialog', - ); + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); - await driver.fill( - '[data-testid="network-form-network-name"]', - networkName, - ); - await driver.fill( - '[data-testid="network-form-chain-id"]', - chainId.toString(), - ); - await driver.fill( - '[data-testid="network-form-ticker-input"]', - currencySymbol, - ); + await driver.clickElement({ + text: 'General', + }); + await driver.delay(largeDelayMs); + await driver.clickElement({ text: 'Add a network' }); - // Add rpc url - const rpcUrlInputDropDown = await driver.waitForSelector( - '[data-testid="test-add-rpc-drop-down"]', - ); - await rpcUrlInputDropDown.click(); - await driver.delay(tinyDelayMs); - await driver.clickElement({ - text: 'Add RPC URL', - tag: 'button', - }); - const rpcUrlInput = await driver.waitForSelector( - '[data-testid="rpc-url-input-test"]', - ); - await rpcUrlInput.clear(); - await rpcUrlInput.sendKeys(networkUrl); - await driver.clickElement({ - text: 'Add URL', - tag: 'button', - }); + await driver.waitForSelector( + '.multichain-network-list-menu-content-wrapper__dialog', + ); - await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.clickElement({ - text: 'Done', - tag: 'button', - }); + await driver.fill( + '[data-testid="network-form-network-name"]', + networkName, + ); + await driver.fill( + '[data-testid="network-form-chain-id"]', + chainId.toString(), + ); + await driver.fill( + '[data-testid="network-form-ticker-input"]', + currencySymbol, + ); - await driver.clickElement('.mm-picker-network'); - await driver.clickElement( - `[data-rbd-draggable-id="${toHex(chainId)}"]`, - ); + // Add rpc url + 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', + }); + const rpcUrlInput = await driver.waitForSelector( + '[data-testid="rpc-url-input-test"]', + ); + await rpcUrlInput.clear(); + await rpcUrlInput.sendKeys(networkUrl); + await driver.clickElement({ + text: 'Add URL', + 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(); - // Check localhost 8546 is selected and its balance value is correct - await driver.findElement({ - css: '[data-testid="network-display"]', - text: networkName, - }); + await driver.delay(largeDelayMs); + + await driver.waitForSelector( + '[data-testid="privacy-settings-back-button"]', + ); + const defaultSettingsBackButton = await driver.findElement( + '[data-testid="privacy-settings-back-button"]', + ); + await defaultSettingsBackButton.click(); + + await driver.delay(largeDelayMs); + + await driver.clickElement({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + await driver.delay(largeDelayMs); - await locateAccountBalanceDOM(driver, secondaryGanacheServer[0]); + await driver.clickElement({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement('.mm-picker-network'); + 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"]', + text: networkName, + }); + + await locateAccountBalanceDOM(driver, secondaryGanacheServer[0]); + } catch (error) { + console.error('Error in test:', error); + throw error; + } }, ); }); - it('User can turn off basic functionality in advanced configurations', async function () { + it('User can turn off basic functionality in default settings', async function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), @@ -354,13 +401,25 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + + await driver.clickElement('[data-testid="onboarding-complete-done"]'); + await driver.clickElement('[data-testid="pin-extension-next"]'); + await driver.clickElement('[data-testid="pin-extension-done"]'); + // Check that the 'basic functionality is off' banner is displayed on the home screen after onboarding completion await driver.waitForSelector({ text: 'Basic functionality is off', diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index aef2f16728de..062a0345a39a 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -5,6 +5,8 @@ const { importSRPOnboardingFlow, WALLET_PASSWORD, tinyDelayMs, + regularDelayMs, + largeDelayMs, defaultGanacheOptions, } = require('../../helpers'); const { METAMASK_STALELIST_URL } = require('../phishing-controller/helpers'); @@ -41,7 +43,7 @@ async function mockApis(mockServer) { } describe('MetaMask onboarding @no-mmi', function () { - it('should prevent network requests to basic functionality endpoints when the basica functionality toggle is off', async function () { + it('should prevent network requests to basic functionality endpoints when the basic functionality toggle is off', async function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), @@ -57,15 +59,36 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); + + await driver.delay(regularDelayMs); + await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); + 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', ); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.delay(regularDelayMs); + 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.clickElement('[data-testid="network-display"]'); @@ -90,7 +113,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); }); - it('should not prevent network requests to basic functionality endpoints when the basica functionality toggle is on', async function () { + it('should not prevent network requests to basic functionality endpoints when the basic functionality toggle is on', async function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), @@ -106,19 +129,29 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); - + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); + await driver.delay(largeDelayMs); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.delay(largeDelayMs); + 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' }); 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'); await driver.clickElement('[data-testid="refresh-list-button"]'); - for (let i = 0; i < mockedEndpoints.length; i += 1) { const requests = await mockedEndpoints[i].getSeenRequests(); assert.equal( diff --git a/test/integration/onboarding/wallet-created.test.tsx b/test/integration/onboarding/wallet-created.test.tsx index 36ff7c8d3ecf..55be476839fe 100644 --- a/test/integration/onboarding/wallet-created.test.tsx +++ b/test/integration/onboarding/wallet-created.test.tsx @@ -10,6 +10,7 @@ import { jest.mock('../../../ui/store/background-connection', () => ({ ...jest.requireActual('../../../ui/store/background-connection'), submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), })); jest.mock('../../../ui/ducks/bridge/actions', () => ({ @@ -21,6 +22,7 @@ const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), + callBackgroundMethod: jest.fn(), }; describe('Wallet Created Events', () => { @@ -34,7 +36,7 @@ describe('Wallet Created Events', () => { backgroundConnection: backgroundConnectionMocked, }); - expect(getByText('Wallet creation successful')).toBeInTheDocument(); + expect(getByText('Congratulations!')).toBeInTheDocument(); fireEvent.click(getByTestId('onboarding-complete-done')); @@ -69,6 +71,18 @@ describe('Wallet Created Events', () => { fireEvent.click(getByTestId('pin-extension-next')); + let onboardingPinExtensionMetricsEvent; + + await waitFor(() => { + onboardingPinExtensionMetricsEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'trackMetaMetricsEvent', + ); + expect(onboardingPinExtensionMetricsEvent?.[0]).toBe( + 'trackMetaMetricsEvent', + ); + }); + await waitFor(() => { expect( getByText( diff --git a/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap b/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap index 27ee8bbf6b69..8c44078d57cd 100644 --- a/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap +++ b/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap @@ -6,7 +6,7 @@ exports[`IncomingTransactionToggle should render existing incoming transaction p class="mm-box mm-incoming-transaction-toggle" > <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-default" > Show incoming transactions </p> diff --git a/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx b/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx index d1274075d056..0295fc1a044d 100644 --- a/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx +++ b/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx @@ -60,7 +60,9 @@ const IncomingTransactionToggle = ({ return ( <Box ref={wrapperRef} className="mm-incoming-transaction-toggle"> - <Text variant={TextVariant.bodyMd}>{t('showIncomingTransactions')}</Text> + <Text variant={TextVariant.bodyMdMedium}> + {t('showIncomingTransactions')} + </Text> <Text variant={TextVariant.bodySm} color={TextColor.textAlternative}> {t('showIncomingTransactionsExplainer')} </Text> diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.js index fab463e5b685..d91e3e54746d 100644 --- a/ui/pages/onboarding-flow/creation-successful/creation-successful.js +++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.js @@ -1,23 +1,34 @@ import React, { useContext } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; - -import Box from '../../../components/ui/box'; -import { Text } from '../../../components/component-library'; -import Button from '../../../components/ui/button'; import { - FontWeight, - TextAlign, - AlignItems, + Button, + ButtonSize, + ButtonVariant, +} from '../../../components/component-library/button'; +import { TextVariant, + Display, + AlignItems, + JustifyContent, + FlexDirection, } from '../../../helpers/constants/design-system'; +import { + Box, + Text, + IconName, + ButtonLink, + ButtonLinkSize, + IconSize, +} from '../../../components/component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { ONBOARDING_PIN_EXTENSION_ROUTE, ONBOARDING_PRIVACY_SETTINGS_ROUTE, } from '../../../helpers/constants/routes'; -import { isBeta } from '../../../helpers/utils/build-types'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import { getFirstTimeFlowType } from '../../../selectors'; +import { getSeedPhraseBackedUp } from '../../../ducks/metamask/metamask'; import { MetaMetricsEventCategory, MetaMetricsEventName, @@ -31,84 +42,160 @@ export default function CreationSuccessful() { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const firstTimeFlowType = useSelector(getFirstTimeFlowType); + const seedPhraseBackedUp = useSelector(getSeedPhraseBackedUp); + const learnMoreLink = + 'https://support.metamask.io/hc/en-us/articles/360015489591-Basic-Safety-and-Security-Tips-for-MetaMask'; + const learnHowToKeepWordsSafe = + 'https://community.metamask.io/t/what-is-a-secret-recovery-phrase-and-how-to-keep-your-crypto-wallet-secure/3440'; const { createSession } = useCreateSession(); const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); return ( - <div className="creation-successful" data-testid="creation-successful"> - <Box textAlign={TextAlign.Center}> - <img src="./images/tada.png" /> + <Box + className="creation-successful" + data-testid="creation-successful" + display={Display.Flex} + flexDirection={FlexDirection.Column} + > + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + justifyContent={JustifyContent.center} + marginTop={6} + > + <Text + justifyContent={JustifyContent.center} + marginBottom={4} + style={{ + alignSelf: AlignItems.center, + fontSize: '70px', + }} + > + <span> + {firstTimeFlowType === FirstTimeFlowType.create && + !seedPhraseBackedUp + ? '🔓' + : '🎉'} + </span> + </Text> <Text variant={TextVariant.headingLg} - fontWeight={FontWeight.Bold} + as="h2" margin={6} + justifyContent={JustifyContent.center} + style={{ + alignSelf: AlignItems.center, + }} > - {t('walletCreationSuccessTitle')} + {firstTimeFlowType === FirstTimeFlowType.import && + t('yourWalletIsReady')} + + {firstTimeFlowType === FirstTimeFlowType.create && + !seedPhraseBackedUp && + t('reminderSet')} + + {firstTimeFlowType === FirstTimeFlowType.create && + seedPhraseBackedUp && + t('congratulations')} </Text> - <Text variant={TextVariant.headingSm} fontWeight={FontWeight.Normal}> - {t('walletCreationSuccessDetail')} + <Text variant={TextVariant.bodyLgMedium} marginBottom={6}> + {firstTimeFlowType === FirstTimeFlowType.import && + t('rememberSRPIfYouLooseAccess', [ + <ButtonLink + key="rememberSRPIfYouLooseAccess" + size={ButtonLinkSize.Inherit} + textProps={{ + variant: TextVariant.bodyMd, + alignItems: AlignItems.flexStart, + }} + as="a" + href={learnHowToKeepWordsSafe} + target="_blank" + rel="noopener noreferrer" + > + {t('learnHow')} + </ButtonLink>, + ])} + + {firstTimeFlowType === FirstTimeFlowType.create && + seedPhraseBackedUp && + t('walletProtectedAndReadyToUse', [ + <b key="walletProtectedAndReadyToUse"> + {t('securityPrivacyPath')} + </b>, + ])} + {firstTimeFlowType === FirstTimeFlowType.create && + !seedPhraseBackedUp && + t('ifYouGetLockedOut', [ + <b key="ifYouGetLockedOut">{t('securityPrivacyPath')}</b>, + ])} </Text> </Box> - <Text - variant={TextVariant.headingSm} + + {firstTimeFlowType === FirstTimeFlowType.create && ( + <Text variant={TextVariant.bodyLgMedium} marginBottom={6}> + {t('keepReminderOfSRP', [ + <ButtonLink + key="keepReminderOfSRP" + size={ButtonLinkSize.Inherit} + textProps={{ + variant: TextVariant.bodyMd, + alignItems: AlignItems.flexStart, + }} + as="a" + href={learnMoreLink} + target="_blank" + rel="noopener noreferrer" + > + {t('learnMoreUpperCaseWithDot')} + </ButtonLink>, + ])} + </Text> + )} + + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} alignItems={AlignItems.flexStart} - fontWeight={FontWeight.Normal} - marginLeft={12} > - {t('remember')} - </Text> - <ul> - <li> - <Text variant={TextVariant.headingSm} fontWeight={FontWeight.Normal}> - {isBeta() - ? t('betaWalletCreationSuccessReminder1') - : t('walletCreationSuccessReminder1')} - </Text> - </li> - <li> - <Text variant={TextVariant.headingSm} fontWeight={FontWeight.Normal}> - {isBeta() - ? t('betaWalletCreationSuccessReminder2') - : t('walletCreationSuccessReminder2')} - </Text> - </li> - <li> - <Text variant={TextVariant.headingSm} fontWeight={FontWeight.Normal}> - {t('walletCreationSuccessReminder3', [ - <span - key="creation-successful__bold" - className="creation-successful__bold" - > - {t('walletCreationSuccessReminder3BoldSection')} - </span>, - ])} - </Text> - </li> - <li> - <Button - href="https://community.metamask.io/t/what-is-a-secret-recovery-phrase-and-how-to-keep-your-crypto-wallet-secure/3440" - target="_blank" - type="link" - rel="noopener noreferrer" - > - {t('learnMoreUpperCase')} - </Button> - </li> - </ul> - <Box marginTop={6} className="creation-successful__actions"> <Button - type="link" + variant={ButtonVariant.Link} + startIconName={IconName.Setting} + startIconProps={{ + size: IconSize.Md, + }} + style={{ + fontSize: 'var(--font-size-5)', + }} onClick={() => history.push(ONBOARDING_PRIVACY_SETTINGS_ROUTE)} + marginTop={4} + marginBottom={4} > - {t('advancedConfiguration')} + {t('manageDefaultSettings')} </Button> + <Text variant={TextVariant.bodySm}> + {t('settingsOptimisedForEaseOfUseAndSecurity')} + </Text> + </Box> + + <Box + marginTop={6} + className="creation-successful__actions" + display={Display.Flex} + flexDirection={FlexDirection.Column} + justifyContent={JustifyContent.center} + alignItems={AlignItems.center} + > <Button data-testid="onboarding-complete-done" - type="primary" - large - rounded + variant={ButtonVariant.Primary} + size={ButtonSize.Lg} + style={{ + width: '184px', + }} + marginTop={6} onClick={() => { trackEvent({ category: MetaMetricsEventCategory.Onboarding, @@ -122,9 +209,9 @@ export default function CreationSuccessful() { history.push(ONBOARDING_PIN_EXTENSION_ROUTE); }} > - {t('gotIt')} + {t('done')} </Button> </Box> - </div> + </Box> ); } diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js index 7d6c55f84642..5349a9f23f9e 100644 --- a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js +++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js @@ -8,6 +8,8 @@ import { } from '../../../helpers/constants/routes'; import { setBackgroundConnection } from '../../../store/background-connection'; import { renderWithProvider } from '../../../../test/jest'; +import initializedMockState from '../../../../test/data/mock-state.json'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import CreationSuccessful from './creation-successful'; const mockHistoryPush = jest.fn(); @@ -25,7 +27,12 @@ jest.mock('react-router-dom', () => ({ describe('Creation Successful Onboarding View', () => { const mockStore = { - metamask: {}, + metamask: { + providerConfig: { + type: 'test', + }, + firstTimeFlowType: FirstTimeFlowType.import, + }, }; const store = configureMockStore([thunk])(mockStore); setBackgroundConnection({ completeOnboarding: completeOnboardingStub }); @@ -34,19 +41,94 @@ describe('Creation Successful Onboarding View', () => { jest.resetAllMocks(); }); - it('should redirect to privacy-settings view when "Advanced configuration" button is clicked', () => { + it('should remind the user to not loose the SRP and keep it safe (Import case)', () => { + const importFirstTimeFlowState = { + ...initializedMockState, + metamask: { + ...initializedMockState.metamask, + firstTimeFlowType: FirstTimeFlowType.import, + }, + }; + const customMockStore = configureMockStore([thunk])( + importFirstTimeFlowState, + ); + + const { getByText } = renderWithProvider( + <CreationSuccessful />, + customMockStore, + ); + + expect(getByText('Your wallet is ready')).toBeInTheDocument(); + expect( + getByText( + /Remember, if you lose your Secret Recovery Phrase, you lose access to your wallet/u, + ), + ).toBeInTheDocument(); + }); + + it('should show the Congratulations! message to the user (New wallet & backed up SRP)', () => { + const importFirstTimeFlowState = { + ...initializedMockState, + metamask: { + ...initializedMockState.metamask, + firstTimeFlowType: FirstTimeFlowType.create, + seedPhraseBackedUp: true, + }, + }; + const customMockStore = configureMockStore([thunk])( + importFirstTimeFlowState, + ); + + const { getByText } = renderWithProvider( + <CreationSuccessful />, + customMockStore, + ); + + expect(getByText('Congratulations!')).toBeInTheDocument(); + expect( + getByText(/Your wallet is protected and ready to use/u), + ).toBeInTheDocument(); + }); + + it('should show the Reminder set! message to the user (New wallet & did not backed up SRP)', () => { + const importFirstTimeFlowState = { + ...initializedMockState, + metamask: { + ...initializedMockState.metamask, + firstTimeFlowType: FirstTimeFlowType.create, + seedPhraseBackedUp: false, + }, + }; + const customMockStore = configureMockStore([thunk])( + importFirstTimeFlowState, + ); + + const { getByText } = renderWithProvider( + <CreationSuccessful />, + customMockStore, + ); + + expect(getByText('Reminder set!')).toBeInTheDocument(); + expect( + getByText( + /If you get locked out of the app or get a new device, you will lose your funds./u, + ), + ).toBeInTheDocument(); + }); + + it('should redirect to privacy-settings view when "Manage default settings" button is clicked', () => { const { getByText } = renderWithProvider(<CreationSuccessful />, store); - const privacySettingsButton = getByText('Advanced configuration'); + const privacySettingsButton = getByText('Manage default settings'); fireEvent.click(privacySettingsButton); expect(mockHistoryPush).toHaveBeenCalledWith( ONBOARDING_PRIVACY_SETTINGS_ROUTE, ); }); - it('should route to pin extension route when "Got it" button is clicked', async () => { + it('should route to pin extension route when "Done" button is clicked', async () => { const { getByText } = renderWithProvider(<CreationSuccessful />, store); - const gotItButton = getByText('Got it'); - fireEvent.click(gotItButton); + const doneButton = getByText('Done'); + fireEvent.click(doneButton); await waitFor(() => { expect(mockHistoryPush).toHaveBeenCalledWith( ONBOARDING_PIN_EXTENSION_ROUTE, diff --git a/ui/pages/onboarding-flow/creation-successful/index.scss b/ui/pages/onboarding-flow/creation-successful/index.scss index ca05b3b1323e..bbb558627caf 100644 --- a/ui/pages/onboarding-flow/creation-successful/index.scss +++ b/ui/pages/onboarding-flow/creation-successful/index.scss @@ -1,46 +1,9 @@ @use "design-system"; .creation-successful { - @include design-system.screen-sm-min { - display: flex; - flex-direction: column; - align-items: center; - } - img { align-self: center; } max-width: 600px; - - ul { - list-style-type: disc; - max-width: 500px; - } - - li { - margin-left: 25px; - - a { - justify-content: flex-start; - padding: 0; - } - } - - &__bold { - font-weight: bold; - } - - &__actions { - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; - - button { - margin-top: 14px; - max-width: 280px; - padding: 16px 0; - } - } } diff --git a/ui/pages/onboarding-flow/pin-extension/pin-extension.js b/ui/pages/onboarding-flow/pin-extension/pin-extension.js index 216bb1416cdf..c9ad1806d49d 100644 --- a/ui/pages/onboarding-flow/pin-extension/pin-extension.js +++ b/ui/pages/onboarding-flow/pin-extension/pin-extension.js @@ -1,13 +1,18 @@ import React, { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) useState, + useContext, ///: END:ONLY_INCLUDE_IF } from 'react'; import { useHistory } from 'react-router-dom'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Carousel } from 'react-responsive-carousel'; -import { setCompletedOnboarding } from '../../../store/actions'; +import { + setCompletedOnboarding, + performSignIn, + toggleExternalServices, +} from '../../../store/actions'; ///: END:ONLY_INCLUDE_IF import { useI18nContext } from '../../../hooks/useI18nContext'; import Button from '../../../components/ui/button'; @@ -30,6 +35,18 @@ import OnboardingPinMmiBillboard from '../../institutional/pin-mmi-billboard/pin ///: END:ONLY_INCLUDE_IF import { Text } from '../../../components/component-library'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + getFirstTimeFlowType, + getExternalServicesOnboardingToggleState, +} from '../../../selectors'; +import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; +import { selectParticipateInMetaMetrics } from '../../../selectors/metamask-notifications/authentication'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import OnboardingPinBillboard from './pin-billboard'; ///: END:ONLY_INCLUDE_IF @@ -39,14 +56,37 @@ export default function OnboardingPinExtension() { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const [selectedIndex, setSelectedIndex] = useState(0); const dispatch = useDispatch(); - ///: END:ONLY_INCLUDE_IF + const trackEvent = useContext(MetaMetricsContext); + const firstTimeFlowType = useSelector(getFirstTimeFlowType); + + const externalServicesOnboardingToggleState = useSelector( + getExternalServicesOnboardingToggleState, + ); + const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); + const participateInMetaMetrics = useSelector(selectParticipateInMetaMetrics); - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const handleClick = async () => { if (selectedIndex === 0) { setSelectedIndex(1); } else { + dispatch(toggleExternalServices(externalServicesOnboardingToggleState)); await dispatch(setCompletedOnboarding()); + + if (externalServicesOnboardingToggleState) { + if (!isProfileSyncingEnabled || participateInMetaMetrics) { + await dispatch(performSignIn()); + } + } + + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.OnboardingWalletSetupComplete, + properties: { + wallet_setup_type: + firstTimeFlowType === FirstTimeFlowType.import ? 'import' : 'new', + new_wallet: firstTimeFlowType === FirstTimeFlowType.create, + }, + }); history.push(DEFAULT_ROUTE); } }; diff --git a/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js b/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js index 8dc0529c86ae..00c7c38cf1d0 100644 --- a/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js +++ b/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js @@ -11,6 +11,8 @@ const completeOnboardingStub = jest .fn() .mockImplementation(() => Promise.resolve()); +const toggleExternalServicesStub = jest.fn(); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: jest.fn(() => []), @@ -18,10 +20,20 @@ jest.mock('react-router-dom', () => ({ describe('Creation Successful Onboarding View', () => { const mockStore = { - metamask: {}, + metamask: { + providerConfig: { + type: 'test', + }, + }, + appState: { + externalServicesOnboardingToggleState: true, + }, }; const store = configureMockStore([thunk])(mockStore); - setBackgroundConnection({ completeOnboarding: completeOnboardingStub }); + setBackgroundConnection({ + completeOnboarding: completeOnboardingStub, + toggleExternalServices: toggleExternalServicesStub, + }); const pushMock = jest.fn(); beforeAll(() => { diff --git a/ui/pages/onboarding-flow/privacy-settings/index.scss b/ui/pages/onboarding-flow/privacy-settings/index.scss index 53ce477fe7af..6e3f793cc5a2 100644 --- a/ui/pages/onboarding-flow/privacy-settings/index.scss +++ b/ui/pages/onboarding-flow/privacy-settings/index.scss @@ -5,22 +5,16 @@ flex-direction: column; justify-content: center; align-items: center; + overflow-x: hidden; @include design-system.screen-sm-max { margin-bottom: 24px; } - @include design-system.screen-sm-min { - margin-bottom: 40px; - } - &__header { - display: flex; - justify-content: center; - flex-direction: column; - text-align: center; - max-width: 500px; - margin: 24px; + a { + color: var(--color-primary-default); + } } &__settings { @@ -29,11 +23,6 @@ max-width: 620px; margin-bottom: 20px; - @include design-system.screen-sm-min { - margin-inline-start: 48px; - margin-inline-end: 48px; - } - a { color: var(--color-primary-default); @@ -65,6 +54,36 @@ } } + .container { + display: flex; + width: 100%; + transition: transform 0.5s ease; + } + + .hidden { + display: none; + } + + .categories-item { + cursor: pointer; + } + + .list-view, + .detail-view { + flex: 0 0 100%; + width: 100%; + } + + /* slide in show the detail view */ + .container.show-detail { + transform: translateX(-100%); + } + + /* slide back to show the list view */ + .container.show-list { + transform: translateX(0%); + } + &__customizable-network:hover { cursor: pointer; } diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index 53cfe99efeb0..ca3bd0af2ff4 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -5,6 +5,7 @@ import { ButtonVariant } from '@metamask/snaps-sdk'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { addUrlProtocolPrefix } from '../../../../app/scripts/lib/util'; + import { useSetIsProfileSyncingEnabled, useEnableProfileSyncing, @@ -19,12 +20,12 @@ import { PRIVACY_POLICY_LINK, TRANSACTION_SIMULATIONS_LEARN_MORE_LINK, } from '../../../../shared/lib/ui-utils'; +import Button from '../../../components/ui/button'; + import { Box, Text, TextField, - ButtonPrimary, - ButtonPrimarySize, IconName, ButtonLink, AvatarNetwork, @@ -34,15 +35,17 @@ import { } from '../../../components/component-library'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - AlignItems, Display, - FlexDirection, - JustifyContent, TextAlign, TextColor, TextVariant, + IconColor, + AlignItems, + JustifyContent, + FlexDirection, + BlockSize, } from '../../../helpers/constants/design-system'; -import { ONBOARDING_PIN_EXTENSION_ROUTE } from '../../../helpers/constants/routes'; +import { ONBOARDING_COMPLETION_ROUTE } from '../../../helpers/constants/routes'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getPetnamesEnabled, @@ -50,9 +53,7 @@ import { getNetworkConfigurationsByChainId, } from '../../../selectors'; import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; -import { selectParticipateInMetaMetrics } from '../../../selectors/metamask-notifications/authentication'; import { - setCompletedOnboarding, setIpfsGateway, setUseCurrencyRateCheck, setUseMultiAccountBalanceChecker, @@ -63,10 +64,8 @@ import { showModal, toggleNetworkMenu, setIncomingTransactionsPreferences, - toggleExternalServices, setUseTransactionSimulations, setPetnamesEnabled, - performSignIn, setEditedNetwork, } from '../../../store/actions'; import { @@ -116,6 +115,10 @@ export default function PrivacySettings() { const dispatch = useDispatch(); const history = useHistory(); + const [showDetail, setShowDetail] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [hiddenClass, setHiddenClass] = useState(true); + const defaultState = useSelector((state) => state.metamask); const { incomingTransactionsPreferences, @@ -128,7 +131,6 @@ export default function PrivacySettings() { useTransactionSimulations, } = defaultState; const petnamesEnabled = useSelector(getPetnamesEnabled); - const participateInMetaMetrics = useSelector(selectParticipateInMetaMetrics); const [usePhishingDetection, setUsePhishingDetection] = useState(null); const [turnOn4ByteResolution, setTurnOn4ByteResolution] = @@ -168,7 +170,6 @@ export default function PrivacySettings() { ); const handleSubmit = () => { - dispatch(toggleExternalServices(externalServicesOnboardingToggleState)); dispatch(setUsePhishDetect(phishingToggleState)); dispatch(setUse4ByteResolution(turnOn4ByteResolution)); dispatch(setUseTokenDetection(turnOnTokenDetection)); @@ -176,20 +177,12 @@ export default function PrivacySettings() { setUseMultiAccountBalanceChecker(isMultiAccountBalanceCheckerEnabled), ); dispatch(setUseCurrencyRateCheck(turnOnCurrencyRateCheck)); - dispatch(setCompletedOnboarding()); dispatch(setUseAddressBarEnsResolution(addressBarResolution)); setUseTransactionSimulations(isTransactionSimulationsEnabled); dispatch(setPetnamesEnabled(turnOnPetnames)); // Profile Syncing Setup - if (externalServicesOnboardingToggleState) { - if ( - profileSyncingProps.isProfileSyncingEnabled || - participateInMetaMetrics - ) { - dispatch(performSignIn()); - } - } else { + if (!externalServicesOnboardingToggleState) { profileSyncingProps.setIsProfileSyncingEnabled(false); } @@ -211,7 +204,7 @@ export default function PrivacySettings() { }, }); - history.push(ONBOARDING_PIN_EXTENSION_ROUTE); + history.push(ONBOARDING_COMPLETION_ROUTE); }; const handleUseProfileSync = async () => { @@ -242,352 +235,513 @@ export default function PrivacySettings() { } }; + const handleItemSelected = (item) => { + setSelectedItem(item); + setShowDetail(true); + + setTimeout(() => { + setHiddenClass(false); + }, 500); + }; + + const handleBack = () => { + setShowDetail(false); + setTimeout(() => { + setHiddenClass(true); + }, 500); + }; + + const items = [ + { id: 1, title: t('general'), subtitle: t('generalDescription') }, + { id: 2, title: t('assets'), subtitle: t('assetsDescription') }, + { id: 3, title: t('security'), subtitle: t('securityDescription') }, + ]; + return ( <> <div className="privacy-settings" data-testid="privacy-settings"> - <div className="privacy-settings__header"> - <Text variant={TextVariant.headingLg} as="h2"> - {t('advancedConfiguration')} - </Text> - <Text variant={TextVariant.headingSm} as="h4"> - {t('setAdvancedPrivacySettingsDetails')} - </Text> - </div> <div - className="privacy-settings__settings" - data-testid="privacy-settings-settings" + className={`container ${showDetail ? 'show-detail' : 'show-list'}`} > - <Setting - dataTestId="basic-functionality-toggle" - value={externalServicesOnboardingToggleState} - setValue={(toggledValue) => { - if (toggledValue === false) { - dispatch(openBasicFunctionalityModal()); - } else { - dispatch(onboardingToggleBasicFunctionalityOn()); - trackEvent({ - category: MetaMetricsEventCategory.Onboarding, - event: MetaMetricsEventName.SettingsUpdated, - properties: { - settings_group: 'onboarding_advanced_configuration', - settings_type: 'basic_functionality', - old_value: false, - new_value: true, - was_profile_syncing_on: false, - }, - }); - } - }} - title={t('basicConfigurationLabel')} - description={t('basicConfigurationDescription', [ - <a - href="https://consensys.io/privacy-policy" - key="link" - target="_blank" - rel="noreferrer noopener" - > - {t('privacyMsg')} - </a>, - ])} - /> - - <IncomingTransactionToggle - networkConfigurations={networkConfigurations} - setIncomingTransactionsPreferences={(chainId, value) => - dispatch(setIncomingTransactionsPreferences(chainId, value)) - } - incomingTransactionsPreferences={incomingTransactionsPreferences} - /> - - <Setting - dataTestId="profile-sync-toggle" - disabled={!externalServicesOnboardingToggleState} - value={profileSyncingProps.isProfileSyncingEnabled} - setValue={handleUseProfileSync} - title={t('profileSync')} - description={t('profileSyncDescription', [ - <a - href="https://support.metamask.io/privacy-and-security/profile-privacy" - key="link" - target="_blank" - rel="noopener noreferrer" + <div className="list-view"> + <Box + className="privacy-settings__header" + marginTop={6} + marginBottom={6} + display={Display.Flex} + flexDirection={FlexDirection.Column} + justifyContent={JustifyContent.flexStart} + > + <Box + display={Display.Flex} + alignItems={AlignItems.center} + flexDirection={FlexDirection.Row} + justifyContent={JustifyContent.flexStart} > - {t('profileSyncPrivacyLink')} - </a>, - ])} - /> - {profileSyncingProps.profileSyncingError && ( - <Box paddingBottom={4}> - <Text - as="p" - color={TextColor.errorDefault} - variant={TextVariant.bodySm} - > - {t('notificationsSettingsBoxError')} + <Button + type="inline" + icon={ + <Icon + name={IconName.ArrowLeft} + size={IconSize.Lg} + color={IconColor.iconDefault} + /> + } + data-testid="privacy-settings-back-button" + onClick={handleSubmit} + /> + <Box + display={Display.Flex} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + width={BlockSize.Full} + > + <Text variant={TextVariant.headingLg} as="h2"> + {t('defaultSettingsTitle')} + </Text> + </Box> + </Box> + <Text variant={TextVariant.bodyLgMedium} marginTop={5}> + {t('defaultSettingsSubTitle')} </Text> - </Box> - )} - - <Setting - value={phishingToggleState} - setValue={setUsePhishingDetection} - title={t('usePhishingDetection')} - description={t('onboardingUsePhishingDetectionDescription', [ <a - href="https://www.jsdelivr.com" + href="https://support.metamask.io/privacy-and-security/privacy-best-practices" target="_blank" rel="noreferrer" - key="jsDeliver" + key="learnMoreAboutPrivacy" + style={{ + fontSize: 'var(--font-size-5)', + }} > - {t('jsDeliver')} - </a>, - <a - href="https://www.jsdelivr.com/terms/privacy-policy-jsdelivr-com" - target="_blank" - rel="noreferrer" - key="privacyMsg" + {t('learnMoreAboutPrivacy')} + </a> + </Box> + <Box> + <Box + as="ul" + marginTop={4} + marginBottom={4} + style={{ listStyleType: 'none' }} + className="privacy-settings__categories-list" > - {t('privacyMsg')} - </a>, - ])} - /> - <Setting - value={turnOn4ByteResolution} - setValue={setTurnOn4ByteResolution} - title={t('use4ByteResolution')} - description={t('use4ByteResolutionDescription')} - /> - <Setting - value={turnOnTokenDetection} - setValue={setTurnOnTokenDetection} - title={t('turnOnTokenDetection')} - description={t('useTokenDetectionPrivacyDesc')} - /> - <Setting - value={isMultiAccountBalanceCheckerEnabled} - setValue={setMultiAccountBalanceCheckerEnabled} - title={t('useMultiAccountBalanceChecker')} - description={t('useMultiAccountBalanceCheckerSettingDescription')} - /> - <Setting - title={t('onboardingAdvancedPrivacyNetworkTitle')} - showToggle={false} - description={ - <> - {t('onboardingAdvancedPrivacyNetworkDescription', [ - <a - href="https://consensys.io/privacy-policy/" - key="link" - target="_blank" - rel="noopener noreferrer" - > - {t('privacyMsg')} - </a>, - ])} - - <Box paddingTop={4}> + {items.map((item) => ( <Box - display={Display.Flex} - flexDirection={FlexDirection.Column} - gap={5} + marginTop={5} + marginBottom={5} + key={item.id} + className="categories-item" + onClick={() => handleItemSelected(item)} > - {Object.values(networkConfigurations) - .filter(({ chainId }) => !TEST_CHAINS.includes(chainId)) - .map((network) => ( - <Box - key={network.chainId} - className="privacy-settings__customizable-network" - onClick={() => { - dispatch( - setEditedNetwork({ chainId: network.chainId }), - ); - dispatch(toggleNetworkMenu()); - }} - display={Display.Flex} - alignItems={AlignItems.center} - justifyContent={JustifyContent.spaceBetween} - > + <Box + display={Display.Flex} + alignItems={AlignItems.flexStart} + justifyContent={JustifyContent.spaceBetween} + data-testid={`category-item-${item.title}`} + > + <Text variant={TextVariant.bodyLgMedium}> + {item.title} + </Text> + <Button + type="inline" + icon={ + <Icon + name={IconName.ArrowRight} + color={IconColor.iconDefault} + /> + } + onClick={() => handleItemSelected(item)} + /> + </Box> + <Text + className="description" + variant={TextVariant.bodyMd} + color={TextColor.textAlternative} + > + {item.subtitle} + </Text> + </Box> + ))} + </Box> + </Box> + </div> + + <div + className={`detail-view ${ + !showDetail && hiddenClass ? 'hidden' : '' + }`} + > + <Box + className="privacy-settings__header" + marginTop={6} + marginBottom={5} + display={Display.Flex} + flexDirection={FlexDirection.Row} + justifyContent={JustifyContent.flexStart} + > + <Button + data-testid="category-back-button" + type="inline" + icon={ + <Icon + name={IconName.ArrowLeft} + size={IconSize.Lg} + color={IconColor.iconDefault} + /> + } + onClick={handleBack} + /> + <Box + display={Display.Flex} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + width={BlockSize.Full} + > + <Text variant={TextVariant.headingLg} as="h2"> + {selectedItem && selectedItem.title} + </Text> + </Box> + </Box> + + <div + className="privacy-settings__settings" + data-testid="privacy-settings-settings" + > + {selectedItem && selectedItem.id === 1 ? ( + <> + <Setting + dataTestId="basic-functionality-toggle" + value={externalServicesOnboardingToggleState} + setValue={(toggledValue) => { + if (toggledValue) { + dispatch(onboardingToggleBasicFunctionalityOn()); + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.SettingsUpdated, + properties: { + settings_group: 'onboarding_advanced_configuration', + settings_type: 'basic_functionality', + old_value: false, + new_value: true, + was_profile_syncing_on: false, + }, + }); + } else { + dispatch(openBasicFunctionalityModal()); + } + }} + title={t('basicConfigurationLabel')} + description={t('basicConfigurationDescription', [ + <a + href="https://consensys.io/privacy-policy" + key="link" + target="_blank" + rel="noreferrer noopener" + > + {t('privacyMsg')} + </a>, + ])} + /> + + <Setting + dataTestId="profile-sync-toggle" + disabled={!externalServicesOnboardingToggleState} + value={profileSyncingProps.isProfileSyncingEnabled} + setValue={handleUseProfileSync} + title={t('profileSync')} + description={t('profileSyncDescription', [ + <a + href="https://support.metamask.io/privacy-and-security/profile-privacy" + key="link" + target="_blank" + rel="noopener noreferrer" + > + {t('profileSyncPrivacyLink')} + </a>, + ])} + /> + + {profileSyncingProps.profileSyncingError && ( + <Box paddingBottom={4}> + <Text + as="p" + color={TextColor.errorDefault} + variant={TextVariant.bodySm} + > + {t('notificationsSettingsBoxError')} + </Text> + </Box> + )} + + <Setting + title={t('onboardingAdvancedPrivacyNetworkTitle')} + showToggle={false} + description={ + <> + {t('onboardingAdvancedPrivacyNetworkDescription', [ + <a + href="https://consensys.io/privacy-policy/" + key="link" + target="_blank" + rel="noopener noreferrer" + > + {t('privacyMsg')} + </a>, + ])} + + <Box paddingTop={4}> <Box display={Display.Flex} - alignItems={AlignItems.center} + flexDirection={FlexDirection.Column} + gap={5} > - <AvatarNetwork - src={ - CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ - network.chainId - ] - } - /> - <Box textAlign={TextAlign.Left} marginLeft={3}> - <Text variant={TextVariant.bodySmMedium}> - {network.name} - </Text> - <Text - variant={TextVariant.bodyXs} - color={TextColor.textAlternative} + {Object.values(networkConfigurations) + .filter( + ({ chainId }) => !TEST_CHAINS.includes(chainId), + ) + .map((network) => ( + <Box + key={network.chainId} + className="privacy-settings__customizable-network" + onClick={() => { + dispatch( + setEditedNetwork({ + chainId: network.chainId, + }), + ); + dispatch(toggleNetworkMenu()); + }} + display={Display.Flex} + alignItems={AlignItems.center} + justifyContent={JustifyContent.spaceBetween} + > + <Box + display={Display.Flex} + alignItems={AlignItems.center} + > + <AvatarNetwork + src={ + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + network.chainId + ] + } + /> + <Box + textAlign={TextAlign.Left} + marginLeft={3} + > + <Text variant={TextVariant.bodySmMedium}> + {network.name} + </Text> + <Text + variant={TextVariant.bodyXs} + color={TextColor.textAlternative} + > + { + // Get just the protocol + domain, not the infura key in path + new URL( + network?.rpcEndpoints[ + network?.defaultRpcEndpointIndex + ]?.url, + )?.origin + } + </Text> + </Box> + </Box> + <ButtonIcon + iconName={IconName.ArrowRight} + size={IconSize.Md} + /> + </Box> + ))} + <ButtonLink + onClick={() => { + dispatch( + toggleNetworkMenu({ + isAddingNewNetwork: true, + }), + ); + }} + justifyContent={JustifyContent.Left} + variant={ButtonVariant.link} + > + <Box + display={Display.Flex} + alignItems={AlignItems.center} > - { - // Get just the protocol + domain, not the infura key in path - new URL( - network?.rpcEndpoints[ - network?.defaultRpcEndpointIndex - ]?.url, - )?.origin - } - </Text> - </Box> + <Icon name={IconName.Add} marginRight={3} /> + <Text color={TextColor.primaryDefault}> + {t('addANetwork')} + </Text> + </Box> + </ButtonLink> </Box> - <ButtonIcon - iconName={IconName.ArrowRight} - size={IconSize.Md} + </Box> + </> + } + /> + </> + ) : null} + {selectedItem && selectedItem.id === 2 ? ( + <> + <Setting + value={turnOnTokenDetection} + setValue={setTurnOnTokenDetection} + title={t('turnOnTokenDetection')} + description={t('useTokenDetectionPrivacyDesc')} + /> + <Setting + value={isTransactionSimulationsEnabled} + setValue={setTransactionSimulationsEnabled} + title={t('simulationsSettingSubHeader')} + description={t('simulationsSettingDescription', [ + <a + key="learn_more_link" + href={TRANSACTION_SIMULATIONS_LEARN_MORE_LINK} + rel="noreferrer" + target="_blank" + > + {t('learnMoreUpperCase')} + </a>, + ])} + /> + <Setting + title={t('onboardingAdvancedPrivacyIPFSTitle')} + showToggle={false} + description={ + <> + {t('onboardingAdvancedPrivacyIPFSDescription')} + <Box paddingTop={2}> + <TextField + value={ipfsURL} + style={{ width: '100%' }} + inputProps={{ 'data-testid': 'ipfs-input' }} + onChange={(e) => { + handleIPFSChange(e.target.value); + }} /> + {ipfsURL ? ( + <Text + variant={TextVariant.bodySm} + color={ + ipfsError + ? TextColor.errorDefault + : TextColor.successDefault + } + > + {ipfsError || + t('onboardingAdvancedPrivacyIPFSValid')} + </Text> + ) : null} </Box> - ))} - <ButtonLink - onClick={() => { - dispatch( - toggleNetworkMenu({ isAddingNewNetwork: true }), - ); - }} - justifyContent={JustifyContent.Left} - variant={ButtonVariant.link} - > - <Box - display={Display.Flex} - alignItems={AlignItems.center} + </> + } + /> + <IncomingTransactionToggle + networkConfigurations={networkConfigurations} + setIncomingTransactionsPreferences={(chainId, value) => + dispatch( + setIncomingTransactionsPreferences(chainId, value), + ) + } + incomingTransactionsPreferences={ + incomingTransactionsPreferences + } + /> + <Setting + value={turnOnCurrencyRateCheck} + setValue={setTurnOnCurrencyRateCheck} + title={t('currencyRateCheckToggle')} + dataTestId="currency-rate-check-toggle" + description={t('currencyRateCheckToggleDescription', [ + <a + key="coingecko_link" + href={COINGECKO_LINK} + rel="noreferrer" + target="_blank" > - <Icon name={IconName.Add} marginRight={3} /> - <Text color={TextColor.primaryDefault}> - {t('addANetwork')} + {t('coingecko')} + </a>, + <a + key="cryptocompare_link" + href={CRYPTOCOMPARE_LINK} + rel="noreferrer" + target="_blank" + > + {t('cryptoCompare')} + </a>, + <a + key="privacy_policy_link" + href={PRIVACY_POLICY_LINK} + rel="noreferrer" + target="_blank" + > + {t('privacyMsg')} + </a>, + ])} + /> + <Setting + value={addressBarResolution} + setValue={setAddressBarResolution} + title={t('ensDomainsSettingTitle')} + description={ + <> + <Text variant={TextVariant.inherit}> + {t('ensDomainsSettingDescriptionIntroduction')} </Text> - </Box> - </ButtonLink> - </Box> - </Box> - </> - } - /> - <Setting - title={t('onboardingAdvancedPrivacyIPFSTitle')} - showToggle={false} - description={ - <> - {t('onboardingAdvancedPrivacyIPFSDescription')} - <Box paddingTop={2}> - <TextField - value={ipfsURL} - style={{ width: '100%' }} - inputProps={{ 'data-testid': 'ipfs-input' }} - onChange={(e) => { - handleIPFSChange(e.target.value); - }} + <Box + as="ul" + marginTop={4} + marginBottom={4} + paddingInlineStart={4} + style={{ listStyleType: 'circle' }} + > + <Text variant={TextVariant.inherit} as="li"> + {t('ensDomainsSettingDescriptionPart1')} + </Text> + <Text variant={TextVariant.inherit} as="li"> + {t('ensDomainsSettingDescriptionPart2')} + </Text> + </Box> + <Text variant={TextVariant.inherit}> + {t('ensDomainsSettingDescriptionOutroduction')} + </Text> + </> + } /> - {ipfsURL ? ( - <Text - variant={TextVariant.bodySm} - color={ - ipfsError - ? TextColor.errorDefault - : TextColor.successDefault - } - > - {ipfsError || t('onboardingAdvancedPrivacyIPFSValid')} - </Text> - ) : null} - </Box> - </> - } - /> - <Setting - value={isTransactionSimulationsEnabled} - setValue={setTransactionSimulationsEnabled} - title={t('simulationsSettingSubHeader')} - description={t('simulationsSettingDescription', [ - <a - key="learn_more_link" - href={TRANSACTION_SIMULATIONS_LEARN_MORE_LINK} - rel="noreferrer" - target="_blank" - > - {t('learnMoreUpperCase')} - </a>, - ])} - /> - <Setting - value={addressBarResolution} - setValue={setAddressBarResolution} - title={t('ensDomainsSettingTitle')} - description={ - <> - <Text variant={TextVariant.inherit}> - {t('ensDomainsSettingDescriptionIntroduction')} - </Text> - <Box - as="ul" - marginTop={4} - marginBottom={4} - paddingInlineStart={4} - style={{ listStyleType: 'circle' }} - > - <Text variant={TextVariant.inherit} as="li"> - {t('ensDomainsSettingDescriptionPart1')} - </Text> - <Text variant={TextVariant.inherit} as="li"> - {t('ensDomainsSettingDescriptionPart2')} - </Text> - </Box> - <Text variant={TextVariant.inherit}> - {t('ensDomainsSettingDescriptionOutroduction')} - </Text> - </> - } - /> - <Setting - value={turnOnCurrencyRateCheck} - setValue={setTurnOnCurrencyRateCheck} - title={t('currencyRateCheckToggle')} - dataTestId="currency-rate-check-toggle" - description={t('currencyRateCheckToggleDescription', [ - <a - key="coingecko_link" - href={COINGECKO_LINK} - rel="noreferrer" - target="_blank" - > - {t('coingecko')} - </a>, - <a - key="cryptocompare_link" - href={CRYPTOCOMPARE_LINK} - rel="noreferrer" - target="_blank" - > - {t('cryptoCompare')} - </a>, - <a - key="privacy_policy_link" - href={PRIVACY_POLICY_LINK} - rel="noreferrer" - target="_blank" - > - {t('privacyMsg')} - </a>, - ])} - /> - <Setting - value={turnOnPetnames} - setValue={setTurnOnPetnames} - title={t('petnamesEnabledToggle')} - description={t('petnamesEnabledToggleDescription')} - /> - <ButtonPrimary - size={ButtonPrimarySize.Lg} - onClick={handleSubmit} - block - marginTop={6} - > - {t('done')} - </ButtonPrimary> + <Setting + value={isMultiAccountBalanceCheckerEnabled} + setValue={setMultiAccountBalanceCheckerEnabled} + title={t('useMultiAccountBalanceChecker')} + description={t( + 'useMultiAccountBalanceCheckerSettingDescription', + )} + /> + </> + ) : null} + {selectedItem && selectedItem.id === 3 ? ( + <> + <Setting + value={phishingToggleState} + setValue={setUsePhishingDetection} + title={t('usePhishingDetection')} + description={t('usePhishingDetectionDescription')} + /> + <Setting + value={turnOn4ByteResolution} + setValue={setTurnOn4ByteResolution} + title={t('use4ByteResolution')} + description={t('use4ByteResolutionDescription')} + /> + <Setting + value={turnOnPetnames} + setValue={setTurnOnPetnames} + title={t('petnamesEnabledToggle')} + description={t('petnamesEnabledToggleDescription')} + /> + </> + ) : null} + </div> + </div> </div> </div> </> diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js index 80561e9376ae..ec8b88fc52e9 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js @@ -97,8 +97,8 @@ describe('Privacy Settings Onboarding View', () => { disableProfileSyncing: disableProfileSyncingStub, }); - it('should update preferences', () => { - const { container, getByText } = renderWithProvider( + it('should update the default settings from each category', () => { + const { container, queryByTestId } = renderWithProvider( <PrivacySettings />, store, ); @@ -114,61 +114,76 @@ describe('Privacy Settings Onboarding View', () => { expect(setUseTransactionSimulationsStub).toHaveBeenCalledTimes(0); expect(setPreferenceStub).toHaveBeenCalledTimes(0); - const toggles = container.querySelectorAll('input[type=checkbox]'); - const submitButton = getByText('Done'); - // TODO: refactor this toggle array, not very readable - // toggle to false + // Default Settings - General category + const itemCategoryGeneral = queryByTestId('category-item-General'); + expect(itemCategoryGeneral).toBeInTheDocument(); + fireEvent.click(itemCategoryGeneral); + + let toggles = container.querySelectorAll('input[type=checkbox]'); + const backButton = queryByTestId('privacy-settings-back-button'); fireEvent.click(toggles[0]); // toggleExternalServicesStub - fireEvent.click(toggles[1]); // setIncomingTransactionsPreferencesStub - fireEvent.click(toggles[2]); // setIncomingTransactionsPreferencesStub (2) - fireEvent.click(toggles[3]); // setIncomingTransactionsPreferencesStub (3) - fireEvent.click(toggles[4]); // setIncomingTransactionsPreferencesStub (4) - fireEvent.click(toggles[5]); // setUsePhishDetectStub - fireEvent.click(toggles[6]); - fireEvent.click(toggles[7]); // setUse4ByteResolutionStub - fireEvent.click(toggles[8]); // setUseTokenDetectionStub - fireEvent.click(toggles[9]); // setUseMultiAccountBalanceCheckerStub - fireEvent.click(toggles[10]); // setUseTransactionSimulationsStub - fireEvent.click(toggles[11]); // setUseAddressBarEnsResolutionStub - fireEvent.click(toggles[12]); // setUseCurrencyRateCheckStub - fireEvent.click(toggles[13]); // setPreferenceStub - - expect(mockOpenBasicFunctionalityModal).toHaveBeenCalledTimes(1); - - fireEvent.click(submitButton); - - expect(toggleExternalServicesStub).toHaveBeenCalledTimes(1); - expect(setIncomingTransactionsPreferencesStub).toHaveBeenCalledTimes(4); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); - expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(1); + + // Default Settings - Assets category + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + + toggles = container.querySelectorAll('input[type=checkbox]'); + + fireEvent.click(toggles[0]); // setUseTokenDetectionStub + fireEvent.click(toggles[1]); // setUseTransactionSimulationsStub + + fireEvent.click(toggles[2]); // setIncomingTransactionsPreferencesStub + fireEvent.click(toggles[3]); // setIncomingTransactionsPreferencesStub (2) + fireEvent.click(toggles[4]); // setIncomingTransactionsPreferencesStub (3) + fireEvent.click(toggles[5]); // setIncomingTransactionsPreferencesStub (4) + + fireEvent.click(toggles[6]); // setUseCurrencyRateCheckStub + fireEvent.click(toggles[7]); // setUseAddressBarEnsResolutionStub + fireEvent.click(toggles[8]); // setUseMultiAccountBalanceCheckerStub + + // Default Settings - Security category + const itemCategorySecurity = queryByTestId('category-item-Security'); + fireEvent.click(itemCategorySecurity); + + toggles = container.querySelectorAll('input[type=checkbox]'); + + fireEvent.click(toggles[0]); // setUsePhishDetectStub + fireEvent.click(toggles[1]); // setUse4ByteResolutionStub + fireEvent.click(toggles[2]); // setPreferenceStub + + fireEvent.click(backButton); + expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(1); - expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(1); - expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(1); - expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(1); + expect(setUseTokenDetectionStub.mock.calls[0][0]).toStrictEqual(true); expect(setUseTransactionSimulationsStub).toHaveBeenCalledTimes(1); - expect(setPreferenceStub).toHaveBeenCalledTimes(1); + expect(setUseTransactionSimulationsStub.mock.calls[0][0]).toStrictEqual( + false, + ); + expect(setIncomingTransactionsPreferencesStub).toHaveBeenCalledTimes(4); expect(setIncomingTransactionsPreferencesStub).toHaveBeenCalledWith( CHAIN_IDS.MAINNET, false, expect.anything(), ); - // toggleExternalServices is true still because modal is "open" but not confirmed yet - expect(toggleExternalServicesStub.mock.calls[0][0]).toStrictEqual(true); - expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); - expect(setUse4ByteResolutionStub.mock.calls[0][0]).toStrictEqual(false); - expect(setUseTokenDetectionStub.mock.calls[0][0]).toStrictEqual(true); - expect(setUseMultiAccountBalanceCheckerStub.mock.calls[0][0]).toStrictEqual( - false, - ); + + expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(1); expect(setUseCurrencyRateCheckStub.mock.calls[0][0]).toStrictEqual(false); + expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(1); expect(setUseAddressBarEnsResolutionStub.mock.calls[0][0]).toStrictEqual( false, ); - expect(setUseTransactionSimulationsStub.mock.calls[0][0]).toStrictEqual( + expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(1); + expect(setUseMultiAccountBalanceCheckerStub.mock.calls[0][0]).toStrictEqual( false, ); + + expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); + expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); + expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(1); + expect(setUse4ByteResolutionStub.mock.calls[0][0]).toStrictEqual(false); + expect(setPreferenceStub).toHaveBeenCalledTimes(1); expect(setPreferenceStub.mock.calls[0][0]).toStrictEqual( 'petnamesEnabled', false, @@ -182,6 +197,9 @@ describe('Privacy Settings Onboarding View', () => { store, ); + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + const ipfsInput = queryByTestId('ipfs-input'); const ipfsEvent = { target: { @@ -194,8 +212,8 @@ describe('Privacy Settings Onboarding View', () => { const validIpfsUrl = queryByText('IPFS gateway URL is valid'); expect(validIpfsUrl).toBeInTheDocument(); - const submitButton = queryByText('Done'); - fireEvent.click(submitButton); + const backButton = queryByTestId('privacy-settings-back-button'); + fireEvent.click(backButton); expect(setIpfsGatewayStub).toHaveBeenCalled(); }); @@ -206,6 +224,9 @@ describe('Privacy Settings Onboarding View', () => { store, ); + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + const ipfsInput = queryByTestId('ipfs-input'); const ipfsEvent = { target: { @@ -226,6 +247,9 @@ describe('Privacy Settings Onboarding View', () => { store, ); + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + const ipfsInput = queryByTestId('ipfs-input'); const ipfsEvent = { target: { diff --git a/ui/pages/onboarding-flow/privacy-settings/setting.js b/ui/pages/onboarding-flow/privacy-settings/setting.js index 31ee059d1126..5811707603c0 100644 --- a/ui/pages/onboarding-flow/privacy-settings/setting.js +++ b/ui/pages/onboarding-flow/privacy-settings/setting.js @@ -7,6 +7,7 @@ import { TextVariant, AlignItems, Display, + TextColor, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -25,7 +26,7 @@ export const Setting = ({ <Box display={Display.Flex} justifyContent={JustifyContent.spaceBetween} - alignItems={AlignItems.center} + alignItems={AlignItems.flexStart} marginTop={3} marginBottom={3} className="privacy-settings__setting__wrapper" @@ -33,7 +34,11 @@ export const Setting = ({ > <div className="privacy-settings__setting"> <Text variant={TextVariant.bodyMdMedium}>{title}</Text> - <Text variant={TextVariant.bodySm} as="div"> + <Text + variant={TextVariant.bodySm} + color={TextColor.textAlternative} + as="div" + > {description} </Text> </div> diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index 343a7f05ecb4..dcec71767fe6 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -551,7 +551,7 @@ exports[`Security Tab should match snapshot 1`] = ` class="mm-box mm-incoming-transaction-toggle" > <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-default" > Show incoming transactions </p> From 1bd1fa480e781dc3b493f6305f21681f1051be09 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 10 Oct 2024 17:22:33 +0100 Subject: [PATCH 117/226] feat: Token send heading component (#27562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27562?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3219 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> #### Token with image <img width="320" alt="Screenshot 2024-10-01 at 14 48 07" src="https://github.com/user-attachments/assets/7cd7e906-ef68-43df-8f14-2561c150c243"> #### Token without image but with symbol <img width="320" alt="Screenshot 2024-10-01 at 14 50 23" src="https://github.com/user-attachments/assets/685e2ec7-5c99-4143-a374-464b407e0106"> #### Token without image and symbol <img width="320" alt="Screenshot 2024-10-01 at 15 12 21" src="https://github.com/user-attachments/assets/1b766a60-1100-4758-bed3-be0a6d557216"> ## **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. --- .../advanced-details-button.test.tsx.snap | 20 +++ .../header/__snapshots__/header.test.tsx.snap | 4 +- .../header/advanced-details-button.test.tsx | 20 +++ .../header/advanced-details-button.tsx | 50 ++++++++ .../components/confirm/header/header-info.tsx | 41 +----- .../header/wallet-initiated-header.test.tsx | 2 +- .../header/wallet-initiated-header.tsx | 39 +----- .../info/hooks/use-token-image.test.ts | 96 ++++++++++++++ .../confirm/info/hooks/use-token-image.ts | 20 +++ .../info/hooks/use-token-values.test.ts | 120 ++++++++++++++++++ .../confirm/info/hooks/use-token-values.ts | 77 +++++++++++ .../confirm/info/shared/selected-token.ts | 7 + .../__snapshots__/send-heading.test.tsx.snap | 20 +++ .../send-heading/send-heading.stories.tsx | 29 +++++ .../shared/send-heading/send-heading.test.tsx | 21 +++ .../info/shared/send-heading/send-heading.tsx | 84 ++++++++++++ .../token-transfer.test.tsx.snap | 19 ++- .../info/token-transfer/token-transfer.tsx | 5 +- ui/selectors/selectors.js | 19 +++ 19 files changed, 616 insertions(+), 77 deletions(-) create mode 100644 ui/pages/confirmations/components/confirm/header/__snapshots__/advanced-details-button.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts create mode 100644 ui/pages/confirmations/components/confirm/info/shared/selected-token.ts create mode 100644 ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx diff --git a/ui/pages/confirmations/components/confirm/header/__snapshots__/advanced-details-button.test.tsx.snap b/ui/pages/confirmations/components/confirm/header/__snapshots__/advanced-details-button.test.tsx.snap new file mode 100644 index 000000000000..66cc3a5a7da7 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/__snapshots__/advanced-details-button.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<AdvancedDetailsButton /> should match snapshot 1`] = ` +<div> + <div + class="mm-box mm-box--margin-right-1 mm-box--background-color-transparent mm-box--rounded-md" + > + <button + aria-label="Advanced tx details" + class="mm-box mm-button-icon mm-button-icon--size-md mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + data-testid="header-advanced-details-button" + > + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/customize.svg');" + /> + </button> + </div> +</div> +`; 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 1af0810d285f..4346963ead15 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 @@ -219,7 +219,7 @@ exports[`Header should match snapshot with token transfer confirmation initiated </div> </div> <div - class="mm-box mm-box--margin-left-4 mm-box--background-color-transparent mm-box--rounded-md" + class="mm-box mm-box--margin-right-1 mm-box--background-color-transparent mm-box--rounded-md" > <button aria-label="Advanced tx details" @@ -383,7 +383,7 @@ exports[`Header should match snapshot with transaction confirmation 1`] = ` </div> </div> <div - class="mm-box mm-box--margin-left-4 mm-box--background-color-transparent mm-box--rounded-md" + class="mm-box mm-box--margin-right-1 mm-box--background-color-transparent mm-box--rounded-md" > <button aria-label="Advanced tx details" diff --git a/ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx b/ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx new file mode 100644 index 000000000000..ab0837a2fb95 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { getMockTokenTransferConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; +import configureStore from '../../../../../store/store'; +import { AdvancedDetailsButton } from './advanced-details-button'; + +const mockStore = getMockTokenTransferConfirmState({}); + +const render = () => { + const store = configureStore(mockStore); + return renderWithConfirmContextProvider(<AdvancedDetailsButton />, store); +}; + +describe('<AdvancedDetailsButton />', () => { + it('should match snapshot', async () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx b/ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx new file mode 100644 index 000000000000..685f1417f064 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Box, + ButtonIcon, + ButtonIconSize, + IconName, +} from '../../../../../components/component-library'; +import { + BackgroundColor, + BorderRadius, + IconColor, +} from '../../../../../helpers/constants/design-system'; +import { setConfirmationAdvancedDetailsOpen } from '../../../../../store/actions'; +import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; + +export const AdvancedDetailsButton = () => { + const dispatch = useDispatch(); + + const showAdvancedDetails = useSelector( + selectConfirmationAdvancedDetailsOpen, + ); + + const setShowAdvancedDetails = (value: boolean): void => { + dispatch(setConfirmationAdvancedDetailsOpen(value)); + }; + + return ( + <Box + backgroundColor={ + showAdvancedDetails + ? BackgroundColor.infoMuted + : BackgroundColor.transparent + } + borderRadius={BorderRadius.MD} + marginRight={1} + > + <ButtonIcon + ariaLabel="Advanced tx details" + color={IconColor.iconDefault} + iconName={IconName.Customize} + data-testid="header-advanced-details-button" + size={ButtonIconSize.Md} + onClick={() => { + setShowAdvancedDetails(!showAdvancedDetails); + }} + /> + </Box> + ); +}; diff --git a/ui/pages/confirmations/components/confirm/header/header-info.tsx b/ui/pages/confirmations/components/confirm/header/header-info.tsx index 5001be21ff07..9cc50b0fe676 100644 --- a/ui/pages/confirmations/components/confirm/header/header-info.tsx +++ b/ui/pages/confirmations/components/confirm/header/header-info.tsx @@ -1,6 +1,6 @@ import { TransactionType } from '@metamask/transaction-controller'; import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { MetaMetricsEventCategory, MetaMetricsEventLocation, @@ -28,8 +28,6 @@ import Tooltip from '../../../../../components/ui/tooltip/tooltip'; import { MetaMetricsContext } from '../../../../../contexts/metametrics'; import { AlignItems, - BackgroundColor, - BorderRadius, Display, FlexDirection, FontWeight, @@ -40,32 +38,22 @@ import { } from '../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { getUseBlockie } from '../../../../../selectors'; -import { setConfirmationAdvancedDetailsOpen } from '../../../../../store/actions'; +import { useConfirmContext } from '../../../context/confirm'; import { useBalance } from '../../../hooks/useBalance'; import useConfirmationRecipientInfo from '../../../hooks/useConfirmationRecipientInfo'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; import { SignatureRequestType } from '../../../types/confirm'; import { isSignatureTransactionType, REDESIGN_DEV_TRANSACTION_TYPES, } from '../../../utils/confirm'; -import { useConfirmContext } from '../../../context/confirm'; +import { AdvancedDetailsButton } from './advanced-details-button'; const HeaderInfo = () => { - const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const useBlockie = useSelector(getUseBlockie); const [showAccountInfo, setShowAccountInfo] = React.useState(false); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - - const setShowAdvancedDetails = (value: boolean): void => { - dispatch(setConfirmationAdvancedDetailsOpen(value)); - }; - const { currentConfirmation } = useConfirmContext(); const { senderAddress: fromAddress, senderName: fromName } = @@ -127,28 +115,7 @@ const HeaderInfo = () => { data-testid="header-info__account-details-button" /> </Tooltip> - {isShowAdvancedDetailsToggle && ( - <Box - backgroundColor={ - showAdvancedDetails - ? BackgroundColor.infoMuted - : BackgroundColor.transparent - } - borderRadius={BorderRadius.MD} - marginLeft={4} - > - <ButtonIcon - ariaLabel={'Advanced tx details'} - color={IconColor.iconDefault} - iconName={IconName.Customize} - data-testid="header-advanced-details-button" - size={ButtonIconSize.Md} - onClick={() => { - setShowAdvancedDetails(!showAdvancedDetails); - }} - /> - </Box> - )} + {isShowAdvancedDetailsToggle && <AdvancedDetailsButton />} </Box> <Modal isOpen={showAccountInfo} diff --git a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.test.tsx b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.test.tsx index f692092ff59f..2a127137e3a9 100644 --- a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.test.tsx +++ b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.test.tsx @@ -13,7 +13,7 @@ const render = ( }; describe('<WalletInitiatedHeader />', () => { - it.only('should match snapshot', () => { + it('should match snapshot', () => { const { container } = render(); expect(container).toMatchSnapshot(); diff --git a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx index c1bca06c74b0..ffc8e7549faf 100644 --- a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx +++ b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx @@ -1,6 +1,6 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { AssetType } from '../../../../../../shared/constants/transaction'; import { @@ -15,7 +15,6 @@ import { editExistingTransaction } from '../../../../../ducks/send'; import { AlignItems, BackgroundColor, - BorderRadius, Display, FlexDirection, IconColor, @@ -25,12 +24,9 @@ import { } from '../../../../../helpers/constants/design-system'; import { SEND_ROUTE } from '../../../../../helpers/constants/routes'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; -import { - setConfirmationAdvancedDetailsOpen, - showSendTokenPage, -} from '../../../../../store/actions'; +import { showSendTokenPage } from '../../../../../store/actions'; import { useConfirmContext } from '../../../context/confirm'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; +import { AdvancedDetailsButton } from './advanced-details-button'; export const WalletInitiatedHeader = () => { const t = useI18nContext(); @@ -39,14 +35,6 @@ export const WalletInitiatedHeader = () => { const { currentConfirmation } = useConfirmContext<TransactionMeta>(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - - const setShowAdvancedDetails = (value: boolean): void => { - dispatch(setConfirmationAdvancedDetailsOpen(value)); - }; - const handleBackButtonClick = useCallback(async () => { const { id } = currentConfirmation; @@ -78,26 +66,7 @@ export const WalletInitiatedHeader = () => { <Text variant={TextVariant.headingMd} color={TextColor.inherit}> {t('review')} </Text> - <Box - backgroundColor={ - showAdvancedDetails - ? BackgroundColor.infoMuted - : BackgroundColor.transparent - } - borderRadius={BorderRadius.MD} - marginRight={1} - > - <ButtonIcon - ariaLabel="Advanced tx details" - color={IconColor.iconDefault} - iconName={IconName.Customize} - data-testid="header-advanced-details-button" - size={ButtonIconSize.Md} - onClick={() => { - setShowAdvancedDetails(!showAdvancedDetails); - }} - /> - </Box> + <AdvancedDetailsButton /> </Box> ); }; diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts new file mode 100644 index 000000000000..23e4cc3c1bda --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts @@ -0,0 +1,96 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; +import { useTokenImage } from './use-token-image'; + +describe('useTokenImage', () => { + it('returns iconUrl from selected token if it exists', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + iconUrl: 'iconUrl', + image: 'image', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ tokenImage: 'iconUrl' }); + }); + + it('returns selected token image if no iconUrl is included', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + image: 'image', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ tokenImage: 'image' }); + }); + + it('returns token list icon url if no image is included in the token', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + { + ...mockState, + metamask: { + ...mockState.metamask, + tokenList: { + '0x076146c765189d51be3160a2140cf80bfc73ad68': { + iconUrl: 'tokenListIconUrl', + }, + }, + }, + }, + ); + + expect(result.current).toEqual({ tokenImage: 'tokenListIconUrl' }); + }); + + it('returns undefined if no image is found', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ tokenImage: undefined }); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts new file mode 100644 index 000000000000..5817d08028ab --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts @@ -0,0 +1,20 @@ +import { TokenListMap } from '@metamask/assets-controllers'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { useSelector } from 'react-redux'; +import { getTokenList } from '../../../../../../selectors'; +import { SelectedToken } from '../shared/selected-token'; + +export const useTokenImage = ( + transactionMeta: TransactionMeta, + selectedToken: SelectedToken, +) => { + const tokenList = useSelector(getTokenList) as TokenListMap; + + // TODO: Add support for NFT images in one of the following tasks + const tokenImage = + selectedToken?.iconUrl || + selectedToken?.image || + tokenList[transactionMeta?.txParams?.to as string]?.iconUrl; + + return { tokenImage }; +}; 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 new file mode 100644 index 000000000000..7ac4aa5b5c92 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts @@ -0,0 +1,120 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +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 useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { useTokenTracker } from '../../../../../../hooks/useTokenTracker'; +import { useTokenValues } from './use-token-values'; + +jest.mock( + '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate', + () => jest.fn(), +); + +jest.mock('../../../../../../hooks/useTokenTracker', () => ({ + ...jest.requireActual('../../../../../../hooks/useTokenTracker'), + useTokenTracker: jest.fn(), +})); + +describe('useTokenValues', () => { + 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', + }); + }); + + it('returns undefined native and fiat balances if no token with balances is returned', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [], + }); + + (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: undefined, + tokenBalance: undefined, + }); + }); + + it('returns undefined fiat balance if no token rate is returned', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [ + { + address: '0x076146c765189d51be3160a2140cf80bfc73ad68', + balance: '1000000000000000000', + decimals: 18, + }, + ], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue(null); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: null, + tokenBalance: '1', + }); + }); +}); 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 new file mode 100644 index 000000000000..9515a45515bf --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -0,0 +1,77 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +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'; + +export const useTokenValues = ( + transactionMeta: TransactionMeta, + selectedToken: SelectedToken, +) => { + const [tokensWithBalances, setTokensWithBalances] = useState< + { balance: string; address: string; decimals: number; string: string }[] + >([]); + + const fetchTokenBalances = async () => { + const result: { + tokensWithBalances: { + balance: string; + address: string; + decimals: number; + string: string; + }[]; + } = await useTokenTracker({ + tokens: [selectedToken], + address: undefined, + }); + + setTokensWithBalances(result.tokensWithBalances); + }; + + fetchTokenBalances(); + + const [exchangeRate, setExchangeRate] = useState<Numeric | undefined>(); + const fetchExchangeRate = async () => { + const result = await useTokenExchangeRate(transactionMeta?.txParams?.to); + + 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(); + + const fiatFormatter = useFiatFormatter(); + + const fiatDisplayValue = + fiatValue && fiatFormatter(fiatValue, { shorten: true }); + + return { + fiatDisplayValue, + tokenBalance: tokenBalance && String(tokenBalance.toNumber()), + }; +}; diff --git a/ui/pages/confirmations/components/confirm/info/shared/selected-token.ts b/ui/pages/confirmations/components/confirm/info/shared/selected-token.ts new file mode 100644 index 000000000000..45abbc6e3032 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/selected-token.ts @@ -0,0 +1,7 @@ +export type SelectedToken = { + address: string; + decimals: number; + symbol: string; + iconUrl?: string; + image?: string; +}; 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 new file mode 100644 index 000000000000..e4222b56cbc5 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/__snapshots__/send-heading.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<SendHeading /> renders component 1`] = ` +<div> + <div + class="mm-box mm-box--padding-top-4 mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-center mm-box--align-items-center" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-token mm-text--body-lg-medium mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-muted mm-box--background-color-overlay-default mm-box--rounded-full" + > + ? + </div> + <h2 + class="mm-box mm-text mm-text--heading-lg mm-box--margin-top-3 mm-box--color-inherit" + > + Unknown + </h2> + </div> +</div> +`; 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 new file mode 100644 index 000000000000..f4bfb484c107 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx @@ -0,0 +1,29 @@ +import { Meta } from '@storybook/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import configureStore from '../../../../../../../store/store'; +import { ConfirmContextProvider } from '../../../../../context/confirm'; +import SendHeading from './send-heading'; + +const store = configureStore(getMockTokenTransferConfirmState({})); + +const Story = { + title: 'Components/App/Confirm/info/SendHeading', + component: SendHeading, + decorators: [ + (story: () => Meta<typeof SendHeading>) => ( + <Provider store={store}>{story()}</Provider> + ), + ], +}; + +export default Story; + +export const DefaultStory = () => ( + <ConfirmContextProvider> + <SendHeading /> + </ConfirmContextProvider> +); + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx new file mode 100644 index 000000000000..613930f9901d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { getMockTokenTransferConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import SendHeading from './send-heading'; + +describe('<SendHeading />', () => { + const middleware = [thunk]; + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore(middleware)(state); + + it('renders component', () => { + const { container } = renderWithConfirmContextProvider( + <SendHeading />, + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 000000000000..d571c61ee93e --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -0,0 +1,84 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { + AvatarToken, + AvatarTokenSize, + 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 { getWatchedToken } from '../../../../../../../selectors'; +import { MultichainState } from '../../../../../../../selectors/multichain'; +import { useConfirmContext } from '../../../../../context/confirm'; +import { useTokenImage } from '../../hooks/use-token-image'; +import { useTokenValues } from '../../hooks/use-token-values'; + +const SendHeading = () => { + const t = useI18nContext(); + const { currentConfirmation: transactionMeta } = + useConfirmContext<TransactionMeta>(); + const selectedToken = useSelector((state: MultichainState) => + getWatchedToken(transactionMeta)(state), + ); + const { tokenImage } = useTokenImage(transactionMeta, selectedToken); + const { tokenBalance, fiatDisplayValue } = useTokenValues( + transactionMeta, + selectedToken, + ); + + const TokenImage = ( + <AvatarToken + src={tokenImage} + name={selectedToken?.symbol} + size={AvatarTokenSize.Xl} + backgroundColor={ + selectedToken?.symbol + ? BackgroundColor.backgroundDefault + : BackgroundColor.overlayDefault + } + color={ + selectedToken?.symbol ? TextColor.textDefault : TextColor.textMuted + } + /> + ); + + const TokenValue = ( + <> + <Text + variant={TextVariant.headingLg} + color={TextColor.inherit} + marginTop={3} + >{`${tokenBalance || ''} ${selectedToken?.symbol || t('unknown')}`}</Text> + {fiatDisplayValue && ( + <Text variant={TextVariant.bodyMd} color={TextColor.textAlternative}> + {fiatDisplayValue} + </Text> + )} + </> + ); + + return ( + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + justifyContent={JustifyContent.center} + alignItems={AlignItems.center} + paddingTop={4} + > + {TokenImage} + {TokenValue} + </Box> + ); +}; + +export default SendHeading; 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 c3aa8e4e26ea..63b44d50173d 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 @@ -1,3 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TokenTransferInfo renders correctly 1`] = `<div />`; +exports[`TokenTransferInfo renders correctly 1`] = ` +<div> + <div + class="mm-box mm-box--padding-top-4 mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-center mm-box--align-items-center" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-token mm-text--body-lg-medium mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-muted mm-box--background-color-overlay-default mm-box--rounded-full" + > + ? + </div> + <h2 + class="mm-box mm-text mm-text--heading-lg mm-box--margin-top-3 mm-box--color-inherit" + > + Unknown + </h2> + </div> +</div> +`; 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 8da9493ebbc4..6fe5ecf166b2 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,5 +1,8 @@ +import React from 'react'; +import SendHeading from '../shared/send-heading/send-heading'; + const TokenTransferInfo = () => { - return null; + return <SendHeading />; }; export default TokenTransferInfo; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 17e6ffc4500a..2059c3a4678d 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -108,6 +108,7 @@ import { MultichainNativeAssets } from '../../shared/constants/multichain/assets // eslint-disable-next-line import/no-restricted-paths import { BridgeFeatureFlagsKey } from '../../app/scripts/controllers/bridge/types'; import { hasTransactionData } from '../../shared/modules/transaction.utils'; +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -537,6 +538,24 @@ export const getSelectedAccount = createDeepEqualSelector( }, ); +export const getWatchedToken = (transactionMeta) => + createSelector( + [getSelectedAccount, getAllTokens], + (selectedAccount, detectedTokens) => { + const { chainId } = transactionMeta; + + const selectedToken = detectedTokens?.[chainId]?.[ + selectedAccount.address + ]?.find( + (token) => + toChecksumHexAddress(token.address) === + toChecksumHexAddress(transactionMeta.txParams.to), + ); + + return selectedToken; + }, + ); + export function getTargetAccount(state, targetAddress) { const accounts = getMetaMaskAccounts(state); return accounts[targetAddress]; From 78e586662b656ee98bffe5e49b455f3f17c8649c Mon Sep 17 00:00:00 2001 From: Matthew Walsh <matthew.walsh@consensys.net> Date: Fri, 11 Oct 2024 09:48:07 +0100 Subject: [PATCH 118/226] feat: support gas fee flows in standard swaps (#27612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Update internal non-smart swaps to support gas fee flows on EIP-1559 networks. This allows swaps to benefit from transaction specific gas fee estimates on Linea chains for example, resulting in lower and more accurate gas fees. This is facilitated by the `estimateGasFee` method of the `TransactionController` and requires the trade and approve transactions to be estimated separately given their alternate transaction data. The changes have been intentionally as light as possible to avoid unnecessary refactor and risk, although a `getSwap1559GasFeeEstimates` utility function has been created to limit duplication between the duck and component. Note that the `ViewQuote` component was intentionally not updated as it is not currently used and pending removal. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27612?quickstart=1) ## **Related issues** Fixes: [#3378](https://github.com/MetaMask/MetaMask-planning/issues/3378) ## **Manual testing steps** - Regression testing of internal swaps. - Smart swaps and standard. - Specific tests on Linea chains. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 1 + ui/ducks/swaps/swaps.js | 106 +++++++++------ .../swaps/prepare-swap-page/review-quote.js | 101 +++++++------- .../prepare-swap-page/review-quote.test.js | 118 +++++++++++++---- ui/pages/swaps/swaps.util.test.js | 123 ++++++++++++++++++ ui/pages/swaps/swaps.util.ts | 102 ++++++++++++--- ui/store/actions.ts | 9 ++ 7 files changed, 433 insertions(+), 127 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index cd899c57e179..96a081e3308d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3609,6 +3609,7 @@ export default class MetamaskController extends EventEmitter { createCancelTransaction: this.createCancelTransaction.bind(this), createSpeedUpTransaction: this.createSpeedUpTransaction.bind(this), estimateGas: this.estimateGas.bind(this), + estimateGasFee: txController.estimateGasFee.bind(txController), getNextNonce: this.getNextNonce.bind(this), addTransaction: (transactionParams, transactionOptions) => addTransaction( diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 97daa88726d3..efbd781f943f 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -5,6 +5,7 @@ import log from 'loglevel'; import { captureMessage } from '@sentry/browser'; import { TransactionType } from '@metamask/transaction-controller'; +import { createProjectLogger } from '@metamask/utils'; import { addToken, addTransactionAndWaitForPublish, @@ -45,6 +46,7 @@ import { getSwapsLivenessForNetwork, parseSmartTransactionsError, StxErrorTypes, + getSwap1559GasFeeEstimates, } from '../../pages/swaps/swaps.util'; import { addHexes, @@ -96,6 +98,8 @@ import { EtherDenomination } from '../../../shared/constants/common'; import { Numeric } from '../../../shared/modules/Numeric'; import { calculateMaxGasLimit } from '../../../shared/lib/swaps-utils'; +const debugLog = createProjectLogger('swaps'); + export const GAS_PRICES_LOADING_STATES = { INITIAL: 'INITIAL', LOADING: 'LOADING', @@ -1087,8 +1091,6 @@ export const signAndSendTransactions = ( } const customSwapsGas = getCustomSwapsGas(state); - const customMaxFeePerGas = getCustomMaxFeePerGas(state); - const customMaxPriorityFeePerGas = getCustomMaxPriorityFeePerGas(state); const fetchParams = getFetchParams(state); const { metaData, value: swapTokenValue, slippage } = fetchParams; const { sourceTokenInfo = {}, destinationTokenInfo = {} } = metaData; @@ -1101,30 +1103,31 @@ export const signAndSendTransactions = ( const { fast: fastGasEstimate } = getSwapGasPriceEstimateData(state); - let maxFeePerGas; - let maxPriorityFeePerGas; - let baseAndPriorityFeePerGas; - let decEstimatedBaseFee; + const usedQuote = getUsedQuote(state); + const usedTradeTxParams = usedQuote.trade; + const approveTxParams = getApproveTxParams(state); + + let transactionGasFeeEstimates; if (networkAndAccountSupports1559) { - const { - high: { suggestedMaxFeePerGas, suggestedMaxPriorityFeePerGas }, - estimatedBaseFee = '0', - } = getGasFeeEstimates(state); - decEstimatedBaseFee = decGWEIToHexWEI(estimatedBaseFee); - maxFeePerGas = - customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); - maxPriorityFeePerGas = - customMaxPriorityFeePerGas || - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas); - baseAndPriorityFeePerGas = addHexes( - decEstimatedBaseFee, - maxPriorityFeePerGas, + const networkGasFeeEstimates = getGasFeeEstimates(state); + const { estimatedBaseFee = '0' } = networkGasFeeEstimates; + + transactionGasFeeEstimates = await getSwap1559GasFeeEstimates( + usedQuote.trade, + approveTxParams, + estimatedBaseFee, + chainId, ); + + debugLog('Received 1559 gas fee estimates', transactionGasFeeEstimates); } - const usedQuote = getUsedQuote(state); - const usedTradeTxParams = usedQuote.trade; + const tradeGasFeeEstimates = + transactionGasFeeEstimates?.tradeGasFeeEstimates; + + const approveGasFeeEstimates = + transactionGasFeeEstimates?.approveGasFeeEstimates; const estimatedGasLimit = new BigNumber(usedQuote?.gasEstimate || 0, 16) .round(0) @@ -1139,38 +1142,57 @@ export const signAndSendTransactions = ( const usedGasPrice = getUsedSwapsGasPrice(state); usedTradeTxParams.gas = maxGasLimit; + if (networkAndAccountSupports1559) { - usedTradeTxParams.maxFeePerGas = maxFeePerGas; - usedTradeTxParams.maxPriorityFeePerGas = maxPriorityFeePerGas; + usedTradeTxParams.maxFeePerGas = tradeGasFeeEstimates?.maxFeePerGas; + usedTradeTxParams.maxPriorityFeePerGas = + tradeGasFeeEstimates?.maxPriorityFeePerGas; delete usedTradeTxParams.gasPrice; } else { usedTradeTxParams.gasPrice = usedGasPrice; } const usdConversionRate = getUSDConversionRate(state); + const destinationValue = calcTokenAmount( usedQuote.destinationAmount, destinationTokenInfo.decimals || 18, ).toPrecision(8); + const usedGasLimitEstimate = usedQuote?.gasEstimateWithRefund || `0x${decimalToHex(usedQuote?.averageGas || 0)}`; - const totalGasLimitEstimate = new BigNumber(usedGasLimitEstimate, 16) - .plus(usedQuote.approvalNeeded?.gas || '0x0', 16) - .toString(16); + + const tradeTotalGasEstimate = calcGasTotal( + usedGasLimitEstimate, + networkAndAccountSupports1559 + ? tradeGasFeeEstimates?.baseAndPriorityFeePerGas + : usedGasPrice, + ); + + const approvalGasLimitEstimate = usedQuote.approvalNeeded?.gas; + + const approvalTotalGasEstimate = approvalGasLimitEstimate + ? calcGasTotal( + approvalGasLimitEstimate, + networkAndAccountSupports1559 + ? approveGasFeeEstimates?.baseAndPriorityFeePerGas + : usedGasPrice, + ) + : '0x0'; + const gasEstimateTotalInUSD = getValueFromWeiHex({ - value: calcGasTotal( - totalGasLimitEstimate, - networkAndAccountSupports1559 ? baseAndPriorityFeePerGas : usedGasPrice, - ), + value: addHexes(tradeTotalGasEstimate, approvalTotalGasEstimate), toCurrency: 'usd', conversionRate: usdConversionRate, numberOfDecimals: 6, }); + const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(state); + const swapMetaData = { token_from: sourceTokenInfo.symbol, token_from_amount: String(swapTokenValue), @@ -1201,10 +1223,13 @@ export const signAndSendTransactions = ( stx_user_opt_in: smartTransactionsOptInStatus, ...additionalTrackingParams, }; + if (networkAndAccountSupports1559) { - swapMetaData.max_fee_per_gas = maxFeePerGas; - swapMetaData.max_priority_fee_per_gas = maxPriorityFeePerGas; - swapMetaData.base_and_priority_fee_per_gas = baseAndPriorityFeePerGas; + swapMetaData.max_fee_per_gas = tradeGasFeeEstimates?.maxFeePerGas; + swapMetaData.max_priority_fee_per_gas = + tradeGasFeeEstimates?.maxPriorityFeePerGas; + swapMetaData.base_and_priority_fee_per_gas = + tradeGasFeeEstimates?.baseAndPriorityFeePerGas; } trackEvent({ @@ -1227,7 +1252,6 @@ export const signAndSendTransactions = ( } let finalApproveTxMeta; - const approveTxParams = getApproveTxParams(state); // For hardware wallets we go to the Awaiting Signatures page first and only after a user // completes 1 or 2 confirmations, we redirect to the Awaiting Swap page. @@ -1237,11 +1261,14 @@ export const signAndSendTransactions = ( if (approveTxParams) { if (networkAndAccountSupports1559) { - approveTxParams.maxFeePerGas = maxFeePerGas; - approveTxParams.maxPriorityFeePerGas = maxPriorityFeePerGas; + approveTxParams.maxFeePerGas = approveGasFeeEstimates?.maxFeePerGas; + approveTxParams.maxPriorityFeePerGas = + approveGasFeeEstimates?.maxPriorityFeePerGas; delete approveTxParams.gasPrice; } + debugLog('Creating approve transaction', approveTxParams); + try { finalApproveTxMeta = await addTransactionAndWaitForPublish( { ...approveTxParams, amount: '0x0' }, @@ -1258,12 +1285,15 @@ export const signAndSendTransactions = ( }, ); } catch (e) { + debugLog('Approve transaction failed', e); await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR)); history.push(SWAPS_ERROR_ROUTE); return; } } + debugLog('Creating trade transaction', usedTradeTxParams); + try { await addTransactionAndWaitForPublish(usedTradeTxParams, { requireApproval: false, @@ -1271,7 +1301,7 @@ export const signAndSendTransactions = ( swaps: { hasApproveTx: Boolean(approveTxParams), meta: { - estimatedBaseFee: decEstimatedBaseFee, + estimatedBaseFee: transactionGasFeeEstimates?.estimatedBaseFee, sourceTokenSymbol: sourceTokenInfo.symbol, destinationTokenSymbol: destinationTokenInfo.symbol, type: TransactionType.swap, @@ -1287,7 +1317,7 @@ export const signAndSendTransactions = ( const errorKey = e.message.includes('EthAppPleaseEnableContractData') ? CONTRACT_DATA_DISABLED_ERROR : SWAP_FAILED_ERROR; - console.error(e); + debugLog('Trade transaction failed', e); await dispatch(setSwapsErrorKey(errorKey)); history.push(SWAPS_ERROR_ROUTE); return; diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 31cf9959f231..9921161c4da4 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -19,7 +19,6 @@ import SelectQuotePopover from '../select-quote-popover'; import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; import { usePrevious } from '../../../hooks/usePrevious'; -import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { getQuotes, @@ -29,9 +28,6 @@ import { getQuotesLastFetched, getBalanceError, getCustomSwapsGas, // Gas limit. - getCustomMaxFeePerGas, - getCustomMaxPriorityFeePerGas, - getSwapsUserFeeLevel, getDestinationTokenInfo, getUsedSwapsGasPrice, getTopQuote, @@ -79,8 +75,6 @@ import { PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import { - addHexes, - decGWEIToHexWEI, decimalToHex, decWEIToDecETH, sumHexes, @@ -92,6 +86,7 @@ import { getRenderableNetworkFeesForQuote, getFeeForSmartTransaction, formatSwapsValueForDisplay, + getSwap1559GasFeeEstimates, } from '../swaps.util'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { @@ -147,6 +142,8 @@ import InfoTooltip from '../../../components/ui/info-tooltip'; import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { getTokenFiatAmount } from '../../../helpers/utils/token-util'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { useAsyncResult } from '../../../hooks/useAsyncResult'; +import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; @@ -222,9 +219,6 @@ export default function ReviewQuote({ setReceiveToAmount }) { // Select necessary data const gasPrice = useSelector(getUsedSwapsGasPrice); const customMaxGas = useSelector(getCustomSwapsGas); - const customMaxFeePerGas = useSelector(getCustomMaxFeePerGas); - const customMaxPriorityFeePerGas = useSelector(getCustomMaxPriorityFeePerGas); - const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); @@ -237,7 +231,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { ); const balanceError = useSelector(getBalanceError); const fetchParams = useSelector(getFetchParams, isEqual); - const approveTxParams = useSelector(getApproveTxParams, shallowEqual); + const approveTxParams = useSelector(getApproveTxParams, isEqual); const topQuote = useSelector(getTopQuote, isEqual); const usedQuote = useSelector(getUsedQuote, isEqual); const tradeValue = usedQuote?.trade?.value ?? '0x0'; @@ -259,6 +253,30 @@ export default function ReviewQuote({ setReceiveToAmount }) { ); const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); + const { estimatedBaseFee = '0' } = useGasFeeEstimates(); + + const gasFeeEstimates = useAsyncResult(async () => { + if (!networkAndAccountSupports1559) { + return undefined; + } + + return await getSwap1559GasFeeEstimates( + usedQuote.trade, + approveTxParams, + estimatedBaseFee, + chainId, + ); + }, [ + usedQuote.trade, + approveTxParams, + estimatedBaseFee, + chainId, + networkAndAccountSupports1559, + ]); + + const gasFeeEstimatesTrade = gasFeeEstimates.value?.tradeGasFeeEstimates; + const gasFeeEstimatesApprove = gasFeeEstimates.value?.approveGasFeeEstimates; + const unsignedTransaction = usedQuote.trade; const { isGasIncludedTrade } = usedQuote; const isSmartTransaction = @@ -274,15 +292,6 @@ export default function ReviewQuote({ setReceiveToAmount }) { return ''; }); - let gasFeeInputs; - if (networkAndAccountSupports1559) { - // For Swaps we want to get 'high' estimations by default. - // eslint-disable-next-line react-hooks/rules-of-hooks - gasFeeInputs = useGasFeeInputs(GasRecommendations.high, { - userFeeLevel: swapsUserFeeLevel || GasRecommendations.high, - }); - } - const fetchParamsSourceToken = fetchParams?.sourceToken; const additionalTrackingParams = { @@ -307,27 +316,11 @@ export default function ReviewQuote({ setReceiveToAmount }) { customMaxGas, ); - let maxFeePerGas; - let maxPriorityFeePerGas; - let baseAndPriorityFeePerGas; - - // EIP-1559 gas fees. - if (networkAndAccountSupports1559) { - const { - maxFeePerGas: suggestedMaxFeePerGas, - maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas, - gasFeeEstimates: { estimatedBaseFee = '0' } = {}, - } = gasFeeInputs; - maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); - maxPriorityFeePerGas = - customMaxPriorityFeePerGas || - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas); - baseAndPriorityFeePerGas = addHexes( - decGWEIToHexWEI(estimatedBaseFee), - maxPriorityFeePerGas, - ); - } - let gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); + let gasTotalInWeiHex = calcGasTotal( + maxGasLimit, + gasFeeEstimatesTrade?.maxFeePerGas || gasPrice, + ); + if (multiLayerL1FeeTotal !== null) { gasTotalInWeiHex = sumHexes( gasTotalInWeiHex || '0x0', @@ -364,12 +357,19 @@ export default function ReviewQuote({ setReceiveToAmount }) { calcTokenAmount(approveValue, selectedFromToken.decimals).toFixed(9); const approveGas = approveTxParams?.gas; + const gasPriceTrade = networkAndAccountSupports1559 + ? gasFeeEstimatesTrade?.baseAndPriorityFeePerGas + : gasPrice; + + const gasPriceApprove = networkAndAccountSupports1559 + ? gasFeeEstimatesApprove?.baseAndPriorityFeePerGas + : gasPrice; + const renderablePopoverData = useMemo(() => { return quotesToRenderableData({ quotes, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -384,9 +384,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { }); }, [ quotes, - gasPrice, - baseAndPriorityFeePerGas, - networkAndAccountSupports1559, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -417,9 +416,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { getRenderableNetworkFeesForQuote({ tradeGas: usedGasLimit, approveGas, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -436,7 +434,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { const renderableMaxFees = getRenderableNetworkFeesForQuote({ tradeGas: maxGasLimit, approveGas, - gasPrice: maxFeePerGas || gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -882,7 +881,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { tokenBalanceUnavailable || disableSubmissionDueToPriceWarning || (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || + gasFeeEstimatesTrade?.baseAndPriorityFeePerGas === undefined) || (!networkAndAccountSupports1559 && (gasPrice === null || gasPrice === undefined)) || (currentSmartTransactionsEnabled && diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.test.js b/ui/pages/swaps/prepare-swap-page/review-quote.test.js index 8dad2a52280c..cacd52ca47ed 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.test.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.test.js @@ -3,13 +3,13 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { NetworkType } from '@metamask/controller-utils'; -import { setBackgroundConnection } from '../../../store/background-connection'; +import { act } from '@testing-library/react'; import { renderWithProvider, createSwapsMockStore, - MOCKS, } from '../../../../test/jest'; import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { getSwap1559GasFeeEstimates } from '../swaps.util'; import ReviewQuote from './review-quote'; jest.mock( @@ -17,17 +17,10 @@ jest.mock( () => () => '<InfoTooltipIcon />', ); -jest.mock('../../confirmations/hooks/useGasFeeInputs', () => { - return { - useGasFeeInputs: () => { - return { - maxFeePerGas: 16, - maxPriorityFeePerGas: 3, - gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(), - }; - }, - }; -}); +jest.mock('../swaps.util', () => ({ + ...jest.requireActual('../swaps.util'), + getSwap1559GasFeeEstimates: jest.fn(), +})); const middleware = [thunk]; const createProps = (customProps = {}) => { @@ -37,16 +30,11 @@ const createProps = (customProps = {}) => { }; }; -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - safeRefetchQuotes: jest.fn(), - setSwapsErrorKey: jest.fn(), - updateTransaction: jest.fn(), - getGasFeeTimeEstimate: jest.fn(), - setSwapsQuotesPollingLimitEnabled: jest.fn(), -}); - describe('ReviewQuote', () => { + const getSwap1559GasFeeEstimatesMock = jest.mocked( + getSwap1559GasFeeEstimates, + ); + it('renders the component with initial props', () => { const store = configureMockStore(middleware)(createSwapsMockStore()); const props = createProps(); @@ -137,4 +125,90 @@ describe('ReviewQuote', () => { expect(getByText('$6.82')).toBeInTheDocument(); expect(getByText('Swap')).toBeInTheDocument(); }); + + describe('uses gas fee estimates from transaction controller if 1559 and smart disabled', () => { + let smartDisabled1559State; + + beforeEach(() => { + smartDisabled1559State = createSwapsMockStore(); + smartDisabled1559State.metamask.selectedNetworkClientId = + NetworkType.mainnet; + smartDisabled1559State.metamask.networksMetadata = { + [NetworkType.mainnet]: { + EIPS: { 1559: true }, + status: 'available', + }, + }; + smartDisabled1559State.metamask.preferences.smartTransactionsOptInStatus = false; + }); + + it('with only trade transaction', async () => { + getSwap1559GasFeeEstimatesMock.mockResolvedValueOnce({ + estimatedBaseFee: '0x1', + tradeGasFeeEstimates: { + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + baseAndPriorityFeePerGas: '0x123456789123', + }, + approveGasFeeEstimates: undefined, + }); + + const store = configureMockStore(middleware)(smartDisabled1559State); + const props = createProps(); + const { getByText } = renderWithProvider( + <ReviewQuote {...props} />, + store, + ); + + await act(() => { + // Intentionally empty + }); + + expect(getByText('Estimated gas fee')).toBeInTheDocument(); + expect(getByText('3.94315 ETH')).toBeInTheDocument(); + expect(getByText('Max fee:')).toBeInTheDocument(); + expect(getByText('$7.37')).toBeInTheDocument(); + }); + + it('with trade and approve transactions', async () => { + smartDisabled1559State.metamask.swapsState.quotes.TEST_AGG_2.approvalNeeded = + { + data: '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00', + to: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + amount: '0', + from: '0x2369267687A84ac7B494daE2f1542C40E37f4455', + gas: '123456', + }; + + getSwap1559GasFeeEstimatesMock.mockResolvedValueOnce({ + estimatedBaseFee: '0x1', + tradeGasFeeEstimates: { + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + baseAndPriorityFeePerGas: '0x123456789123', + }, + approveGasFeeEstimates: { + maxFeePerGas: '0x4', + maxPriorityFeePerGas: '0x5', + baseAndPriorityFeePerGas: '0x9876543210', + }, + }); + + const store = configureMockStore(middleware)(smartDisabled1559State); + const props = createProps(); + const { getByText } = renderWithProvider( + <ReviewQuote {...props} />, + store, + ); + + await act(() => { + // Intentionally empty + }); + + expect(getByText('Estimated gas fee')).toBeInTheDocument(); + expect(getByText('4.72438 ETH')).toBeInTheDocument(); + expect(getByText('Max fee:')).toBeInTheDocument(); + expect(getByText('$8.15')).toBeInTheDocument(); + }); + }); }); diff --git a/ui/pages/swaps/swaps.util.test.js b/ui/pages/swaps/swaps.util.test.js index 4b277ab56345..d081e8d58ee1 100644 --- a/ui/pages/swaps/swaps.util.test.js +++ b/ui/pages/swaps/swaps.util.test.js @@ -18,6 +18,7 @@ import { LINEA, BASE, } from '../../../shared/constants/swaps'; +import { estimateGasFee } from '../../store/actions'; import { TOKENS, EXPECTED_TOKENS_RESULT, @@ -36,6 +37,7 @@ import { getFeeForSmartTransaction, formatSwapsValueForDisplay, fetchTopAssetsList, + getSwap1559GasFeeEstimates, } from './swaps.util'; jest.mock('../../../shared/lib/storage-helpers', () => ({ @@ -43,7 +45,24 @@ jest.mock('../../../shared/lib/storage-helpers', () => ({ setStorageItem: jest.fn(), })); +jest.mock('../../store/actions', () => ({ + estimateGasFee: jest.fn(), +})); + +const ESTIMATED_BASE_FEE_GWEI_MOCK = '1'; +const TRADE_TX_PARAMS_MOCK = { data: '0x123' }; +const APPROVE_TX_PARAMS_MOCK = { data: '0x456' }; +const CHAIN_ID_MOCK = '0x1'; +const MAX_FEE_PER_GAS_MOCK = '0x1'; +const MAX_PRIORITY_FEE_PER_GAS_MOCK = '0x2'; + describe('Swaps Util', () => { + const estimateGasFeeMock = jest.mocked(estimateGasFee); + + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { nock.cleanAll(); }); @@ -545,4 +564,108 @@ describe('Swaps Util', () => { ).toBeNull(); }); }); + + describe('getSwap1559GasFeeEstimates', () => { + it('returns estimated base fee in WEI as hex', async () => { + estimateGasFeeMock.mockResolvedValueOnce({}); + + const { estimatedBaseFee } = await getSwap1559GasFeeEstimates( + {}, + undefined, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(estimatedBaseFee).toBe('3b9aca00'); + }); + + it('returns trade gas fee estimates', async () => { + estimateGasFeeMock.mockResolvedValueOnce({ + estimates: { + high: { + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + }, + }, + }); + + const { tradeGasFeeEstimates } = await getSwap1559GasFeeEstimates( + TRADE_TX_PARAMS_MOCK, + undefined, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(tradeGasFeeEstimates).toStrictEqual({ + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + baseAndPriorityFeePerGas: '3b9aca02', + }); + + expect(estimateGasFeeMock).toHaveBeenCalledTimes(1); + expect(estimateGasFeeMock).toHaveBeenCalledWith({ + transactionParams: TRADE_TX_PARAMS_MOCK, + chainId: CHAIN_ID_MOCK, + networkClientId: undefined, + }); + }); + + it('returns approve gas fee estimates if approve params', async () => { + estimateGasFeeMock.mockResolvedValueOnce({}); + estimateGasFeeMock.mockResolvedValueOnce({ + estimates: { + high: { + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + }, + }, + }); + + const { approveGasFeeEstimates } = await getSwap1559GasFeeEstimates( + TRADE_TX_PARAMS_MOCK, + APPROVE_TX_PARAMS_MOCK, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(approveGasFeeEstimates).toStrictEqual({ + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + baseAndPriorityFeePerGas: '3b9aca02', + }); + + expect(estimateGasFeeMock).toHaveBeenCalledTimes(2); + expect(estimateGasFeeMock).toHaveBeenCalledWith({ + transactionParams: TRADE_TX_PARAMS_MOCK, + chainId: CHAIN_ID_MOCK, + networkClientId: undefined, + }); + expect(estimateGasFeeMock).toHaveBeenCalledWith({ + transactionParams: APPROVE_TX_PARAMS_MOCK, + chainId: CHAIN_ID_MOCK, + networkClientId: undefined, + }); + }); + + it('returns no approve gas fee estimates if no approve params', async () => { + estimateGasFeeMock.mockResolvedValueOnce({}); + estimateGasFeeMock.mockResolvedValueOnce({ + estimates: { + high: { + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + }, + }, + }); + + const { approveGasFeeEstimates } = await getSwap1559GasFeeEstimates( + TRADE_TX_PARAMS_MOCK, + undefined, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(approveGasFeeEstimates).toBeUndefined(); + }); + }); }); diff --git a/ui/pages/swaps/swaps.util.ts b/ui/pages/swaps/swaps.util.ts index 21de45b5f349..9cbf0b67a867 100644 --- a/ui/pages/swaps/swaps.util.ts +++ b/ui/pages/swaps/swaps.util.ts @@ -1,6 +1,10 @@ import { BigNumber } from 'bignumber.js'; -import { Json } from '@metamask/utils'; +import { Hex, Json } from '@metamask/utils'; import { IndividualTxFees } from '@metamask/smart-transactions-controller/dist/types'; +import { + FeeMarketGasFeeEstimates, + TransactionParams, +} from '@metamask/transaction-controller'; import { ALLOWED_CONTRACT_ADDRESSES, ARBITRUM, @@ -39,11 +43,14 @@ import { validateData, } from '../../../shared/lib/swaps-utils'; import { + addHexes, + decGWEIToHexWEI, decimalToHex, getValueFromWeiHex, sumHexes, } from '../../../shared/modules/conversion.utils'; import { EtherDenomination } from '../../../shared/constants/common'; +import { estimateGasFee } from '../../store/actions'; const CACHE_REFRESH_FIVE_MINUTES = 300000; const USD_CURRENCY_CODE = 'usd'; @@ -355,7 +362,8 @@ export const getFeeForSmartTransaction = ({ export function getRenderableNetworkFeesForQuote({ tradeGas, approveGas, - gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -368,7 +376,8 @@ export function getRenderableNetworkFeesForQuote({ }: { tradeGas: string; approveGas: string; - gasPrice: string; + gasPriceTrade: string; + gasPriceApprove: string; currentCurrency: string; conversionRate: number; USDConversionRate?: number; @@ -386,16 +395,17 @@ export function getRenderableNetworkFeesForQuote({ feeInEth: string; nonGasFee: string; } { - const totalGasLimitForCalculation = new BigNumber(tradeGas || '0x0', 16) - .plus(approveGas || '0x0', 16) - .toString(16); - let gasTotalInWeiHex = calcGasTotal(totalGasLimitForCalculation, gasPrice); - if (multiLayerL1FeeTotal !== null) { - gasTotalInWeiHex = sumHexes( - gasTotalInWeiHex || '0x0', - multiLayerL1FeeTotal || '0x0', - ); - } + const tradeGasFeeTotalHex = calcGasTotal(tradeGas, gasPriceTrade); + + const approveGasFeeTotalHex = approveGas + ? calcGasTotal(approveGas, gasPriceApprove) + : '0x0'; + + const gasTotalInWeiHex = sumHexes( + tradeGasFeeTotalHex, + approveGasFeeTotalHex, + multiLayerL1FeeTotal || '0x0', + ); const nonGasFee = new BigNumber(tradeValue, 16) .minus( @@ -447,7 +457,8 @@ export function getRenderableNetworkFeesForQuote({ export function quotesToRenderableData({ quotes, - gasPrice, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -458,7 +469,8 @@ export function quotesToRenderableData({ multiLayerL1ApprovalFeeTotal, }: { quotes: object; - gasPrice: string; + gasPriceTrade: string; + gasPriceApprove: string; conversionRate: number; currentCurrency: string; approveGas: string; @@ -517,7 +529,8 @@ export function quotesToRenderableData({ getRenderableNetworkFeesForQuote({ tradeGas: gasEstimateWithRefund || decimalToHex(averageGas || 800000), approveGas, - gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, tradeValue: trade.value, @@ -780,3 +793,60 @@ export const parseSmartTransactionsError = (errorMessage: string): string => { const errorJson = errorMessage.slice(12); return JSON.parse(errorJson.trim()); }; + +export const getSwap1559GasFeeEstimates = async ( + tradeTxParams: TransactionParams, + approveTxParams: TransactionParams | undefined, + estimatedBaseFeeGwei: string, + chainId: Hex, +) => { + const estimatedBaseFee = decGWEIToHexWEI(estimatedBaseFeeGwei) as Hex; + + const tradeGasFeeEstimates = await getTransaction1559GasFeeEstimates( + tradeTxParams, + estimatedBaseFee, + chainId, + ); + + const approveGasFeeEstimates = approveTxParams + ? await getTransaction1559GasFeeEstimates( + approveTxParams, + estimatedBaseFee, + chainId, + ) + : undefined; + + return { + tradeGasFeeEstimates, + approveGasFeeEstimates, + estimatedBaseFee, + }; +}; + +async function getTransaction1559GasFeeEstimates( + transactionParams: TransactionParams, + estimatedBaseFee: Hex, + chainId: Hex, +) { + const transactionGasFeeResponse = await estimateGasFee({ + transactionParams, + chainId, + }); + + const transactionGasFeeEstimates = transactionGasFeeResponse?.estimates as + | FeeMarketGasFeeEstimates + | undefined; + + const { maxFeePerGas } = transactionGasFeeEstimates?.high ?? {}; + const { maxPriorityFeePerGas } = transactionGasFeeEstimates?.high ?? {}; + + const baseAndPriorityFeePerGas = maxPriorityFeePerGas + ? (addHexes(estimatedBaseFee, maxPriorityFeePerGas) as Hex) + : undefined; + + return { + baseAndPriorityFeePerGas, + maxFeePerGas, + maxPriorityFeePerGas, + }; +} diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 3dbf61ba0386..91453590791c 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -28,6 +28,7 @@ import { UpdateProposedNamesResult, } from '@metamask/name-controller'; import { + GasFeeEstimates, TransactionMeta, TransactionParams, TransactionType, @@ -4488,6 +4489,14 @@ export function estimateGas(params: TransactionParams): Promise<Hex> { return submitRequestToBackground('estimateGas', [params]); } +export function estimateGasFee(request: { + transactionParams: TransactionParams; + chainId?: Hex; + networkClientId?: NetworkClientId; +}): Promise<{ estimates: GasFeeEstimates }> { + return submitRequestToBackground('estimateGasFee', [request]); +} + export async function updateTokenType( tokenAddress: string, ): Promise<Token | undefined> { From 05dda700a9f6ba11f3336d1f6d3c9e9cad6d7f08 Mon Sep 17 00:00:00 2001 From: David Walsh <davidwalsh83@gmail.com> Date: Fri, 11 Oct 2024 05:00:31 -0500 Subject: [PATCH 119/226] test: Onboarding: Fix vault-decryption-chrome.spec.js (#27779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Fixes the vault decryption test broken by https://github.com/MetaMask/metamask-extension/pull/24562/files#diff-d62d4e96adf6102c2b13c65f73e2c276fc08d4a93edeb969a2a1c8bb23679f56 The test broke due to (1) text change and (2) forward/backward navigation of onboarding [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27779?quickstart=1) ## **Related issues** Fixes: #27776 ## **Manual testing steps** 1. Run test 2. It succeeds ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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/helpers.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 21cc84a6fcb9..bf55c7bbf52c 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -550,11 +550,23 @@ const onboardingCompleteWalletCreation = async (driver) => { await driver.clickElement('[data-testid="onboarding-complete-done"]'); }; +/** + * Move through the steps of pinning extension after successful onboarding + * + * @param {WebDriver} driver + */ +const onboardingPinExtension = async (driver) => { + // pin extension + await driver.clickElement('[data-testid="pin-extension-next"]'); + await driver.clickElement('[data-testid="pin-extension-done"]'); +}; + const onboardingCompleteWalletCreationWithOptOut = async (driver) => { // wait for h2 to appear - await driver.findElement({ text: 'Wallet creation successful', tag: 'h2' }); + await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); // opt-out from third party API - await driver.clickElement({ text: 'Manage default settings', tag: 'a' }); + await driver.clickElement({ text: 'Manage default settings', tag: 'button' }); + await driver.clickElement({ text: 'General', tag: 'p' }); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); @@ -568,19 +580,12 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { ) ).map((toggle) => toggle.click()), ); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.clickElement('[data-testid="privacy-settings-back-button"]'); + // complete onboarding await driver.clickElement({ text: 'Done', tag: 'button' }); -}; - -/** - * Move through the steps of pinning extension after successful onboarding - * - * @param {WebDriver} driver - */ -const onboardingPinExtension = async (driver) => { - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); + await onboardingPinExtension(driver); }; const completeCreateNewWalletOnboardingFlowWithOptOut = async ( From ca1281b4562cd4d0e830208a5cdf82c64c3fd03b Mon Sep 17 00:00:00 2001 From: Guillaume Roux <guillaumeroux123@gmail.com> Date: Fri, 11 Oct 2024 15:03:10 +0200 Subject: [PATCH 120/226] fix(snaps): Restore confirmation switching on routed confirmation (#27753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes the inability to switch between pending confirmations when routing to a specific confirmation. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27753?quickstart=1) ## **Related issues** Fixes: #27695 ## **Manual testing steps** 1. Redirect to a confirmation 2. Trigger another confirmation after 3. try to switch between the two ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/95b002fd-c4d2-4521-8e21-ac2f6f42e1d8 ### **After** https://github.com/user-attachments/assets/72e95e8d-a2d9-4024-8ba5-e3b91d409078 ## **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. --- .../confirmations/confirmation/confirmation.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ui/pages/confirmations/confirmation/confirmation.js b/ui/pages/confirmations/confirmation/confirmation.js index b37743c256bc..12b2af503f7f 100644 --- a/ui/pages/confirmations/confirmation/confirmation.js +++ b/ui/pages/confirmations/confirmation/confirmation.js @@ -217,16 +217,19 @@ export default function ConfirmationPage({ ); const [approvalFlowLoadingText, setApprovalFlowLoadingText] = useState(null); - const [currentPendingConfirmation, setCurrentPendingConfirmation] = - useState(0); const { id } = useParams(); - const pendingRoutedConfirmation = pendingConfirmations.find( + const pendingRoutedConfirmation = pendingConfirmations.findIndex( (confirmation) => confirmation.id === id, ); - // Confirmations that are directly routed to get priority and will be shown above the current queue. - const pendingConfirmation = - pendingRoutedConfirmation ?? - pendingConfirmations[currentPendingConfirmation]; + + const isRoutedConfirmation = id && pendingRoutedConfirmation !== -1; + + const [currentPendingConfirmation, setCurrentPendingConfirmation] = useState( + // Confirmations that are directly routed to get priority and will be initially shown above the current queue. + isRoutedConfirmation ? pendingRoutedConfirmation : 0, + ); + + const pendingConfirmation = pendingConfirmations[currentPendingConfirmation]; const [matchedChain, setMatchedChain] = useState({}); const [chainFetchComplete, setChainFetchComplete] = useState(false); From a4484f2bf7f78640af42438c5346c24e80196669 Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:04:43 +0200 Subject: [PATCH 121/226] test: [POM] Migrate transaction with snap account e2e tests to page object modal (#27760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the `snap-account-transfers.spec.ts` e2e tests to the Page Object Model (POM) pattern, improving test stability and maintainability. Changes include: - Migrate test snap-account-transfers.spec.ts to POM - Avoid several delays in the original function implementation - Reduce flakiness [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27765 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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> --- .../snap-account-contract-interaction.spec.ts | 7 +- .../accounts/snap-account-eth-swap.spec.ts | 15 +- .../accounts/snap-account-transfers.spec.ts | 94 ---------- .../flows/send-transaction.flow.ts | 30 +++- .../flows/snap-simple-keyring.flow.ts | 2 +- .../page-objects/pages/account-list-page.ts | 18 ++ test/e2e/page-objects/pages/header-navbar.ts | 33 ++-- test/e2e/page-objects/pages/homepage.ts | 59 +++++-- .../pages/snap-simple-keyring-page.ts | 152 ++++++++++++++-- .../account/snap-account-transfers.spec.ts | 165 ++++++++++++++++++ .../e2e/tests/transaction/simple-send.spec.ts | 1 + 11 files changed, 431 insertions(+), 145 deletions(-) delete mode 100644 test/e2e/accounts/snap-account-transfers.spec.ts create mode 100644 test/e2e/tests/account/snap-account-transfers.spec.ts diff --git a/test/e2e/accounts/snap-account-contract-interaction.spec.ts b/test/e2e/accounts/snap-account-contract-interaction.spec.ts index 4588d014802c..885e048272d8 100644 --- a/test/e2e/accounts/snap-account-contract-interaction.spec.ts +++ b/test/e2e/accounts/snap-account-contract-interaction.spec.ts @@ -14,8 +14,10 @@ import { 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 { installSnapSimpleKeyring, importKeyAndSwitch } from './common'; +import { importKeyAndSwitch } from './common'; describe('Snap Account Contract interaction', function () { const smartContract = SMART_CONTRACTS.PIGGYBANK; @@ -43,7 +45,8 @@ describe('Snap Account Contract interaction', function () { ganacheServer, }: TestSuiteArguments) => { // Install Snap Simple Keyring and import key - await installSnapSimpleKeyring(driver, false); + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver); await importKeyAndSwitch(driver); // Open DApp with contract diff --git a/test/e2e/accounts/snap-account-eth-swap.spec.ts b/test/e2e/accounts/snap-account-eth-swap.spec.ts index 2a99287e5db1..2a0f230f0d54 100644 --- a/test/e2e/accounts/snap-account-eth-swap.spec.ts +++ b/test/e2e/accounts/snap-account-eth-swap.spec.ts @@ -1,6 +1,7 @@ import { withFixtures, defaultGanacheOptions, WINDOW_TITLES } from '../helpers'; import { Driver } from '../webdriver/driver'; import FixtureBuilder from '../fixture-builder'; +import { Ganache } from '../seeder/ganache'; import { buildQuote, reviewQuote, @@ -8,8 +9,9 @@ import { checkActivityTransaction, } from '../tests/swaps/shared'; import { TRADES_API_MOCK_RESULT } from '../../data/mock-data'; +import { installSnapSimpleKeyring } from '../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; import { Mockttp } from '../mock-e2e'; -import { installSnapSimpleKeyring } from './common'; const DAI = 'DAI'; const TEST_ETH = 'TESTETH'; @@ -34,8 +36,15 @@ describe('Snap Account - Swap', function () { title: this.test?.fullTitle(), testSpecificMock: mockSwapsTransactionQuote, }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); diff --git a/test/e2e/accounts/snap-account-transfers.spec.ts b/test/e2e/accounts/snap-account-transfers.spec.ts deleted file mode 100644 index cb344e188640..000000000000 --- a/test/e2e/accounts/snap-account-transfers.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Suite } from 'mocha'; -import { - sendTransaction, - withFixtures, - WINDOW_TITLES, - clickNestedButton, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - PUBLIC_KEY, - installSnapSimpleKeyring, - importKeyAndSwitch, - approveOrRejectRequest, -} from './common'; - -describe('Snap Account Transfers', function (this: Suite) { - it('can import a private key and transfer 1 ETH (sync flow)', async function () { - await withFixtures( - accountSnapFixtures(this.test?.fullTitle()), - async ({ driver }: { driver: Driver }) => { - await importPrivateKeyAndTransfer1ETH(driver, 'sync'); - }, - ); - }); - - it('can import a private key and transfer 1 ETH (async flow approve)', async function () { - await withFixtures( - accountSnapFixtures(this.test?.fullTitle()), - async ({ driver }: { driver: Driver }) => { - await importPrivateKeyAndTransfer1ETH(driver, 'approve'); - }, - ); - }); - - it('can import a private key and transfer 1 ETH (async flow reject)', async function () { - await withFixtures( - accountSnapFixtures(this.test?.fullTitle()), - async ({ driver }: { driver: Driver }) => { - await importPrivateKeyAndTransfer1ETH(driver, 'reject'); - }, - ); - }); - - /** - * @param driver - * @param flowType - */ - async function importPrivateKeyAndTransfer1ETH( - driver: Driver, - flowType: string, - ) { - const isAsyncFlow = flowType !== 'sync'; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - await importKeyAndSwitch(driver); - - // send 1 ETH from Account 2 to Account 1 - await sendTransaction(driver, PUBLIC_KEY, 1, isAsyncFlow); - - if (isAsyncFlow) { - await driver.assertElementNotPresent({ - text: 'Please complete the transaction on the Snap.', - }); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await driver.navigate(); - await driver.delay(2000); - await driver.clickElement({ - text: 'Go to site', - tag: 'button', - }); - - await driver.delay(1000); - await approveOrRejectRequest(driver, flowType); - } - - if (flowType === 'sync' || flowType === 'approve') { - // click on Accounts - await driver.clickElement('[data-testid="account-menu-icon"]'); - - // ensure one account has 26 ETH and the other has 24 ETH - await driver.findElement('[title="26 ETH"]'); - await driver.findElement('[title="24 ETH"]'); - } else if (flowType === 'reject') { - // ensure the transaction was rejected by the Snap - await clickNestedButton(driver, 'Activity'); - await driver.findElement( - '[data-original-title="Request rejected by user or snap."]', - ); - } - } -}); diff --git a/test/e2e/page-objects/flows/send-transaction.flow.ts b/test/e2e/page-objects/flows/send-transaction.flow.ts index 8291dc96a4e6..1cffe2428873 100644 --- a/test/e2e/page-objects/flows/send-transaction.flow.ts +++ b/test/e2e/page-objects/flows/send-transaction.flow.ts @@ -2,6 +2,7 @@ import HomePage from '../pages/homepage'; import ConfirmTxPage from '../pages/send/confirm-tx-page'; import SendTokenPage from '../pages/send/send-token-page'; import { Driver } from '../../webdriver/driver'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; /** * This function initiates the steps required to send a transaction from the homepage to final confirmation. @@ -37,7 +38,32 @@ export const sendTransaction = async ( const confirmTxPage = new ConfirmTxPage(driver); await confirmTxPage.check_pageIsLoaded(gasfee, totalfee); await confirmTxPage.confirmTx(); +}; - // user should land on homepage after transaction is confirmed - await homePage.check_pageIsLoaded(); +/** + * This function initiates the steps required to send a transaction from snap account on homepage to final confirmation. + * + * @param driver - The webdriver instance. + * @param recipientAddress - The recipient address. + * @param amount - The amount of the asset to be sent in the transaction. + * @param gasfee - The expected transaction gas fee. + * @param totalfee - The expected total transaction fee. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const sendTransactionWithSnapAccount = async ( + driver: Driver, + recipientAddress: string, + amount: string, + gasfee: string, + totalfee: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise<void> => { + await sendTransaction(driver, recipientAddress, amount, gasfee, totalfee); + if (!isSyncFlow) { + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + ); + } }; diff --git a/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts index 68febce34b6b..76016a9c370f 100644 --- a/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts +++ b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts @@ -17,7 +17,7 @@ export async function installSnapSimpleKeyring( const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); await snapSimpleKeyringPage.check_pageIsLoaded(); await snapSimpleKeyringPage.installSnap(); - if (isSyncFlow) { + if (!isSyncFlow) { await snapSimpleKeyringPage.toggleUseSyncApproval(); } } diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 03bdeef1579d..7218c727a929 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -3,6 +3,9 @@ import { Driver } from '../../webdriver/driver'; class AccountListPage { private readonly driver: Driver; + private readonly accountListBalance = + '[data-testid="second-currency-display"]'; + private readonly accountListItem = '.multichain-account-menu-popover__list--menu-item'; @@ -156,6 +159,21 @@ class AccountListPage { await this.driver.clickElement(this.pinUnpinAccountButton); } + /** + * Checks that the account balance is displayed in the account list. + * + * @param expectedBalance - The expected balance to check. + */ + async check_accountBalanceDisplayed(expectedBalance: string): Promise<void> { + console.log( + `Check that account balance ${expectedBalance} is displayed in account list`, + ); + await this.driver.waitForSelector({ + css: this.accountListBalance, + text: expectedBalance, + }); + } + async check_accountDisplayedInAccountList( expectedLabel: string = 'Account', ): Promise<void> { diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 495f7ddbf3c8..8bd29ea8c602 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -3,26 +3,35 @@ import { Driver } from '../../webdriver/driver'; class HeaderNavbar { private driver: Driver; - private accountMenuButton: string; + private readonly accountMenuButton = '[data-testid="account-menu-icon"]'; - private accountOptionMenu: string; + private readonly accountOptionMenu = + '[data-testid="account-options-menu-button"]'; - private lockMetaMaskButton: string; + private readonly accountSnapButton = { text: 'Snaps', tag: 'div' }; - private mmiPortfolioButton: string; + private readonly lockMetaMaskButton = '[data-testid="global-menu-lock"]'; - private settingsButton: string; + private readonly mmiPortfolioButton = + '[data-testid="global-menu-mmi-portfolio"]'; - private accountSnapButton: object; + private readonly settingsButton = '[data-testid="global-menu-settings"]'; constructor(driver: Driver) { this.driver = driver; - this.accountMenuButton = '[data-testid="account-menu-icon"]'; - this.accountOptionMenu = '[data-testid="account-options-menu-button"]'; - this.lockMetaMaskButton = '[data-testid="global-menu-lock"]'; - this.mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; - this.settingsButton = '[data-testid="global-menu-settings"]'; - this.accountSnapButton = { text: 'Snaps', tag: 'div' }; + } + + async check_pageIsLoaded(): Promise<void> { + try { + await this.driver.waitForMultipleSelectors([ + this.accountMenuButton, + this.accountOptionMenu, + ]); + } catch (e) { + console.log('Timeout while waiting for header navbar to be loaded', e); + throw e; + } + console.log('Header navbar is loaded'); } async lockMetaMask(): Promise<void> { diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 23c050f49526..326ecc3188b7 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -6,36 +6,35 @@ import HeaderNavbar from './header-navbar'; class HomePage { private driver: Driver; - private sendButton: string; + public headerNavbar: HeaderNavbar; - private activityTab: string; + private readonly activityTab = + '[data-testid="account-overview__activity-tab"]'; - private tokensTab: string; + private readonly balance = '[data-testid="eth-overview__primary-currency"]'; - private balance: string; + private readonly completedTransactions = '[data-testid="activity-list-item"]'; - private completedTransactions: string; + private readonly confirmedTransactions = { + text: 'Confirmed', + css: '.transaction-status-label--confirmed', + }; - private confirmedTransactions: object; + private readonly failedTransactions = { + text: 'Failed', + css: '.transaction-status-label--failed', + }; - private transactionAmountsInActivity: string; + private readonly sendButton = '[data-testid="eth-overview-send"]'; - public headerNavbar: HeaderNavbar; + private readonly tokensTab = '[data-testid="account-overview__asset-tab"]'; + + private readonly transactionAmountsInActivity = + '[data-testid="transaction-list-item-primary-currency"]'; constructor(driver: Driver) { this.driver = driver; this.headerNavbar = new HeaderNavbar(driver); - this.sendButton = '[data-testid="eth-overview-send"]'; - this.activityTab = '[data-testid="account-overview__activity-tab"]'; - this.tokensTab = '[data-testid="account-overview__asset-tab"]'; - this.confirmedTransactions = { - text: 'Confirmed', - css: '.transaction-status-label--confirmed', - }; - this.balance = '[data-testid="eth-overview__primary-currency"]'; - this.completedTransactions = '[data-testid="activity-list-item"]'; - this.transactionAmountsInActivity = - '[data-testid="transaction-list-item-primary-currency"]'; } async check_pageIsLoaded(): Promise<void> { @@ -134,6 +133,28 @@ class HomePage { ); } + /** + * This function checks if the specified number of failed transactions are displayed in the activity list on homepage. + * It waits up to 10 seconds for the expected number of failed transactions to be visible. + * + * @param expectedNumber - The number of failed transactions expected to be displayed in activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of failed transactions is displayed within the timeout period. + */ + async check_failedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise<void> { + console.log( + `Wait for ${expectedNumber} failed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const failedTxs = await this.driver.findElements(this.failedTransactions); + return failedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} failed transactions found in activity list on homepage`, + ); + } + async check_ganacheBalanceIsDisplayed( ganacheServer?: Ganache, address = null, 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 fd4ae9d1ecc1..7f7a97d7d861 100644 --- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -19,6 +19,18 @@ class SnapSimpleKeyringPage { tag: 'h3', }; + private readonly approveRequestButton = { + text: 'Approve Request', + tag: 'button', + }; + + private readonly approveRequestIdInput = '#approve-request-request-id'; + + private readonly approveRequestSection = { + text: 'Approve request', + tag: 'div', + }; + private readonly cancelAddAccountWithNameButton = '[data-testid="cancel-add-account-with-name"]'; @@ -65,16 +77,55 @@ class SnapSimpleKeyringPage { tag: 'p', }; + private readonly importAccountButton = { + text: 'Import Account', + tag: 'button', + }; + + private readonly importAccountPrivateKeyInput = '#import-account-private-key'; + + private readonly importAccountSection = { + text: 'Import account', + tag: 'div', + }; + private readonly installationCompleteMessage = { text: 'Installation complete', tag: 'h2', }; + private readonly listRequestsButton = { + text: 'List Requests', + tag: 'button', + }; + + private readonly listRequestsSection = { + text: 'List requests', + tag: 'div', + }; + private readonly pageTitle = { text: 'Snap Simple Keyring', tag: 'p', }; + private readonly rejectRequestButton = { + text: 'Reject Request', + tag: 'button', + }; + + private readonly rejectRequestIdInput = '#reject-request-request-id'; + + private readonly rejectRequestSection = { + text: 'Reject request', + tag: 'div', + }; + + private readonly requestMessage = { + text: '"scope":', + tag: 'div', + }; + private readonly snapConnectedMessage = '#snapConnected'; private readonly snapInstallScrollButton = @@ -106,6 +157,57 @@ class SnapSimpleKeyringPage { console.log('Snap Simple Keyring page is loaded'); } + /** + * 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. + */ + async approveRejectSnapAccountTransaction( + approveTransaction: boolean = true, + ): Promise<void> { + console.log( + 'Approve/Reject snap account transaction on Snap Simple Keyring page', + ); + await this.driver.clickElementAndWaitToDisappear( + this.confirmationSubmitButton, + ); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + + // Get the first request from the requests list on simple keyring snap page + await this.driver.clickElementUsingMouseMove(this.listRequestsSection); + await this.driver.clickElement(this.listRequestsButton); + const requestJSON = await ( + await this.driver.waitForSelector(this.requestMessage) + ).getText(); + + if (approveTransaction) { + console.log( + 'Approve snap account transaction on Snap Simple Keyring page', + ); + await this.driver.clickElementUsingMouseMove(this.approveRequestSection); + await this.driver.fill( + this.approveRequestIdInput, + JSON.parse(requestJSON)[0].id, + ); + await this.driver.clickElement(this.approveRequestButton); + } else { + console.log( + 'Reject snap account transaction on Snap Simple Keyring page', + ); + await this.driver.clickElementUsingMouseMove(this.rejectRequestSection); + await this.driver.fill( + this.rejectRequestIdInput, + JSON.parse(requestJSON)[0].id, + ); + await this.driver.clickElement(this.rejectRequestButton); + } + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + } + async cancelCreateSnapOnConfirmationScreen(): Promise<void> { console.log('Cancel create snap on confirmation screen'); await this.driver.clickElementAndWaitForWindowToClose( @@ -120,6 +222,29 @@ class SnapSimpleKeyringPage { ); } + /** + * Confirms the add account dialog on Snap Simple Keyring page. + * + * @param accountName - Optional: name for the snap account. Defaults to "SSK Account". + */ + async confirmAddAccountDialog( + accountName: string = 'SSK Account', + ): Promise<void> { + console.log('Confirm add account dialog'); + await this.driver.waitForSelector(this.createSnapAccountName); + await this.driver.fill(this.createSnapAccountName, accountName); + await this.driver.clickElement(this.submitAddAccountWithNameButton); + + await this.driver.waitForSelector(this.accountCreatedMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await this.check_accountSupportedMethodsDisplayed(); + } + async confirmCreateSnapOnConfirmationScreen(): Promise<void> { console.log('Confirm create snap on confirmation screen'); await this.driver.clickElement(this.confirmationSubmitButton); @@ -138,19 +263,22 @@ class SnapSimpleKeyringPage { console.log('Create new account on Snap Simple Keyring page'); await this.openCreateSnapAccountConfirmationScreen(isFirstAccount); await this.confirmCreateSnapOnConfirmationScreen(); + await this.confirmAddAccountDialog(accountName); + } - await this.driver.waitForSelector(this.createSnapAccountName); - await this.driver.fill(this.createSnapAccountName, accountName); - await this.driver.clickElement(this.submitAddAccountWithNameButton); - - await this.driver.waitForSelector(this.accountCreatedMessage); - await this.driver.clickElementAndWaitForWindowToClose( - this.confirmationSubmitButton, - ); - await this.driver.switchToWindowWithTitle( - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - await this.check_accountSupportedMethodsDisplayed(); + /** + * Imports an account with a private key on Snap Simple Keyring page. + * + * @param privateKey - The private key to import. + */ + async importAccountWithPrivateKey(privateKey: string): Promise<void> { + console.log('Import account with private key on Snap Simple Keyring page'); + await this.driver.clickElement(this.importAccountSection); + await this.driver.fill(this.importAccountPrivateKeyInput, privateKey); + await this.driver.clickElement(this.importAccountButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.confirmCreateSnapOnConfirmationScreen(); + await this.confirmAddAccountDialog(); } /** diff --git a/test/e2e/tests/account/snap-account-transfers.spec.ts b/test/e2e/tests/account/snap-account-transfers.spec.ts new file mode 100644 index 000000000000..23cc5d510eb2 --- /dev/null +++ b/test/e2e/tests/account/snap-account-transfers.spec.ts @@ -0,0 +1,165 @@ +import { Suite } from 'mocha'; +import { + multipleGanacheOptions, + PRIVATE_KEY_TWO, + WINDOW_TITLES, + withFixtures, +} from '../../helpers'; +import { DEFAULT_FIXTURE_ACCOUNT } from '../../constants'; +import { Driver } from '../../webdriver/driver'; +import { Ganache } from '../../seeder/ganache'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import FixtureBuilder from '../../fixture-builder'; +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 { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { sendTransactionWithSnapAccount } from '../../page-objects/flows/send-transaction.flow'; + +describe('Snap Account Transfers @no-mmi', function (this: Suite) { + it('can import a private key and transfer 1 ETH (sync flow)', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: multipleGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + 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'); + + // send 1 ETH from snap account to account 1 + await sendTransactionWithSnapAccount( + driver, + DEFAULT_FIXTURE_ACCOUNT, + '1', + '0.000042', + '1.000042', + ); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.openAccountMenu(); + const accountList = new AccountListPage(driver); + await accountList.check_pageIsLoaded(); + + // check the balance of the 2 accounts are updated + await accountList.check_accountBalanceDisplayed('26'); + await accountList.check_accountBalanceDisplayed('24'); + }, + ); + }); + + it('can import a private key and transfer 1 ETH (async flow approve)', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: multipleGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver, false); + 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'); + + // send 1 ETH from snap account to account 1 and approve the transaction + await sendTransactionWithSnapAccount( + driver, + DEFAULT_FIXTURE_ACCOUNT, + '1', + '0.000042', + '1.000042', + false, + ); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.openAccountMenu(); + const accountList = new AccountListPage(driver); + await accountList.check_pageIsLoaded(); + + // check the balance of the 2 accounts are updated + await accountList.check_accountBalanceDisplayed('26'); + await accountList.check_accountBalanceDisplayed('24'); + }, + ); + }); + + it('can import a private key and transfer 1 ETH (async flow reject)', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: multipleGanacheOptions, + title: this.test?.fullTitle(), + ignoredConsoleErrors: ['Request rejected by user or snap.'], + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver, false); + 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'); + + // send 1 ETH from snap account to account 1 and reject the transaction + await sendTransactionWithSnapAccount( + driver, + DEFAULT_FIXTURE_ACCOUNT, + '1', + '0.000042', + '1.000042', + false, + false, + ); + + // check the transaction is failed in MetaMask activity list + const homepage = new HomePage(driver); + await homepage.check_pageIsLoaded(); + await homepage.check_failedTxNumberDisplayedInActivity(); + }, + ); + }); +}); diff --git a/test/e2e/tests/transaction/simple-send.spec.ts b/test/e2e/tests/transaction/simple-send.spec.ts index 43b096bf4ec8..0615a0e21d74 100644 --- a/test/e2e/tests/transaction/simple-send.spec.ts +++ b/test/e2e/tests/transaction/simple-send.spec.ts @@ -31,6 +31,7 @@ describe('Simple send eth', function (this: Suite) { '1.000042', ); const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); await homePage.check_confirmedTxNumberDisplayedInActivity(); await homePage.check_txAmountInActivity(); }, From 39e0251b9f76cab4700124422c49f185c93982ee Mon Sep 17 00:00:00 2001 From: Victor Thomas <10986371+vthomas13@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:49:09 -0400 Subject: [PATCH 122/226] chore: update Trezor Connect to v9.4.0, remove workarounds (#27112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR includes changes from PR #26749 by @martykan. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** > With MV3, MetaMask started to use Trezor Connect inside an offscreen environment, in a way that was previously unsupported and required a workaround by patching the Trezor Connect library. > > In recent versions of Trezor Connect, the library can handle working in an offscreen environment correctly, without the need for the workaround, which could cause issues with compatibility. > > This PR removes the patch and updates the Trezor Connect library to the latest version (v9.4.0). > The change in manifest.json is due to new URL parameters, Firefox needs the asterisk at the end to match the URL with them. > I haven't removed the WebUSB device request which was added on MetaMask's side in relation to the workaround, since it can improve UX of the pairing process, but it could be removed if desired. > > The dependency update affects Lavamoat, I am including the policy changes in my commit, however let me know if you would like me to remove them and handle them using your own process. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27112?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** > 1. Open accounts dropdown, "Add hardware wallet" > 2. Select Trezor > 3. Follow prompts to connect the device > 4. See a list of accounts from the Trezor ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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 - [ ] 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. --- ...zor-connect-web-npm-9.3.0-040ab10d9a.patch | 57 -- app/manifest/v2/_base.json | 2 +- app/manifest/v3/_base.json | 2 +- lavamoat/build-system/policy.json | 73 ++- offscreen/scripts/trezor.ts | 2 +- package.json | 4 +- yarn.lock | 505 +++++++++--------- 7 files changed, 291 insertions(+), 354 deletions(-) delete mode 100644 .yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch diff --git a/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch b/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch deleted file mode 100644 index 515ff415b4df..000000000000 --- a/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch +++ /dev/null @@ -1,57 +0,0 @@ -diff --git a/lib/impl/core-in-iframe.js b/lib/impl/core-in-iframe.js -index c47cf3bff860d6b1855341c00b80fc6c40f9d6d5..0151bcaac6689ecb26f1b4575ece4f3760ca1b87 100644 ---- a/lib/impl/core-in-iframe.js -+++ b/lib/impl/core-in-iframe.js -@@ -116,7 +116,9 @@ class CoreInIframe { - this._log.enabled = !!this._settings.debug; - window.addEventListener('message', this.boundHandleMessage); - window.addEventListener('unload', this.boundDispose); -- await iframe.init(this._settings); -+ var modifiedSettings = Object.assign({}, this._settings); -+ modifiedSettings.env = 'webextension'; -+ await iframe.init(modifiedSettings); - if (this._settings.sharedLogger !== false) { - iframe.initIframeLogger(); - } -@@ -132,7 +134,9 @@ class CoreInIframe { - } - this._popupManager.request(); - try { -- await this.init(this._settings); -+ var modifiedSettings = Object.assign({}, this._settings); -+ modifiedSettings.env = 'webextension'; -+ await this.init(modifiedSettings); - } - catch (error) { - if (this._popupManager) { -diff --git a/lib/popup/index.js b/lib/popup/index.js -index 9b13c370a5ac8b4e4fc0315ed40cdf615d0bb0cb..595a7d9e1aa397b3aa53ba5d75e4ccf22a61bcf1 100644 ---- a/lib/popup/index.js -+++ b/lib/popup/index.js -@@ -229,10 +229,12 @@ class PopupManager extends events_1.default { - } - else if (message.type === events_2.POPUP.LOADED) { - this.handleMessage(message); -+ var modifiedSettings = Object.assign({}, this.settings); -+ modifiedSettings.env = 'webextension'; - this.channel.postMessage({ - type: events_2.POPUP.INIT, - payload: { -- settings: this.settings, -+ settings: modifiedSettings, - useCore: true, - }, - }); -@@ -292,9 +294,11 @@ class PopupManager extends events_1.default { - this.popupPromise = undefined; - } - (_b = this.iframeHandshakePromise) === null || _b === void 0 ? void 0 : _b.promise.then(payload => { -+ var modifiedSettings = Object.assign({}, this.settings); -+ modifiedSettings.env = 'webextension'; - this.channel.postMessage({ - type: events_2.POPUP.INIT, -- payload: Object.assign(Object.assign({}, payload), { settings: this.settings }), -+ payload: Object.assign(Object.assign({}, payload), { settings: modifiedSettings }), - }); - }); - } diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json index 31b1c82224fd..f29b7458a9e5 100644 --- a/app/manifest/v2/_base.json +++ b/app/manifest/v2/_base.json @@ -42,7 +42,7 @@ "all_frames": true }, { - "matches": ["*://connect.trezor.io/*/popup.html"], + "matches": ["*://connect.trezor.io/*/popup.html*"], "js": ["vendor/trezor/content-script.js"] } ], diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json index 71d083208b55..4d6ee38437d3 100644 --- a/app/manifest/v3/_base.json +++ b/app/manifest/v3/_base.json @@ -40,7 +40,7 @@ "all_frames": true }, { - "matches": ["*://connect.trezor.io/*/popup.html"], + "matches": ["*://connect.trezor.io/*/popup.html*"], "js": ["vendor/trezor/content-script.js"] } ], diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 9bb4675d14c9..6e3b319da1e8 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -160,8 +160,7 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-module-imports": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true, + "depcheck>@babel/traverse": true, "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true } }, @@ -478,7 +477,7 @@ "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-syntax-async-generators": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator": { @@ -494,14 +493,14 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": { "packages": { "@babel/core>@babel/template": true, "@babel/core>@babel/types": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true } }, "@babel/preset-env>@babel/plugin-transform-block-scoped-functions": { @@ -535,12 +534,12 @@ "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-optimise-call-expression": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": true, - "@babel/preset-env>@babel/plugin-transform-classes>globals": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": true, + "@babel/preset-env>@babel/plugin-transform-classes>globals": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": { @@ -548,6 +547,12 @@ "@babel/core>@babel/types": true } }, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": { + "packages": { + "@babel/core>@babel/template": true, + "@babel/core>@babel/types": true + } + }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-optimise-call-expression": { "packages": { "@babel/core>@babel/types": true @@ -558,7 +563,7 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-optimise-call-expression": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": { @@ -566,6 +571,11 @@ "@babel/core>@babel/types": true } }, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": { + "packages": { + "@babel/core>@babel/types": true + } + }, "@babel/preset-env>@babel/plugin-transform-computed-properties": { "packages": { "@babel/core": true, @@ -673,7 +683,7 @@ "packages": { "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true } }, "@babel/preset-env>@babel/plugin-transform-json-strings": { @@ -716,10 +726,15 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": true, + "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": true, "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true } }, + "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": { + "packages": { + "@babel/core>@babel/types": true + } + }, "@babel/preset-env>@babel/plugin-transform-modules-umd": { "builtin": { "path.basename": true, @@ -811,9 +826,7 @@ "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin>semver": true, "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin>semver": { @@ -1042,6 +1055,7 @@ "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin": true, + "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true, "@babel/preset-typescript>@babel/plugin-transform-typescript>@babel/plugin-syntax-typescript": true } }, @@ -2346,32 +2360,13 @@ "@babel/code-frame": true, "@babel/core>@babel/generator": true, "@babel/core>@babel/parser": true, + "@babel/core>@babel/template": true, "@babel/core>@babel/types": true, "babel/preset-env>b@babel/types": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true, "depcheck>@babel/traverse>globals": true, "nock>debug": true } }, - "depcheck>@babel/traverse>@babel/helper-function-name": { - "packages": { - "@babel/core>@babel/template": true, - "@babel/core>@babel/types": true - } - }, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": { - "packages": { - "@babel/core>@babel/types": true - } - }, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": { - "packages": { - "@babel/core>@babel/types": true - } - }, "depcheck>cosmiconfig>parse-json": { "packages": { "@babel/code-frame": true, @@ -6333,10 +6328,10 @@ "packages": { "@babel/code-frame": true, "@babel/core>@babel/generator": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": true, + "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": true, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>@babel/parser": true, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>@babel/types": true, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>globals": true, diff --git a/offscreen/scripts/trezor.ts b/offscreen/scripts/trezor.ts index e9482a03c575..a6c1b5b2788e 100644 --- a/offscreen/scripts/trezor.ts +++ b/offscreen/scripts/trezor.ts @@ -47,7 +47,7 @@ export default function init() { TrezorConnectSDK.init({ ...msg.params, - env: 'web', + env: 'webextension', }).then(() => { sendResponse(); }); diff --git a/package.json b/package.json index fad9fae96418..201f76f3c473 100644 --- a/package.json +++ b/package.json @@ -253,7 +253,6 @@ "@babel/runtime@npm:^7.8.4": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@spruceid/siwe-parser@npm:1.1.3": "patch:@spruceid/siwe-parser@npm%3A2.1.0#~/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch", "@spruceid/siwe-parser@npm:2.1.0": "patch:@spruceid/siwe-parser@npm%3A2.1.0#~/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch", - "@trezor/connect-web@npm:^9.2.2": "patch:@trezor/connect-web@npm%3A9.2.2#~/.yarn/patches/@trezor-connect-web-npm-9.2.2-a4de8e45fc.patch", "ts-mixer@npm:^6.0.3": "patch:ts-mixer@npm%3A6.0.4#~/.yarn/patches/ts-mixer-npm-6.0.4-5d9747bdf5.patch", "sucrase@npm:3.34.0": "^3.35.0", "@expo/config/glob": "^10.3.10", @@ -262,7 +261,6 @@ "@metamask/message-manager": "^10.1.0", "@metamask/gas-fee-controller@npm:^15.1.1": "patch:@metamask/gas-fee-controller@npm%3A15.1.2#~/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch", "@metamask/nonce-tracker@npm:^5.0.0": "patch:@metamask/nonce-tracker@npm%3A5.0.0#~/.yarn/patches/@metamask-nonce-tracker-npm-5.0.0-d81478218e.patch", - "@trezor/connect-web@npm:^9.1.11": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", "@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", @@ -371,7 +369,7 @@ "@sentry/types": "^8.33.1", "@sentry/utils": "^8.33.1", "@swc/core": "1.4.11", - "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", + "@trezor/connect-web": "^9.4.0", "@zxing/browser": "^0.1.4", "@zxing/library": "0.20.0", "await-semaphore": "^0.1.1", diff --git a/yarn.lock b/yarn.lock index f2992051fdf0..a003e9a42cd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -177,24 +177,24 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.24.8, @babel/generator@npm:^7.7.2": - version: 7.24.10 - resolution: "@babel/generator@npm:7.24.10" +"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.25.4, @babel/generator@npm:^7.7.2": + version: 7.25.5 + resolution: "@babel/generator@npm:7.25.5" dependencies: - "@babel/types": "npm:^7.24.9" + "@babel/types": "npm:^7.25.4" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 10/c2491fb7d985527a165546cbcf9e5f6a2518f2a968c7564409c012acce1019056b21e67a152af89b3f4d4a295ca2e75a1a16858152f750efbc4b5087f0cb7253 + checksum: 10/e6d046afe739cfa706c40c127b7436731acb2a3146d408a7d89dbf16448491b35bc09b7d285cc19c2c1f8980d74b5a99df200d67c859bb5260986614685b0770 languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" +"@babel/helper-annotate-as-pure@npm:^7.22.5, @babel/helper-annotate-as-pure@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/53da330f1835c46f26b7bf4da31f7a496dee9fd8696cca12366b94ba19d97421ce519a74a837f687749318f94d1a37f8d1abcbf35e8ed22c32d16373b2f6198d + "@babel/types": "npm:^7.24.7" + checksum: 10/a9017bfc1c4e9f2225b967fbf818004703de7cf29686468b54002ffe8d6b56e0808afa20d636819fcf3a34b89ba72f52c11bdf1d69f303928ee10d92752cad95 languageName: node linkType: hard @@ -220,22 +220,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.5, @babel/helper-create-class-features-plugin@npm:^7.24.5": - version: 7.24.5 - resolution: "@babel/helper-create-class-features-plugin@npm:7.24.5" +"@babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.5, @babel/helper-create-class-features-plugin@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/helper-create-class-features-plugin@npm:7.25.4" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-member-expression-to-functions": "npm:^7.24.5" - "@babel/helper-optimise-call-expression": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.24.1" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.24.5" + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.8" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/helper-replace-supers": "npm:^7.25.0" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.4" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/9f65cf44ff838dae2a51ba7fdca1a27cc6eb7c0589e2446e807f7e8dc18e9866775f6e7a209d4f1d25bfed265e450ea338ca6c3570bc11a77fbfe683694130f3 + checksum: 10/47218da9fd964af30d41f0635d9e33eed7518e03aa8f10c3eb8a563bb2c14f52be3e3199db5912ae0e26058c23bb511c811e565c55ecec09427b04b867ed13c2 languageName: node linkType: hard @@ -267,7 +265,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5, @babel/helper-environment-visitor@npm:^7.24.7": +"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5": version: 7.24.7 resolution: "@babel/helper-environment-visitor@npm:7.24.7" dependencies: @@ -276,7 +274,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0, @babel/helper-function-name@npm:^7.24.7": +"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0": version: 7.24.7 resolution: "@babel/helper-function-name@npm:7.24.7" dependencies: @@ -286,7 +284,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.22.5, @babel/helper-hoist-variables@npm:^7.24.7": +"@babel/helper-hoist-variables@npm:^7.22.5": version: 7.24.7 resolution: "@babel/helper-hoist-variables@npm:7.24.7" dependencies: @@ -295,7 +293,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.23.0, @babel/helper-member-expression-to-functions@npm:^7.24.5": +"@babel/helper-member-expression-to-functions@npm:^7.24.8": version: 7.24.8 resolution: "@babel/helper-member-expression-to-functions@npm:7.24.8" dependencies: @@ -305,31 +303,31 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.24.3": - version: 7.24.3 - resolution: "@babel/helper-module-imports@npm:7.24.3" +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-module-imports@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.24.0" - checksum: 10/42fe124130b78eeb4bb6af8c094aa749712be0f4606f46716ce74bc18a5ea91c918c547c8bb2307a2e4b33f163e4ad2cb6a7b45f80448e624eae45b597ea3499 + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/df8bfb2bb18413aa151ecd63b7d5deb0eec102f924f9de6bc08022ced7ed8ca7fed914562d2f6fa5b59b74a5d6e255dc35612b2bc3b8abf361e13f61b3704770 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.23.0, @babel/helper-module-transforms@npm:^7.23.3": - version: 7.24.5 - resolution: "@babel/helper-module-transforms@npm:7.24.5" +"@babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.23.0, @babel/helper-module-transforms@npm:^7.24.8": + version: 7.25.2 + resolution: "@babel/helper-module-transforms@npm:7.25.2" dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-module-imports": "npm:^7.24.3" - "@babel/helper-simple-access": "npm:^7.24.5" - "@babel/helper-split-export-declaration": "npm:^7.24.5" - "@babel/helper-validator-identifier": "npm:^7.24.5" + "@babel/helper-module-imports": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.2" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/1a91e8abc2f427f8273ce3b99ef7b9c013eb3628221428553e0d4bc9c6db2e73bc4fc1b8535bd258544936accab9380e0d095f2449f913cad650ddee744b2124 + checksum: 10/a3bcf7815f3e9d8b205e0af4a8d92603d685868e45d119b621357e274996bf916216bb95ab5c6a60fde3775b91941555bf129d608e3d025b04f8aac84589f300 languageName: node linkType: hard -"@babel/helper-optimise-call-expression@npm:^7.22.5": +"@babel/helper-optimise-call-expression@npm:^7.22.5, @babel/helper-optimise-call-expression@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-optimise-call-expression@npm:7.24.7" dependencies: @@ -338,10 +336,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.24.5 - resolution: "@babel/helper-plugin-utils@npm:7.24.5" - checksum: 10/6e11ca5da73e6bd366848236568c311ac10e433fc2034a6fe6243af28419b07c93b4386f87bbc940aa058b7c83f370ef58f3b0fd598106be040d21a3d1c14276 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.24.8, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": + version: 7.24.8 + resolution: "@babel/helper-plugin-utils@npm:7.24.8" + checksum: 10/adbc9fc1142800a35a5eb0793296924ee8057fe35c61657774208670468a9fbfbb216f2d0bc46c680c5fefa785e5ff917cc1674b10bd75cdf9a6aa3444780630 languageName: node linkType: hard @@ -358,38 +356,40 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9, @babel/helper-replace-supers@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/helper-replace-supers@npm:7.24.1" +"@babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9, @babel/helper-replace-supers@npm:^7.25.0": + version: 7.25.0 + resolution: "@babel/helper-replace-supers@npm:7.25.0" dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-member-expression-to-functions": "npm:^7.23.0" - "@babel/helper-optimise-call-expression": "npm:^7.22.5" + "@babel/helper-member-expression-to-functions": "npm:^7.24.8" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.0" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/1103b28ce0cc7fba903c21bc78035c696ff191bdbbe83c20c37030a2e10ae6254924556d942cdf8c44c48ba606a8266fdb105e6bb10945de9285f79cb1905df1 + checksum: 10/97c6c17780cb9692132f7243f5a21fb6420104cb8ff8752dc03cfc9a1912a243994c0290c77ff096637ab6f2a7363b63811cfc68c2bad44e6b39460ac2f6a63f languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.22.5, @babel/helper-simple-access@npm:^7.24.5": - version: 7.24.5 - resolution: "@babel/helper-simple-access@npm:7.24.5" +"@babel/helper-simple-access@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-simple-access@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.24.5" - checksum: 10/db8768a16592faa1bde9061cac3d903bdbb2ddb2a7e9fb73c5904daee1f1b1dc69ba4d249dc22c45885c0d4b54fd0356ee78e6d67a9a90330c7dd37e6cd3acff + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/5083e190186028e48fc358a192e4b93ab320bd016103caffcfda81302a13300ccce46c9cd255ae520c25d2a6a9b47671f93e5fe5678954a2329dc0a685465c49 languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5" +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5, @babel/helper-skip-transparent-expression-wrappers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/1012ef2295eb12dc073f2b9edf3425661e9b8432a3387e62a8bc27c42963f1f216ab3124228015c748770b2257b4f1fda882ca8fa34c0bf485e929ae5bc45244 + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/784a6fdd251a9a7e42ccd04aca087ecdab83eddc60fda76a2950e00eb239cc937d3c914266f0cc476298b52ac3f44ffd04c358e808bd17552a7e008d75494a77 languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.6, @babel/helper-split-export-declaration@npm:^7.24.5, @babel/helper-split-export-declaration@npm:^7.24.7": +"@babel/helper-split-export-declaration@npm:^7.22.6": version: 7.24.7 resolution: "@babel/helper-split-export-declaration@npm:7.24.7" dependencies: @@ -405,17 +405,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.24.5, @babel/helper-validator-identifier@npm:^7.24.7": +"@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-identifier@npm:7.24.7" checksum: 10/86875063f57361471b531dbc2ea10bbf5406e12b06d249b03827d361db4cad2388c6f00936bcd9dc86479f7e2c69ea21412c2228d4b3672588b754b70a449d4b languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/helper-validator-option@npm:7.23.5" - checksum: 10/537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e +"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5, @babel/helper-validator-option@npm:^7.24.7": + version: 7.24.8 + resolution: "@babel/helper-validator-option@npm:7.24.8" + checksum: 10/a52442dfa74be6719c0608fee3225bd0493c4057459f3014681ea1a4643cd38b68ff477fe867c4b356da7330d085f247f0724d300582fa4ab9a02efaf34d107c languageName: node linkType: hard @@ -482,12 +482,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.0, @babel/parser@npm:^7.13.9, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/parser@npm:7.24.8" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.0, @babel/parser@npm:^7.13.9, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.4": + version: 7.25.4 + resolution: "@babel/parser@npm:7.25.4" + dependencies: + "@babel/types": "npm:^7.25.4" bin: parser: ./bin/babel-parser.js - checksum: 10/e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef + checksum: 10/343b8a76c43549e370fe96f4f6d564382a6cdff60e9c3b8a594c51e4cefd58ec9945e82e8c4dfbf15ac865a04e4b29806531440760748e28568e6aec21bc9cb5 languageName: node linkType: hard @@ -645,14 +647,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.24.1, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.24.1 - resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.24.7, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.24.7 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.7" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-plugin-utils": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/712f7e7918cb679f106769f57cfab0bc99b311032665c428b98f4c3e2e6d567601d45386a4f246df6a80d741e1f94192b3f008800d66c4f1daae3ad825c243f0 + checksum: 10/a93516ae5b34868ab892a95315027d4e5e38e8bd1cfca6158f2974b0901cbb32bbe64ea10ad5b25f919ddc40c6d8113c4823372909c9c9922170c12b0b1acecb languageName: node linkType: hard @@ -744,14 +746,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.24.1, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.24.1 - resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" +"@babel/plugin-syntax-typescript@npm:^7.24.7, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.25.4 + resolution: "@babel/plugin-syntax-typescript@npm:7.25.4" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-plugin-utils": "npm:^7.24.8" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/bf4bd70788d5456b5f75572e47a2e31435c7c4e43609bd4dffd2cc0c7a6cf90aabcf6cd389e351854de9a64412a07d30effef5373251fe8f6a4c9db0c0163bda + checksum: 10/0771b45a35fd536cd3b3a48e5eda0f53e2d4f4a0ca07377cc247efa39eaf6002ed1c478106aad2650e54aefaebcb4f34f3284c4ae9252695dbd944bf66addfb0 languageName: node linkType: hard @@ -1047,16 +1049,16 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" +"@babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.7": + version: 7.24.8 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.8" dependencies: - "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.24.0" - "@babel/helper-simple-access": "npm:^7.22.5" + "@babel/helper-module-transforms": "npm:^7.24.8" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-simple-access": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7326a62ed5f766f93ee75684868635b59884e2801533207ea11561c296de53037949fecad4055d828fa7ebeb6cc9e55908aa3e7c13f930ded3e62ad9f24680d7 + checksum: 10/18e5d229767c7b5b6ff0cbf1a8d2d555965b90201839d0ac2dc043b56857624ea344e59f733f028142a8c1d54923b82e2a0185694ef36f988d797bfbaf59819c languageName: node linkType: hard @@ -1361,17 +1363,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.24.1": - version: 7.24.5 - resolution: "@babel/plugin-transform-typescript@npm:7.24.5" +"@babel/plugin-transform-typescript@npm:^7.24.7": + version: 7.25.2 + resolution: "@babel/plugin-transform-typescript@npm:7.25.2" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-create-class-features-plugin": "npm:^7.24.5" - "@babel/helper-plugin-utils": "npm:^7.24.5" - "@babel/plugin-syntax-typescript": "npm:^7.24.1" + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-create-class-features-plugin": "npm:^7.25.0" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/plugin-syntax-typescript": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/3d35accd6d7ae075509e01ce2cc3921ef3b44159b8ec15dd6201050c56dab4cfe14c5c0538e26e3beffb14c33731527041b60444cfba1ceae740f0748caf0aa0 + checksum: 10/50e017ffd131c08661daa22b6c759999bb7a6cdfbf683291ee4bcbea4ae839440b553d2f8896bcf049aca1d267b39f3b09e8336059e919e83149b5ad859671f6 languageName: node linkType: hard @@ -1554,18 +1557,18 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.23.2, @babel/preset-typescript@npm:^7.23.3": - version: 7.24.1 - resolution: "@babel/preset-typescript@npm:7.24.1" +"@babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.23.2, @babel/preset-typescript@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/preset-typescript@npm:7.24.7" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" - "@babel/helper-validator-option": "npm:^7.23.5" - "@babel/plugin-syntax-jsx": "npm:^7.24.1" - "@babel/plugin-transform-modules-commonjs": "npm:^7.24.1" - "@babel/plugin-transform-typescript": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/helper-validator-option": "npm:^7.24.7" + "@babel/plugin-syntax-jsx": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/ba774bd427c9f376769ddbc2723f5801a6b30113a7c3aaa14c36215508e347a527fdae98cfc294f0ecb283d800ee0c1f74e66e38e84c9bc9ed2fe6ed50dcfaf8 + checksum: 10/995e9783f8e474581e7533d6b10ec1fbea69528cc939ad8582b5937e13548e5215d25a8e2c845e7b351fdaa13139896b5e42ab3bde83918ea4e41773f10861ac languageName: node linkType: hard @@ -1610,12 +1613,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/runtime@npm:7.24.8" +"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/runtime@npm:7.25.4" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10/e6f335e472a8a337379effc15815dd0eddf6a7d0c00b50deb4f9e9585819b45431d0ff3c2d3d0fa58c227a9b04dcc4a85e7245fb57493adb2863b5208c769cbd + checksum: 10/70d2a420c24a3289ea6c4addaf3a1c4186bc3d001c92445faa3cd7601d7d2fbdb32c63b3a26b9771e20ff2f511fa76b726bf256f823cdb95bc37b8eadbd02f70 languageName: node linkType: hard @@ -1628,14 +1631,14 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.3.3": - version: 7.24.7 - resolution: "@babel/template@npm:7.24.7" +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3": + version: 7.25.0 + resolution: "@babel/template@npm:7.25.0" dependencies: "@babel/code-frame": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10/5975d404ef51cf379515eb0f80b115981d0b9dff5539e53a47516644abb8c83d7559f5b083eb1d4977b20d8359ebb2f911ccd4f729143f8958fdc465f976d843 + "@babel/parser": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" + checksum: 10/07ebecf6db8b28244b7397628e09c99e7a317b959b926d90455c7253c88df3677a5a32d1501d9749fe292a263ff51a4b6b5385bcabd5dadd3a48036f4d4949e0 languageName: node linkType: hard @@ -1657,21 +1660,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.12.5, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/traverse@npm:7.24.8" +"@babel/traverse@npm:^7.12.5, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.2, @babel/traverse@npm:^7.25.4": + version: 7.25.4 + resolution: "@babel/traverse@npm:7.25.4" dependencies: "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.24.8" - "@babel/helper-environment-visitor": "npm:^7.24.7" - "@babel/helper-function-name": "npm:^7.24.7" - "@babel/helper-hoist-variables": "npm:^7.24.7" - "@babel/helper-split-export-declaration": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.8" - "@babel/types": "npm:^7.24.8" + "@babel/generator": "npm:^7.25.4" + "@babel/parser": "npm:^7.25.4" + "@babel/template": "npm:^7.25.0" + "@babel/types": "npm:^7.25.4" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/47d8ecf8cfff58fe621fc4d8454b82c97c407816d8f9c435caa0c849ea7c357b91119a06f3c69f21a0228b5d06ac0b44f49d1f78cff032d6266317707f1fe615 + checksum: 10/a85c16047ab8e454e2e758c75c31994cec328bd6d8b4b22e915fa7393a03b3ab96d1218f43dc7ef77c957cc488dc38100bdf504d08a80a131e89b2e49cfa2be5 languageName: node linkType: hard @@ -1686,14 +1686,14 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.0, @babel/types@npm:^7.13.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.5, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.24.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.24.9 - resolution: "@babel/types@npm:7.24.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.0, @babel/types@npm:^7.13.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.25.4 + resolution: "@babel/types@npm:7.25.4" dependencies: "@babel/helper-string-parser": "npm:^7.24.8" "@babel/helper-validator-identifier": "npm:^7.24.7" to-fast-properties: "npm:^2.0.0" - checksum: 10/21873a08a124646824aa230de06af52149ab88206dca59849dcb3003990a6306ec2cdaa4147ec1127c0cfc5f133853cfc18f80d7f6337b6662a3c378ed565f15 + checksum: 10/d4a1194612d0a2a6ce9a0be325578b43d74e5f5278c67409468ba0a924341f0ad349ef0245ee8a36da3766efe5cc59cd6bb52547674150f97d8dc4c8cfa5d6b8 languageName: node linkType: hard @@ -2383,7 +2383,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/common@npm:^4.2.0, @ethereumjs/common@npm:^4.4.0": +"@ethereumjs/common@npm:^4.3.0, @ethereumjs/common@npm:^4.4.0": version: 4.4.0 resolution: "@ethereumjs/common@npm:4.4.0" dependencies: @@ -2441,7 +2441,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^5.1.0, @ethereumjs/tx@npm:^5.2.1": +"@ethereumjs/tx@npm:^5.1.0, @ethereumjs/tx@npm:^5.2.1, @ethereumjs/tx@npm:^5.3.0": version: 5.4.0 resolution: "@ethereumjs/tx@npm:5.4.0" dependencies: @@ -8116,11 +8116,11 @@ __metadata: languageName: node linkType: hard -"@solana/web3.js@npm:^1.90.2, @solana/web3.js@npm:^1.91.6": - version: 1.95.1 - resolution: "@solana/web3.js@npm:1.95.1" +"@solana/web3.js@npm:^1.95.0": + version: 1.95.3 + resolution: "@solana/web3.js@npm:1.95.3" dependencies: - "@babel/runtime": "npm:^7.24.8" + "@babel/runtime": "npm:^7.25.0" "@noble/curves": "npm:^1.4.2" "@noble/hashes": "npm:^1.4.0" "@solana/buffer-layout": "npm:^4.0.1" @@ -8135,7 +8135,7 @@ __metadata: node-fetch: "npm:^2.7.0" rpc-websockets: "npm:^9.0.2" superstruct: "npm:^2.0.2" - checksum: 10/6b9b00bba37cf8b1f5de9b1bc82efc2006eb2fa8fd5b90bee6f0ce174c0a1a41e97e5ee1db8391fc8a1d50b4610a77744cb3b1364584a3d65bc931a26d635193 + checksum: 10/25bdc5100faae6d3e48cbfac965b129060bec61669dcd75d0a525cea3ce8d23632ebea249a7b21616c89641bf7ea26d18826ce51246274b6aa1278d32180c870 languageName: node linkType: hard @@ -9590,87 +9590,87 @@ __metadata: languageName: node linkType: hard -"@trezor/analytics@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/analytics@npm:1.1.0" +"@trezor/analytics@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/analytics@npm:1.2.0" dependencies: - "@trezor/env-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/env-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/6a5b426c12b7ba7bfbbb955ac003733ca0b36a33f52d49c13a37ab341ae6f9c38a5aa0696f60dd31da650b01326a93d27d06ef830190a608159cc833451a413b + checksum: 10/652dea1b54515c10931fe67671a5043b22557629224da3ae8fff153a4a9af45eb27c7cc2cdef68e0dbfab53b7544df0dce1a903adf4e0c0c27531a6abc1d2a19 languageName: node linkType: hard -"@trezor/blockchain-link-types@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/blockchain-link-types@npm:1.1.0" +"@trezor/blockchain-link-types@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/blockchain-link-types@npm:1.2.0" dependencies: - "@solana/web3.js": "npm:^1.91.6" + "@solana/web3.js": "npm:^1.95.0" "@trezor/type-utils": "npm:1.1.0" - "@trezor/utxo-lib": "npm:2.1.0" + "@trezor/utxo-lib": "npm:2.2.0" socks-proxy-agent: "npm:6.1.1" peerDependencies: tslib: ^2.6.2 - checksum: 10/d7730bf1cc9e77293d5bf4dc7138d0719f0ae564273b51b1f142dc527269147e7701d8a20dc5f96326cb0a7d8294eb4394d6a0076ef692c78763bcb10633b62d + checksum: 10/3165250e4404ed8f4619662aa9a3aca0057da8867a8919a8b4a44b2643bda29661e65224946b3e5ab2c8e13677308f87dc0cdfaaa9324da886132fbe1899b841 languageName: node linkType: hard -"@trezor/blockchain-link-utils@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/blockchain-link-utils@npm:1.1.0" +"@trezor/blockchain-link-utils@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/blockchain-link-utils@npm:1.2.0" dependencies: "@mobily/ts-belt": "npm:^3.13.1" - "@solana/web3.js": "npm:^1.91.6" - "@trezor/env-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@solana/web3.js": "npm:^1.95.0" + "@trezor/env-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/b7866a4a59afc76b60dcd785ad9567cbfae4810a7510b09a7f1e2b0207834677216ce5b9c82c6f9a228cfb51e38ecf4f7e0f7501c67c5f083a4db06a8a9786e5 + checksum: 10/dacc6529ac568901269478484436b99da8e54dd111fc1663ff66a1b71e799d3663a21df1ce232acc11d4776eb9c77b976e52eafb3b496d941b5ad0996cc6b027 languageName: node linkType: hard -"@trezor/blockchain-link@npm:2.2.0": - version: 2.2.0 - resolution: "@trezor/blockchain-link@npm:2.2.0" +"@trezor/blockchain-link@npm:2.3.0": + version: 2.3.0 + resolution: "@trezor/blockchain-link@npm:2.3.0" dependencies: "@solana/buffer-layout": "npm:^4.0.1" - "@solana/web3.js": "npm:^1.90.2" - "@trezor/blockchain-link-types": "npm:1.1.0" - "@trezor/blockchain-link-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" - "@trezor/utxo-lib": "npm:2.1.0" + "@solana/web3.js": "npm:^1.95.0" + "@trezor/blockchain-link-types": "npm:1.2.0" + "@trezor/blockchain-link-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" + "@trezor/utxo-lib": "npm:2.2.0" "@types/web": "npm:^0.0.138" events: "npm:^3.3.0" ripple-lib: "npm:^1.10.1" socks-proxy-agent: "npm:6.1.1" - ws: "npm:^8.17.1" + ws: "npm:^8.18.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/3e0c5ddadb6d66f9c1b87ebecf9ac35729f1929c840bcab512de71cae04cf04d4a3562e6893443e57143adbf4a66de5780e29e95e969103a756bdb9454988313 + checksum: 10/46358539986f4804a2a9de51f01ca0cf8cf0183ec70bef2d2bce6d7baa813a9b6220657c39cbf5a0a1e5e27db0670f1cfbcaae8eb804bdd6d2327d6a798e7068 languageName: node linkType: hard -"@trezor/connect-analytics@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/connect-analytics@npm:1.1.0" +"@trezor/connect-analytics@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/connect-analytics@npm:1.2.0" dependencies: - "@trezor/analytics": "npm:1.1.0" + "@trezor/analytics": "npm:1.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/e6beecb036be00d3c62af7f4f4ff96a6756df698ac19807a1b4be3fb0bd50a702780ee9a47e7e64ffebfab353ee532b07d0b5e7efdb3b611f88b9d8f9bb40157 + checksum: 10/15763dc7ddd3c8b8033c9e14cce2104639b47b1e5c4f1faabe61d4275ad2ab00368216949d1085d17b6ba1c106ab2ee3627a0afb4923152e71eb9f92db5c4459 languageName: node linkType: hard -"@trezor/connect-common@npm:0.1.0": - version: 0.1.0 - resolution: "@trezor/connect-common@npm:0.1.0" +"@trezor/connect-common@npm:0.2.0": + version: 0.2.0 + resolution: "@trezor/connect-common@npm:0.2.0" dependencies: - "@trezor/env-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/env-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/a88a2798597bfa876876ad98752fd76dbad6c185f0130127e032279a21c78b1dc3af93a5a7b8e29956acf84645f65843964a5ec2c8ef4dc30cc6cfe8f6507d45 + checksum: 10/54313304deabed9349b2cd147613dfdbfdee32ddac5a111c077b5991eb5d123cc65f28f81c9049f27d9601d610d7f3c6df3374315695a90691a0d84bf9a4b43e languageName: node linkType: hard @@ -9684,63 +9684,50 @@ __metadata: languageName: node linkType: hard -"@trezor/connect-web@npm:9.3.0": - version: 9.3.0 - resolution: "@trezor/connect-web@npm:9.3.0" - dependencies: - "@trezor/connect": "npm:9.3.0" - "@trezor/connect-common": "npm:0.1.0" - "@trezor/utils": "npm:9.1.0" - peerDependencies: - tslib: ^2.6.2 - checksum: 10/a09a04f33d44ea2934863650313dda1f255e8e0ce283760e0e9bccbec49234865b9620cf37d2e4e4f5426b32f62bd3e2618f24df6df16248c159baf2fdb1eb0e - languageName: node - linkType: hard - -"@trezor/connect-web@patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch": - version: 9.3.0 - resolution: "@trezor/connect-web@patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch::version=9.3.0&hash=3ffb2f" +"@trezor/connect-web@npm:^9.1.11, @trezor/connect-web@npm:^9.4.0": + version: 9.4.0 + resolution: "@trezor/connect-web@npm:9.4.0" dependencies: - "@trezor/connect": "npm:9.3.0" - "@trezor/connect-common": "npm:0.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/connect": "npm:9.4.0" + "@trezor/connect-common": "npm:0.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/58781efa397d2028c0eb6362fb1a8567e0b8c4ab22581633742a20f6ece37e0011e7005a951956dfee3cf3360d6e72f2c7a82549611284897631d39959ccb37f + checksum: 10/16bf476da1a0800d062379cda7b9fc06f0d296cd268d2c8995c0b2d4db37dd24668fd440543aded5f9737ff92c5defa4c0f854332d128ff31d4141430d92dc75 languageName: node linkType: hard -"@trezor/connect@npm:9.3.0": - version: 9.3.0 - resolution: "@trezor/connect@npm:9.3.0" +"@trezor/connect@npm:9.4.0": + version: 9.4.0 + resolution: "@trezor/connect@npm:9.4.0" dependencies: - "@babel/preset-typescript": "npm:^7.23.3" - "@ethereumjs/common": "npm:^4.2.0" - "@ethereumjs/tx": "npm:^5.2.1" + "@babel/preset-typescript": "npm:^7.24.7" + "@ethereumjs/common": "npm:^4.3.0" + "@ethereumjs/tx": "npm:^5.3.0" "@fivebinaries/coin-selection": "npm:2.2.1" - "@trezor/blockchain-link": "npm:2.2.0" - "@trezor/blockchain-link-types": "npm:1.1.0" - "@trezor/connect-analytics": "npm:1.1.0" - "@trezor/connect-common": "npm:0.1.0" - "@trezor/protobuf": "npm:1.1.0" - "@trezor/protocol": "npm:1.1.0" - "@trezor/schema-utils": "npm:1.1.0" - "@trezor/transport": "npm:1.2.0" - "@trezor/utils": "npm:9.1.0" - "@trezor/utxo-lib": "npm:2.1.0" + "@trezor/blockchain-link": "npm:2.3.0" + "@trezor/blockchain-link-types": "npm:1.2.0" + "@trezor/connect-analytics": "npm:1.2.0" + "@trezor/connect-common": "npm:0.2.0" + "@trezor/protobuf": "npm:1.2.0" + "@trezor/protocol": "npm:1.2.0" + "@trezor/schema-utils": "npm:1.2.0" + "@trezor/transport": "npm:1.3.0" + "@trezor/utils": "npm:9.2.0" + "@trezor/utxo-lib": "npm:2.2.0" blakejs: "npm:^1.2.1" bs58: "npm:^5.0.0" bs58check: "npm:^3.0.1" cross-fetch: "npm:^4.0.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/4210e5c72d59a3bc3a40597b585aa425967865e5067b51714157f239184ca23f9044e145948dfd4afb2aa0a7405563c8286f0bfe2ef1b9cd947e63eee283f962 + checksum: 10/1f1e0dd077474643a908acd2e9089cf62202202e377b4171a5f5c03ddb5f8c5bae8694d113cb8bc047af4d79305b62f60c342af80bd7f51c7fe0c6e18a7ba9b1 languageName: node linkType: hard -"@trezor/env-utils@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/env-utils@npm:1.1.0" +"@trezor/env-utils@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/env-utils@npm:1.2.0" dependencies: ua-parser-js: "npm:^1.0.37" peerDependencies: @@ -9755,58 +9742,57 @@ __metadata: optional: true react-native: optional: true - checksum: 10/1b09c9ebc6070396528d5f1f9f44085b0465356cfcb936a7d69cff0b26ee024d90f0bf4e531cc927a5744651d70d3fddbd4d8e5aa771a9b62b86c29d08d2682d + checksum: 10/8b63897816ceb4437847f8672bb2767394addfae47964e5435c417600b8e3b24388d1d928c30e3acccf84547508f330829db7adb517008225da76dbd3c403a19 languageName: node linkType: hard -"@trezor/protobuf@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/protobuf@npm:1.1.0" +"@trezor/protobuf@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/protobuf@npm:1.2.0" dependencies: - "@trezor/schema-utils": "npm:1.1.0" + "@trezor/schema-utils": "npm:1.2.0" protobufjs: "npm:7.2.6" peerDependencies: tslib: ^2.6.2 - checksum: 10/61846d9a236af832834a7d6c3a8f73e81f83effe9c66a37cab6819f4318e019b63749aa450c2c8bf3b24ed745f3897d01dd4b23b144a43c62a6f2359055b8710 + checksum: 10/1f510e384b0e7d1a60ecc1dd05be14a8071834138e8bb64593a8585eff81298680d055c06ec3aa11133fa08b0283630ed0fa9301165f1765ed3d6d56e207835f languageName: node linkType: hard -"@trezor/protocol@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/protocol@npm:1.1.0" +"@trezor/protocol@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/protocol@npm:1.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/860601a91621561d8e8b5c4004d3d6f6ef5ab34a2c793ce9554ff0989d4a8f57465f5f1d93a8c3f828366449254d8357efa661770d2ed135d70a88de6b7d36c8 + checksum: 10/4440973bc20cc3f58c489f7a90292591c8994bace7477205287b504947d0a1e4ea7bf9e029e6a6bdd438281a8d9ff7ea54567dc377b39b8eaa7028522d12adca languageName: node linkType: hard -"@trezor/schema-utils@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/schema-utils@npm:1.1.0" +"@trezor/schema-utils@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/schema-utils@npm:1.2.0" dependencies: "@sinclair/typebox": "npm:^0.31.28" ts-mixer: "npm:^6.0.3" peerDependencies: tslib: ^2.6.2 - checksum: 10/cb0d6fa877f44b10d41b4d5f07e5852776da16b1fb76395f35d3a310701c809bc68f9ffa9c13487a9fcdbbabf0edafe70193b1bedc43329267885857eabaa5e7 + checksum: 10/ce1e4c8d95068e45834d33346d3596745e9263d3ac58482a56010584dfd89383e3915dee9f2b729ee411a2b417c3b4e14575192e462e576630124f9ea3957d28 languageName: node linkType: hard -"@trezor/transport@npm:1.2.0": - version: 1.2.0 - resolution: "@trezor/transport@npm:1.2.0" +"@trezor/transport@npm:1.3.0": + version: 1.3.0 + resolution: "@trezor/transport@npm:1.3.0" dependencies: - "@trezor/protobuf": "npm:1.1.0" - "@trezor/protocol": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/protobuf": "npm:1.2.0" + "@trezor/protocol": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" cross-fetch: "npm:^4.0.0" - json-stable-stringify: "npm:^1.1.1" long: "npm:^4.0.0" protobufjs: "npm:7.2.6" usb: "npm:^2.11.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/e3725f98d5fa35956c81d2f9f0cf64149d7747195842654572e40810da94c1e44c8ca021fa55495d3c547f19c130f28fd13b13c051643fecb3c395c01428fc7b + checksum: 10/0b345bf848fddcf46c8c44e1f5c659794ab4a790749522fe266e1f81f6a612a477cef99c1104505aff39e976e1a25a868249ef440322faa76cea1cf8a02ffc78 languageName: node linkType: hard @@ -9817,22 +9803,22 @@ __metadata: languageName: node linkType: hard -"@trezor/utils@npm:9.1.0": - version: 9.1.0 - resolution: "@trezor/utils@npm:9.1.0" +"@trezor/utils@npm:9.2.0": + version: 9.2.0 + resolution: "@trezor/utils@npm:9.2.0" dependencies: bignumber.js: "npm:^9.1.2" peerDependencies: tslib: ^2.6.2 - checksum: 10/59590dcbb7c062991cbe0075a1b5e3b683929f2251ade96f90da12b2a01accbe14a12ef8d52e028934c97466aaeeb971b82669f0ecc69c52c42eb25f68ba92b3 + checksum: 10/9ca9f47af18cf939d02b2481666d0af15d58e53dabcae59fb9e5c18d65edcc91f793cf9104bf6505ba3041d8d2b8c9d61e252df2d5cb8e665e8b7ac41c3ac4c7 languageName: node linkType: hard -"@trezor/utxo-lib@npm:2.1.0": - version: 2.1.0 - resolution: "@trezor/utxo-lib@npm:2.1.0" +"@trezor/utxo-lib@npm:2.2.0": + version: 2.2.0 + resolution: "@trezor/utxo-lib@npm:2.2.0" dependencies: - "@trezor/utils": "npm:9.1.0" + "@trezor/utils": "npm:9.2.0" bchaddrjs: "npm:^0.5.2" bech32: "npm:^2.0.0" bip66: "npm:^1.1.5" @@ -9851,7 +9837,7 @@ __metadata: wif: "npm:^4.0.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/6b57d393c0315e8599a2381b6f09f6df419e8d11e068dd853d6c8556c113fdd6167d696507a5e32e990e09ad3b84645874e15773f83b78e3df7b7bfe040125d3 + checksum: 10/398f58ca12efb4cc72985bd8bd6a9b637a49d0c56f4de8a7eb0332c7fa7e1e797a96a103dd55fed44cc0ed630c51e7d8712b17895ac26347087c4ffd5a5a456e languageName: node linkType: hard @@ -24271,7 +24257,7 @@ __metadata: languageName: node linkType: hard -"json-stable-stringify@npm:1.1.1, json-stable-stringify@npm:^1.0.0, json-stable-stringify@npm:^1.1.1": +"json-stable-stringify@npm:1.1.1, json-stable-stringify@npm:^1.0.0": version: 1.1.1 resolution: "json-stable-stringify@npm:1.1.1" dependencies: @@ -26208,7 +26194,7 @@ __metadata: "@testing-library/react": "npm:^10.4.8" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:^14.4.3" - "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch" + "@trezor/connect-web": "npm:^9.4.0" "@tsconfig/node20": "npm:^20.1.2" "@types/babelify": "npm:^7.3.7" "@types/browserify": "npm:^12.0.37" @@ -37152,7 +37138,22 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:8.17.1, ws@npm:>=8.14.2, ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.2.3, ws@npm:^8.5.0, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:>=8.14.2, ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.2.3, ws@npm:^8.5.0, ws@npm:^8.8.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + languageName: node + linkType: hard + +"ws@npm:8.17.1": version: 8.17.1 resolution: "ws@npm:8.17.1" peerDependencies: From e4c71b70e1b0096fc8234b4951e1514cea1b3c47 Mon Sep 17 00:00:00 2001 From: Brian Bergeron <brian.e.bergeron@gmail.com> Date: Fri, 11 Oct 2024 11:26:54 -0700 Subject: [PATCH 123/226] chore: remove token list display component (#27772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes the token-list-display component, which appears to not be used anywhere. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27772?quickstart=1) ## **Related issues** ## **Manual testing steps** ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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/app-components.scss | 1 - .../app/assets/token-list-display/index.js | 1 - .../token-list-display/token-list-display.js | 61 ------------------- .../token-list-display.scss | 46 -------------- 4 files changed, 109 deletions(-) delete mode 100644 ui/components/app/assets/token-list-display/index.js delete mode 100644 ui/components/app/assets/token-list-display/token-list-display.js delete mode 100644 ui/components/app/assets/token-list-display/token-list-display.scss diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 900c49731594..0995f4b52a4a 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -56,7 +56,6 @@ @import 'assets/asset-list/asset-list-control-bar/index'; @import 'assets/asset-list/sort-control/index'; @import 'assets/token-cell/token-cell'; -@import 'assets/token-list-display/token-list-display'; @import 'transaction-activity-log/index'; @import 'transaction-breakdown/index'; @import 'transaction-icon/transaction-icon'; diff --git a/ui/components/app/assets/token-list-display/index.js b/ui/components/app/assets/token-list-display/index.js deleted file mode 100644 index 54b9cb4877d5..000000000000 --- a/ui/components/app/assets/token-list-display/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './token-list-display'; diff --git a/ui/components/app/assets/token-list-display/token-list-display.js b/ui/components/app/assets/token-list-display/token-list-display.js deleted file mode 100644 index a18f0a8f79f1..000000000000 --- a/ui/components/app/assets/token-list-display/token-list-display.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; -import { isEqual } from 'lodash'; - -import { getShouldHideZeroBalanceTokens } from '../../../../selectors'; -import { useTokenTracker } from '../../../../hooks/useTokenTracker'; -import Identicon from '../../../ui/identicon'; -import TokenBalance from '../../../ui/token-balance'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { getTokens } from '../../../../ducks/metamask/metamask'; - -export default function TokenListDisplay({ clickHandler }) { - const t = useI18nContext(); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); - - const tokens = useSelector(getTokens, isEqual); - const { loading, tokensWithBalances } = useTokenTracker({ - tokens, - includeFailedTokens: true, - hideZeroBalanceTokens: shouldHideZeroBalanceTokens, - }); - if (loading) { - return <div className="loading-span">{t('loadingTokens')}</div>; - } - - const sendableTokens = tokensWithBalances.filter((token) => !token.isERC721); - - return ( - <> - {sendableTokens.map((tokenData) => { - const { address, symbol, image } = tokenData; - - return ( - <div - key={address} - className="token-list-item" - onClick={() => clickHandler(tokenData)} - > - <Identicon address={address} diameter={36} image={image} /> - <div className="token-list-item__data"> - <div className="token-list-item__symbol">{symbol}</div> - <div className="token-list-item__balance"> - <span className="token-list-item__balance__label"> - {`${t('balance')}:`} - </span> - <TokenBalance token={tokenData} /> - </div> - </div> - </div> - ); - })} - </> - ); -} - -TokenListDisplay.propTypes = { - clickHandler: PropTypes.func, -}; diff --git a/ui/components/app/assets/token-list-display/token-list-display.scss b/ui/components/app/assets/token-list-display/token-list-display.scss deleted file mode 100644 index 6f61f9b685a1..000000000000 --- a/ui/components/app/assets/token-list-display/token-list-display.scss +++ /dev/null @@ -1,46 +0,0 @@ -@use "design-system"; - -.loading-span { - display: flex; - height: 250px; - align-items: center; - justify-content: center; - padding: 32px; -} - -.token-list-item { - display: flex; - flex-flow: row nowrap; - align-items: center; - cursor: pointer; - padding: 8px; - - &:hover { - background-color: var(--color-background-hover); - } - - &__data { - margin-left: 8px; - } - - &__symbol { - @include design-system.Paragraph; - - line-height: 140%; - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__balance { - @include design-system.H7; - - display: flex; - flex-flow: row nowrap; - } - - &__balance__label { - margin-right: 4px; - } -} From 146779de3c9d0b98b59fdabc8c6309d2af03f1df Mon Sep 17 00:00:00 2001 From: Brian Bergeron <brian.e.bergeron@gmail.com> Date: Fri, 11 Oct 2024 14:14:32 -0700 Subject: [PATCH 124/226] chore: remove old token details page (#27774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** There used to be an inner token details page nested within the asset page, that you accessed from the kebab menu: <img width="652" alt="Screenshot 2024-10-10 at 11 14 05 AM" src="https://github.com/user-attachments/assets/55089465-c0df-40f7-9537-32e69d05bb5e"> But with the redesign of the asset page in https://github.com/MetaMask/metamask-extension/pull/24522, there is no longer any way to access this page. The equivalent data and hide functionality is now directly on the main asset page. This PR removes the component. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27774?quickstart=1) ## **Related issues** ## **Manual testing steps** ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/de/messages.json | 6 - app/_locales/el/messages.json | 6 - app/_locales/en/messages.json | 6 - app/_locales/es/messages.json | 6 - app/_locales/fr/messages.json | 6 - app/_locales/hi/messages.json | 6 - app/_locales/id/messages.json | 6 - app/_locales/ja/messages.json | 6 - app/_locales/ko/messages.json | 6 - app/_locales/pt/messages.json | 6 - app/_locales/ru/messages.json | 6 - app/_locales/tl/messages.json | 6 - app/_locales/tr/messages.json | 6 - app/_locales/vi/messages.json | 6 - app/_locales/zh_CN/messages.json | 6 - ui/helpers/constants/routes.ts | 3 - ui/pages/pages.scss | 1 - ui/pages/routes/routes.component.js | 7 - ui/pages/token-details/index.js | 1 - ui/pages/token-details/index.scss | 47 --- ui/pages/token-details/token-details-page.js | 238 ----------- .../token-details/token-details-page.test.js | 370 ------------------ 22 files changed, 757 deletions(-) delete mode 100644 ui/pages/token-details/index.js delete mode 100644 ui/pages/token-details/index.scss delete mode 100644 ui/pages/token-details/token-details-page.js delete mode 100644 ui/pages/token-details/token-details-page.test.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 6177fe229bfb..9931e17a83a7 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Sensible Informationen verbergen" }, - "hideToken": { - "message": "Token verbergen" - }, "hideTokenPrompt": { "message": "Token verbergen?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Tokendezimal erforderlich. Finden Sie es auf: $1" }, - "tokenDecimalTitle": { - "message": "Token-Dezimale:" - }, "tokenDetails": { "message": "Token-Details" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index c7ce18893b4d..95e1e43cf51f 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Απόκρυψη ευαίσθητων πληροφοριών" }, - "hideToken": { - "message": "Απόκρυψη token" - }, "hideTokenPrompt": { "message": "Απόκρυψη του token;" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Απαιτείται δεκαδικό token. Βρείτε το σε: $1" }, - "tokenDecimalTitle": { - "message": "Δεκαδικά Ψηφία του token:" - }, "tokenDetails": { "message": "Λεπτομέρειες του token" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index e8b2625103d3..862b761abd8f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2284,9 +2284,6 @@ "hideSentitiveInfo": { "message": "Hide sensitive information" }, - "hideToken": { - "message": "Hide token" - }, "hideTokenPrompt": { "message": "Hide token?" }, @@ -6120,9 +6117,6 @@ "tokenDecimalFetchFailed": { "message": "Token decimal required. Find it on: $1" }, - "tokenDecimalTitle": { - "message": "Token decimal:" - }, "tokenDetails": { "message": "Token details" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 0b4dc1432ac8..9fd0f3d20941 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -2060,9 +2060,6 @@ "hideSentitiveInfo": { "message": "Ocultar información confidencial" }, - "hideToken": { - "message": "Ocultar token" - }, "hideTokenPrompt": { "message": "¿Ocultar token?" }, @@ -5698,9 +5695,6 @@ "tokenDecimalFetchFailed": { "message": "Se requiere decimal del token. Encuéntrelo en: $1" }, - "tokenDecimalTitle": { - "message": "Decimales del token:" - }, "tokenDetails": { "message": "Detalles del token" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 780300aebe08..05c67e49462f 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Masquer les informations sensibles" }, - "hideToken": { - "message": "Masquer le token" - }, "hideTokenPrompt": { "message": "Masquer le jeton ?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "La décimale du jeton est requise. Trouvez-la sur : $1" }, - "tokenDecimalTitle": { - "message": "Nombre de décimales du token :" - }, "tokenDetails": { "message": "Détails du token" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 8a3744a255f5..e23b10a874f0 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "संवेदनशील जानकारी छिपाएं" }, - "hideToken": { - "message": "टोकन छुपा दें" - }, "hideTokenPrompt": { "message": "टोकन छिपाएं?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "टोकन डेसीमल की आवश्यकता है। इसे: $1 पर पाएं" }, - "tokenDecimalTitle": { - "message": "टोकन डेसीमल:" - }, "tokenDetails": { "message": "टोकन विवरण" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 39d64cc98618..12ab926cf9ce 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Sembunyikan informasi sensitif" }, - "hideToken": { - "message": "Sembunyikan token" - }, "hideTokenPrompt": { "message": "Sembunyikan token?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Desimal token diperlukan. Temukan di: $1" }, - "tokenDecimalTitle": { - "message": "Desimal token:" - }, "tokenDetails": { "message": "Detail token" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ca1e76018a81..0c3887643691 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "機密情報を非表示" }, - "hideToken": { - "message": "トークンを非表示" - }, "hideTokenPrompt": { "message": "トークンを非表示にしますか?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "トークンの小数点以下の桁数が必要です。確認はこちら: $1" }, - "tokenDecimalTitle": { - "message": "トークンの小数桁数:" - }, "tokenDetails": { "message": "トークンの詳細" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 051a19589005..6e4dad181512 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "민감한 정보 숨기기" }, - "hideToken": { - "message": "토큰 숨기기" - }, "hideTokenPrompt": { "message": "토큰을 숨기겠습니까?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "토큰 십진수가 필요합니다. $1에서 찾아보세요" }, - "tokenDecimalTitle": { - "message": "토큰 소수점:" - }, "tokenDetails": { "message": "토큰 상세 정보" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 8d686eaee7c2..47a53a6ed328 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Ocultar informações confidenciais" }, - "hideToken": { - "message": "Ocultar token" - }, "hideTokenPrompt": { "message": "Ocultar token?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "É necessário o decimal do token. Encontre-o em: $1" }, - "tokenDecimalTitle": { - "message": "Decimal do token:" - }, "tokenDetails": { "message": "Dados do token" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b36ac87f7dbe..0ecd4f0eb8d6 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Скрыть конфиденциальную информацию" }, - "hideToken": { - "message": "Скрыть токен" - }, "hideTokenPrompt": { "message": "Скрыть токен?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Требуется десятичный токен. Найдите его здесь: $1" }, - "tokenDecimalTitle": { - "message": "Десятичный токен:" - }, "tokenDetails": { "message": "Сведения о токене" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index e076ac55176b..c6614483aa5b 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Itago ang sensitibong impormasyon" }, - "hideToken": { - "message": "Itago ang token" - }, "hideTokenPrompt": { "message": "Itago ang token?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Kailangan ng decimal ng token. Hanapin ito sa: $1" }, - "tokenDecimalTitle": { - "message": "Mga Decimal ng Katumpakan:" - }, "tokenDetails": { "message": "Mga detalye ng token" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 4465fd7c0d78..361b92cdd87e 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Hassas bilgileri gizle" }, - "hideToken": { - "message": "Tokeni gizle" - }, "hideTokenPrompt": { "message": "Tokeni gizle?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Token ondalığı gereklidir. Şurada bulabilirsiniz: $1" }, - "tokenDecimalTitle": { - "message": "Token ondalığı:" - }, "tokenDetails": { "message": "Token bilgileri" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 2d8d89a25ee7..3be725af9351 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Ẩn thông tin nhạy cảm" }, - "hideToken": { - "message": "Ẩn token" - }, "hideTokenPrompt": { "message": "Ẩn token?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "Yêu cầu số thập phân của token. Tìm trên: $1" }, - "tokenDecimalTitle": { - "message": "Số thập phân của token:" - }, "tokenDetails": { "message": "Chi tiết token" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 37836c219ccd..58298abdf542 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -2063,9 +2063,6 @@ "hideSentitiveInfo": { "message": "隐藏敏感信息" }, - "hideToken": { - "message": "隐藏代币" - }, "hideTokenPrompt": { "message": "隐藏代币?" }, @@ -5701,9 +5698,6 @@ "tokenDecimalFetchFailed": { "message": "需要代币小数位。请在下方查找:$1" }, - "tokenDecimalTitle": { - "message": "代币小数:" - }, "tokenDetails": { "message": "代币详情" }, diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 8052ad867084..5e4fffe413e2 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -125,9 +125,6 @@ PATH_NAME_MAP[PERMISSIONS] = 'Permissions'; export const REVIEW_PERMISSIONS = '/review-permissions'; -export const TOKEN_DETAILS = '/token-details'; -PATH_NAME_MAP[`${TOKEN_DETAILS}/:address`] = 'Token Details Page'; - export const CONNECT_ROUTE = '/connect'; PATH_NAME_MAP[`${CONNECT_ROUTE}/:id`] = 'Connect To Site Confirmation Page'; diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 2578286032d2..9f86ad6f6e5a 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -28,5 +28,4 @@ @import 'create-snap-account/index'; @import 'remove-snap-account/index'; @import 'swaps/index'; -@import 'token-details/index'; @import 'unlock-page/index'; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 25c41ca37c82..7cfd33f655ac 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -43,7 +43,6 @@ import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; import OnboardingAppHeader from '../onboarding-flow/onboarding-app-header/onboarding-app-header'; -import TokenDetailsPage from '../token-details'; import Notifications from '../notifications'; import NotificationsSettings from '../notifications-settings'; import NotificationDetails from '../notification-details'; @@ -76,7 +75,6 @@ import { CONFIRMATION_V_NEXT_ROUTE, ONBOARDING_ROUTE, ONBOARDING_UNLOCK_ROUTE, - TOKEN_DETAILS, CONNECTIONS, PERMISSIONS, REVIEW_PERMISSIONS, @@ -369,11 +367,6 @@ export default class Routes extends Component { component={ConfirmTransaction} /> <Authenticated path={SEND_ROUTE} component={SendPage} exact /> - <Authenticated - path={`${TOKEN_DETAILS}/:address/`} - component={TokenDetailsPage} - exact - /> <Authenticated path={SWAPS_ROUTE} component={Swaps} /> <Authenticated path={CROSS_CHAIN_SWAP_ROUTE} diff --git a/ui/pages/token-details/index.js b/ui/pages/token-details/index.js deleted file mode 100644 index 0a8a820e90fe..000000000000 --- a/ui/pages/token-details/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './token-details-page'; diff --git a/ui/pages/token-details/index.scss b/ui/pages/token-details/index.scss deleted file mode 100644 index 6751544d7506..000000000000 --- a/ui/pages/token-details/index.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use "design-system"; - -.token-details { - &__title { - text-transform: capitalize; - } - - &__closeButton { - float: right; - width: 10px; - margin-top: -17px; - margin-inline-end: -8px; - - &::after { - font-size: 24px; - content: '\00D7'; - color: var(--color-icon-default); - } - } - - &__token-value { - font-size: 32px; - } - - &__token-address { - width: 222px; - } - - &__copy-icon { - float: right; - margin-inline-start: 62px; - - @include design-system.screen-sm-min { - margin-inline-start: 112px; - } - } - - &__hide-token-button { - width: 319px; - height: 39px; - margin-top: 70px; - - @include design-system.screen-sm-min { - margin-inline-start: 20px; - } - } -} diff --git a/ui/pages/token-details/token-details-page.js b/ui/pages/token-details/token-details-page.js deleted file mode 100644 index 086a2de61f4b..000000000000 --- a/ui/pages/token-details/token-details-page.js +++ /dev/null @@ -1,238 +0,0 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Redirect, useHistory, useParams } from 'react-router-dom'; -import { getProviderConfig, getTokens } from '../../ducks/metamask/metamask'; -import { getTokenList } from '../../selectors'; -import { useCopyToClipboard } from '../../hooks/useCopyToClipboard'; -import Identicon from '../../components/ui/identicon'; -import { I18nContext } from '../../contexts/i18n'; -import { useTokenTracker } from '../../hooks/useTokenTracker'; -import { useTokenFiatAmount } from '../../hooks/useTokenFiatAmount'; -import { showModal } from '../../store/actions'; -import { NETWORK_TYPES } from '../../../shared/constants/network'; -import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'; -import Tooltip from '../../components/ui/tooltip'; -import Button from '../../components/ui/button'; -import Box from '../../components/ui/box'; -import { - TextVariant, - FontWeight, - DISPLAY, - TextAlign, - OverflowWrap, - TextColor, - IconColor, -} from '../../helpers/constants/design-system'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; -import { - ButtonIcon, - ButtonIconSize, - IconName, - Text, -} from '../../components/component-library'; - -export default function TokenDetailsPage() { - const dispatch = useDispatch(); - const history = useHistory(); - const t = useContext(I18nContext); - const tokens = useSelector(getTokens); - const tokenList = useSelector(getTokenList); - const { address: tokenAddress } = useParams(); - const tokenMetadata = tokenList[tokenAddress.toLowerCase()]; - const aggregators = tokenMetadata?.aggregators?.join(', '); - - const token = tokens.find(({ address }) => - isEqualCaseInsensitive(address, tokenAddress), - ); - - // When the user did not import the token - // the token variable will be undefined. - // In that case we want to call useTokenTracker with [] instead of [undefined] - const { tokensWithBalances } = useTokenTracker({ - tokens: token ? [token] : [], - }); - const tokenBalance = tokensWithBalances[0]?.string; - - const tokenCurrencyBalance = useTokenFiatAmount( - token?.address, - tokenBalance, - token?.symbol, - ); - - const { nickname, type: networkType } = useSelector(getProviderConfig); - - const [copied, handleCopy] = useCopyToClipboard(); - - if (!token) { - return <Redirect to={{ pathname: DEFAULT_ROUTE }} />; - } - return ( - <Box className="page-container token-details"> - <Box marginLeft={5} marginRight={6}> - <Text - fontWeight={FontWeight.Bold} - margin={0} - marginTop={4} - variant={TextVariant.bodySm} - as="h6" - color={TextColor.textDefault} - className="token-details__title" - > - {t('tokenDetails')} - <Button - type="link" - onClick={() => history.push(`${ASSET_ROUTE}/${token.address}`)} - className="token-details__closeButton" - /> - </Text> - <Box display={DISPLAY.FLEX} marginTop={4}> - <Text - align={TextAlign.Center} - fontWeight={FontWeight.Bold} - margin={0} - marginRight={5} - variant={TextVariant.headingSm} - as="h4" - color={TextColor.textDefault} - className="token-details__token-value" - > - {tokenBalance || ''} - </Text> - <Box marginTop={1}> - <Identicon - diameter={32} - address={token.address} - image={tokenMetadata ? tokenMetadata.iconUrl : token.image} - /> - </Box> - </Box> - <Text - margin={0} - marginTop={4} - variant={TextVariant.bodySm} - as="h6" - color={TextColor.textAlternative} - > - {tokenCurrencyBalance || ''} - </Text> - <Text - margin={0} - marginTop={6} - variant={TextVariant.bodyXs} - as="h6" - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('tokenContractAddress')} - </Text> - <Box display={DISPLAY.FLEX}> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={0} - marginTop={2} - color={TextColor.textDefault} - overflowWrap={OverflowWrap.BreakWord} - className="token-details__token-address" - > - {token.address} - </Text> - <Tooltip - position="bottom" - title={copied ? t('copiedExclamation') : t('copyToClipboard')} - containerClassName="token-details__copy-icon" - > - <ButtonIcon - ariaLabel="copy" - name={copied ? IconName.CopySuccess : IconName.Copy} - className="token-details__copyIcon" - onClick={() => handleCopy(token.address)} - color={IconColor.primaryDefault} - size={ButtonIconSize.Sm} - /> - </Tooltip> - </Box> - <Text - variant={TextVariant.bodyXs} - as="h6" - margin={0} - marginTop={4} - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('tokenDecimalTitle')} - </Text> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={0} - marginTop={1} - color={TextColor.textDefault} - > - {token.decimals} - </Text> - <Text - variant={TextVariant.bodyXs} - as="h6" - margin={0} - marginTop={4} - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('network')} - </Text> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={1} - marginTop={0} - color={TextColor.textDefault} - > - {networkType === NETWORK_TYPES.RPC - ? nickname ?? t('privateNetwork') - : t(networkType)} - </Text> - {aggregators && ( - <> - <Text - variant={TextVariant.bodyXs} - as="h6" - margin={0} - marginTop={4} - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('tokenList')}: - </Text> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={0} - marginTop={1} - color={TextColor.textDefault} - > - {`${aggregators}.`} - </Text> - </> - )} - <Button - type="secondary" - className="token-details__hide-token-button" - onClick={() => { - dispatch( - showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token, history }), - ); - }} - > - <Text - variant={TextVariant.bodySm} - as="h6" - color={TextColor.primaryDefault} - > - {t('hideToken')} - </Text> - </Button> - </Box> - </Box> - ); -} diff --git a/ui/pages/token-details/token-details-page.test.js b/ui/pages/token-details/token-details-page.test.js deleted file mode 100644 index c7d7933c6c3b..000000000000 --- a/ui/pages/token-details/token-details-page.test.js +++ /dev/null @@ -1,370 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { EthAccountType } from '@metamask/keyring-api'; -import { fireEvent } from '@testing-library/react'; -import { renderWithProvider } from '../../../test/lib/render-helpers'; -import Identicon from '../../components/ui/identicon'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; -import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; -import { mockNetworkState } from '../../../test/stub/networks'; -import { CHAIN_IDS } from '../../../shared/constants/network'; -import TokenDetailsPage from './token-details-page'; - -const testTokenAddress = '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F'; -const state = { - metamask: { - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0xAddress', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - contractExchangeRates: { - '0xAnotherToken': 0.015, - }, - useTokenDetection: true, - tokenList: { - '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': { - address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Synthetix', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', - name: 'Synthetix Network Token', - occurrences: 12, - symbol: 'SNX', - }, - '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: - 'https://images.prismic.io/token-price-prod/d0352dd9-5de8-4633-839d-bc3422c44d9c_UNI%404x.png', - name: 'Uniswap', - occurrences: 11, - symbol: 'UNI', - }, - '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e': { - address: '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: - 'https://images.prismic.io/token-price-prod/917bc4fa-59d4-40f5-a3ef-33035698ffe0_YFIxxxhdpi.png', - name: 'yearn.finance', - occurrences: 11, - symbol: 'YFI', - }, - '0x408e41876cccdc0f92210600ef50372656052a38': { - address: '0x408e41876cccdc0f92210600ef50372656052a38', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/REN/color_icon.png', - name: 'Republic Token', - occurrences: 11, - symbol: 'REN', - }, - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599': { - address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 8, - iconUrl: - 'https://images.prismic.io/token-price-prod/c27778b1-f402-45f0-9225-f24f24b0518a_WBTC-xxxhdpi.png', - name: 'Wrapped BTC', - occurrences: 11, - symbol: 'WBTC', - }, - '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2': { - address: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/MKR/color_icon.png', - name: 'MakerDAO', - occurrences: 11, - symbol: 'MKR', - }, - '0x514910771af9ca656af840dff83e8264ecf986ca': { - address: '0x514910771af9ca656af840dff83e8264ecf986ca', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/LINK/color_icon.png', - name: 'ChainLink Token', - occurrences: 11, - symbol: 'LINK', - }, - '0x6b175474e89094c44da98b954eedeac495271d0f': { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/DAI/color_icon.png', - name: 'Dai Stablecoin', - occurrences: 11, - symbol: 'DAI', - }, - '0x04fa0d235c4abf4bcf4787af4cf447de572ef828': { - address: '0x04fa0d235c4abf4bcf4787af4cf447de572ef828', - aggregators: [ - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: - 'https://images.prismic.io/token-price-prod/e2850554-ccf6-4514-9c3c-a17e19dea82f_UMAxxxhdpi.png', - name: 'UMA', - occurrences: 10, - symbol: 'UMA', - }, - }, - ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - currencyRates: {}, - preferences: { - showFiatInTestnets: true, - }, - tokens: [ - { - address: testTokenAddress, - symbol: 'SNX', - decimals: 18, - image: 'testImage', - isERC721: false, - }, - { - address: '0xaD6D458402F60fD3Bd25163575031ACDce07538U', - symbol: 'DAU', - decimals: 18, - image: null, - isERC721: false, - }, - ], - }, -}; - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - return { - ...original, - useHistory: () => ({ - push: jest.fn(), - }), - useParams: () => ({ - address: testTokenAddress, - }), - }; -}); - -describe('TokenDetailsPage', () => { - it('should render title "Token details" in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token details')).toBeInTheDocument(); - }); - - it('should close token details page when close button is clicked', () => { - const store = configureMockStore()(state); - const { container } = renderWithProvider(<TokenDetailsPage />, store); - const onCloseBtn = container.querySelector('.token-details__closeButton'); - fireEvent.click(onCloseBtn); - expect(onCloseBtn).toBeDefined(); - }); - - it('should render an icon image', () => { - const token = state.metamask.tokens.find(({ address }) => - isEqualCaseInsensitive(address, testTokenAddress), - ); - const iconImage = ( - <Identicon diameter={32} address={testTokenAddress} image={token.image} /> - ); - expect(iconImage).toBeDefined(); - }); - - it('should render token contract address title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token contract address')).toBeInTheDocument(); - }); - - it('should render token contract address in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText(testTokenAddress)).toBeInTheDocument(); - }); - - it('should call copy button when click is simulated', () => { - const store = configureMockStore()(state); - const { container } = renderWithProvider(<TokenDetailsPage />, store); - const handleCopyBtn = container.querySelector('.token-details__copyIcon'); - fireEvent.click(handleCopyBtn); - expect(handleCopyBtn).toBeDefined(); - }); - - it('should render token decimal title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token decimal:')).toBeInTheDocument(); - }); - - it('should render number of token decimals in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('18')).toBeInTheDocument(); - }); - - it('should render current network title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Network:')).toBeInTheDocument(); - }); - - it('should render current network in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Ethereum Mainnet')).toBeInTheDocument(); - }); - - it('should render token list title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token lists:')).toBeInTheDocument(); - }); - - it('should render token list for the token in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect( - getByText( - 'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.', - ), - ).toBeInTheDocument(); - }); - - it('should call hide token button when button is clicked in token details page', () => { - const store = configureMockStore()(state); - const { container } = renderWithProvider(<TokenDetailsPage />, store); - const hideTokenBtn = container.querySelector( - '.token-details__hide-token-button', - ); - fireEvent.click(hideTokenBtn); - expect(hideTokenBtn).toBeDefined(); - }); - - it('should render label of hide token button in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Hide token')).toBeInTheDocument(); - }); -}); From 726aa05a6c23ca3255640b7c19c2e61f20438fa9 Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:41:12 +0200 Subject: [PATCH 125/226] test: Fix Vault Decryptor Page e2e test on develop branch (#27794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Fix ci failure: https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/104860/workflows/4005eafc-4fa7-4efb-a0e1-53d4283c09b8/jobs/3910197 The failing reason is that we should turn off all the privacy settings toggles. With the onboarding settings redesign, the incoming transactions toggle is on a different page than the basic functionality toggle, so we should also toggle it off in the assets section. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27794?quickstart=1) ## **Related issues** Fixes: #27791 ## **Manual testing steps** Test pass on CI ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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. --- test/e2e/helpers.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index bf55c7bbf52c..926b152e899b 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -564,7 +564,8 @@ const onboardingPinExtension = async (driver) => { const onboardingCompleteWalletCreationWithOptOut = async (driver) => { // wait for h2 to appear await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); - // opt-out from third party API + + // opt-out from third party API on general section await driver.clickElement({ text: 'Manage default settings', tag: 'button' }); await driver.clickElement({ text: 'General', tag: 'p' }); await driver.clickElement( @@ -573,6 +574,9 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); + // opt-out from third party API on assets section + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.clickElement({ text: 'Assets', tag: 'p' }); await Promise.all( ( await driver.findClickableElements( From f2f180bc988f7c86bfdde0d7c1b96209b37207b9 Mon Sep 17 00:00:00 2001 From: Mark Stacey <markjstacey@gmail.com> Date: Sat, 12 Oct 2024 05:15:47 -0230 Subject: [PATCH 126/226] ci: Improve validation for `sentry:publish` script (#26580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The script now validates that the "dist directory" exists and is non- empty. Previously in that circumstance, the script would "successfully" upload zero files, making this problem easy to miss. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26580?quickstart=1) ## **Related issues** This is a minor improvement to a release script to address a problem discovered during testing. ## **Manual testing steps** Before running these commands, you'll need to have a personal Sentry account setup. Set the environment variable `SENTRY_AUTH_TOKEN` to a custom integration Auth Token with access to the `metamask` project on your account. * Run `yarn sentry:publish` with no `dist` directory, and see that it has a helpful error * Run `yarn sentry:publish --dist mv2` without a `dist-mv2` directory, and see that it has a helpful error. * Try again with those directories empty, and see that it errors as well * Try with real builds, and see that the command succeeds ## **Screenshots/Recordings** 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** - [ ] 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. --- development/sentry-publish.js | 57 +++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/development/sentry-publish.js b/development/sentry-publish.js index 22ff6156a243..5b60b5bd6a4f 100755 --- a/development/sentry-publish.js +++ b/development/sentry-publish.js @@ -1,5 +1,7 @@ #!/usr/bin/env node +const fs = require('node:fs/promises'); +const path = require('node:path'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); @@ -80,17 +82,19 @@ async function start() { ]); } - const additionalUploadArgs = []; - if (dist) { - additionalUploadArgs.push('--dist', dist); - } + let distDirectory = 'dist'; if (buildType !== loadBuildTypesConfig().default) { - additionalUploadArgs.push( - '--dist-directory', - dist ? `dist-${buildType}-${dist}` : `dist-${buildType}`, - ); + distDirectory = dist ? `dist-${buildType}-${dist}` : `dist-${buildType}`; } else if (dist) { - additionalUploadArgs.push('--dist-directory', `dist-${dist}`); + distDirectory = `dist-${dist}`; + } + + const absoluteDistDirectory = path.resolve(__dirname, '../', distDirectory); + await assertIsNonEmptyDirectory(absoluteDistDirectory); + + const additionalUploadArgs = ['--dist-directory', distDirectory]; + if (dist) { + additionalUploadArgs.push('--dist', dist); } // upload sentry source and sourcemaps await runInShell('./development/sentry-upload-artifacts.sh', [ @@ -123,3 +127,38 @@ async function doesNotFail(asyncFn) { throw error; } } + +/** + * Assert that the given path exists, and is a non-empty directory. + * + * @param {string} directoryPath - The path to check. + */ +async function assertIsNonEmptyDirectory(directoryPath) { + await assertIsDirectory(directoryPath); + + const files = await fs.readdir(directoryPath); + if (!files.length) { + throw new Error(`Directory empty: '${directoryPath}'`); + } +} + +/** + * Assert that the given path exists, and is a directory. + * + * @param {string} directoryPath - The path to check. + */ +async function assertIsDirectory(directoryPath) { + try { + const directoryStats = await fs.stat(directoryPath); + if (!directoryStats.isDirectory()) { + throw new Error(`Invalid path '${directoryPath}'; must be a directory`); + } + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`Directory '${directoryPath}' not found`, { + cause: error, + }); + } + throw error; + } +} From f9ec0e037fbf0b8ec680fc1303bdee4aa263562e Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:13:11 +0200 Subject: [PATCH 127/226] fix: disable balance checker for Sepolia in account tracker (#27763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> This PR resolves a bug introduced in [PR #27258](https://github.com/MetaMask/metamask-extension/pull/27258), where a change enabled the use of the balance checker for the Sepolia network. Line that caused the bug: https://github.com/MetaMask/metamask-extension/pull/27258/files#diff-1acb7898d60977530c97169551d22dbe477a4e3aeb74f1f14bf2eea0b4d75d35R695 (be sure to expand the changes in `app/scripts/controllers/account-tracker-controller.ts`) ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27763?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Switch to the current `develop` branch. 2. Run the extension and open the service worker console. 3. Attempt to open the extension. 4. You should see the following warning: ``` MetaMask - Account Tracker single call balance fetch failed Error: resolver or addr is not configured for ENS name (argument="name", value="", code=INVALID_ARGUMENT, version=contracts/5.7.0) (anonymous) @ sentry-install.js:37777 _updateAccountsViaBalanceChecker2 @ account-tracker-controller.ts:825 await in _updateAccountsViaBalanceChecker2 updateAccounts @ account-tracker-controller.ts:704 start @ account-tracker-controller.ts:306 triggerNetworkrequests @ metamask-controller.js:2394 (anonymous) @ metamask-controller.js:1653 setupControllerConnection @ metamask-controller.js:5110 setupTrustedCommunication @ metamask-controller.js:4984 connectRemote @ background.js:846 (anonymous) @ background.js:340 ``` 5. Switch to my PR and repeat steps 2 and 3. 6. The warning should no longer appear. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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/constants/contracts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/constants/contracts.ts b/app/scripts/constants/contracts.ts index bc27be31d95c..fae185b1a499 100644 --- a/app/scripts/constants/contracts.ts +++ b/app/scripts/constants/contracts.ts @@ -4,7 +4,7 @@ export const SINGLE_CALL_BALANCES_ADDRESSES = { [CHAIN_IDS.MAINNET]: '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39', [CHAIN_IDS.GOERLI]: '0x9788C4E93f9002a7ad8e72633b11E8d1ecd51f9b', // TODO(SEPOLIA) There is currently no balance call address for Sepolia - [CHAIN_IDS.SEPOLIA]: '', + // [CHAIN_IDS.SEPOLIA]: '', [CHAIN_IDS.BSC]: '0x2352c63A83f9Fd126af8676146721Fa00924d7e4', [CHAIN_IDS.OPTIMISM]: '0xB1c568e9C3E6bdaf755A60c7418C269eb11524FC', [CHAIN_IDS.POLYGON]: '0x2352c63A83f9Fd126af8676146721Fa00924d7e4', From c678db23f437865334c9c4362d52827451036928 Mon Sep 17 00:00:00 2001 From: David Drazic <david@timechaser.org> Date: Mon, 14 Oct 2024 15:11:12 +0200 Subject: [PATCH 128/226] fix: sticky footer UI issue on Snaps Home Page in extended view (#27799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix issue with sticky Snaps UI Footer component in extended view. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27799?quickstart=1) ## **Related issues** Fixes: n/a ## **Manual testing steps** 1. Try all the Snaps that use custom footer (Home Page Snap, Custom Dialog Snap with custom UI, etc.). 2. Make sure that footer has correct width matching the width of the content view. ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/user-attachments/assets/f2a2c924-2bd9-451e-9b26-aadda9e94b22) ### **After** ![Screenshot 2024-10-11 at 20 38 13](https://github.com/user-attachments/assets/644d97f6-89b3-4971-bc8e-d51322888788) ![Screenshot 2024-10-11 at 20 38 48](https://github.com/user-attachments/assets/b65b113c-8aa1-4d54-b70c-ab88dad41505) ![Screenshot 2024-10-11 at 20 40 55](https://github.com/user-attachments/assets/215ae7a8-4e20-4a6b-a2ff-f4276515ded4) ![Screenshot 2024-10-11 at 20 41 15](https://github.com/user-attachments/assets/fa3f324d-3fc3-473b-81ea-bc4ce65ebaf3) ![Screenshot 2024-10-11 at 20 56 29](https://github.com/user-attachments/assets/6ad44d4b-a869-4f2a-9043-397e5730e757) ## **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/snaps/snap-ui-renderer/index.scss | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss index b1bc569af333..7e18e72c917f 100644 --- a/ui/components/app/snaps/snap-ui-renderer/index.scss +++ b/ui/components/app/snaps/snap-ui-renderer/index.scss @@ -1,4 +1,10 @@ +@use "design-system"; + .snap-ui-renderer { + $width-screen-sm-min: 85vw; + $width-screen-md-min: 80vw; + $width-screen-lg-min: 62vw; + &__content { margin-bottom: 0 !important; } @@ -69,5 +75,17 @@ &__footer { margin-top: auto; + + @include design-system.screen-sm-min { + max-width: $width-screen-sm-min; + } + + @include design-system.screen-md-min { + max-width: $width-screen-md-min; + } + + @include design-system.screen-lg-min { + max-width: $width-screen-lg-min; + } } } From ca14e7b82ed7889c3c2d52664edff38bfcdcfbe3 Mon Sep 17 00:00:00 2001 From: Frederik Bolding <frederik.bolding@gmail.com> Date: Mon, 14 Oct 2024 15:28:11 +0200 Subject: [PATCH 129/226] ci: Revert minimum E2E timeout to 20 minutes (#27827) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Reverts the minimum E2E timeout back to 20 minutes to unblock merges to `develop` while we investigate why E2E's have slowed down dramatically. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27827?quickstart=1) --- .circleci/scripts/test-run-e2e-timeout-minutes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/scripts/test-run-e2e-timeout-minutes.ts b/.circleci/scripts/test-run-e2e-timeout-minutes.ts index 1fc06696712a..c539133b0c60 100644 --- a/.circleci/scripts/test-run-e2e-timeout-minutes.ts +++ b/.circleci/scripts/test-run-e2e-timeout-minutes.ts @@ -2,7 +2,7 @@ import { filterE2eChangedFiles } from '../../test/e2e/changedFilesUtil'; const changedOrNewTests = filterE2eChangedFiles(); -//15 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes -const extraTime = Math.min(15 + changedOrNewTests.length * 3, 30); +// 20 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes +const extraTime = Math.min(20 + changedOrNewTests.length * 3, 30); console.log(extraTime); From 1f1e142f498c96c142731cea602abd8069b2f5e0 Mon Sep 17 00:00:00 2001 From: Frederik Bolding <frederik.bolding@gmail.com> Date: Mon, 14 Oct 2024 16:19:01 +0200 Subject: [PATCH 130/226] fix: Fix Snaps usage of PhishingController (#27817) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Fixes two problems with Snaps usage of `PhishingController`. Following https://github.com/MetaMask/metamask-extension/pull/25839 the PhishingController expects full URLs instead of hostnames as the input to `testOrigin`. In that PR, the argument of `isOnPhishingList` was incorrectly changed. This PR also patches in some changes from the `snaps` repo that are currently blocked by a release: https://github.com/MetaMask/snaps/pull/2835, https://github.com/MetaMask/snaps/pull/2750 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27817?quickstart=1) ## **Manual testing steps** 1. Create a Snap that links to an URL blocked with `eth-phishing-detect` 2. See that triggering the Snap is disallowed if the user has phishing detection enabled --- ...ask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch | 120 ++++++++++++++++++ app/scripts/metamask-controller.js | 4 +- package.json | 5 +- yarn.lock | 39 +++++- 4 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 .yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch 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 new file mode 100644 index 000000000000..3361025d4860 --- /dev/null +++ b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch @@ -0,0 +1,120 @@ +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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file ++{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";;;;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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file +diff --git a/dist/ui.d.cts b/dist/ui.d.cts +index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644 +--- a/dist/ui.d.cts ++++ b/dist/ui.d.cts +@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; + /** +diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map +index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644 +--- a/dist/ui.d.cts.map ++++ b/dist/ui.d.cts.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file ++{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file +diff --git a/dist/ui.d.mts b/dist/ui.d.mts +index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644 +--- a/dist/ui.d.mts ++++ b/dist/ui.d.mts +@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; + /** +diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map +index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644 +--- a/dist/ui.d.mts.map ++++ b/dist/ui.d.mts.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file ++{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file +diff --git a/dist/ui.mjs b/dist/ui.mjs +index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644 +--- a/dist/ui.mjs ++++ b/dist/ui.mjs +@@ -195,13 +195,23 @@ function getMarkdownLinks(text) { + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + export function validateLink(link, isOnPhishingList) { + try { + const url = new URL(link); + assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); +- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; +- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); ++ if (url.protocol === 'mailto:') { ++ const emails = url.pathname.split(','); ++ for (const email of emails) { ++ const hostname = email.split('@')[1]; ++ assert(!hostname.includes(':')); ++ const href = `https://${hostname}`; ++ assert(!isOnPhishingList(href), 'The specified URL is not allowed.'); ++ } ++ return; ++ } ++ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); + } + catch (error) { + throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); +diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map +index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644 +--- a/dist/ui.mjs.map ++++ b/dist/ui.mjs.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file ++{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 96a081e3308d..83edd7ade418 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2781,7 +2781,7 @@ export default class MetamaskController extends EventEmitter { 'PhishingController:maybeUpdateState', ); }, - isOnPhishingList: (sender) => { + isOnPhishingList: (url) => { const { usePhishDetect } = this.preferencesController.store.getState(); @@ -2791,7 +2791,7 @@ export default class MetamaskController extends EventEmitter { return this.controllerMessenger.call( 'PhishingController:testOrigin', - sender.url, + url, ).result; }, createInterface: this.controllerMessenger.call.bind( diff --git a/package.json b/package.json index 201f76f3c473..90da21554bc2 100644 --- a/package.json +++ b/package.json @@ -264,7 +264,8 @@ "@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" + "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" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -356,7 +357,7 @@ "@metamask/snaps-execution-environments": "^6.7.2", "@metamask/snaps-rpc-methods": "^11.1.1", "@metamask/snaps-sdk": "^6.5.1", - "@metamask/snaps-utils": "^8.1.1", + "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch", "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", diff --git a/yarn.lock b/yarn.lock index a003e9a42cd4..bc7fc36aabbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6357,6 +6357,37 @@ __metadata: 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 + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" @@ -6388,9 +6419,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1": +"@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@npm: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" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6415,7 +6446,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f + checksum: 10/6b1d3d70c5ebee684d5b76bf911c66ebd122a0607cefcfc9fffd4bf6882a7acfca655d97be87c0f7f47e59a981b58234578ed8a123e554a36e6c48ff87492655 languageName: node linkType: hard @@ -26149,7 +26180,7 @@ __metadata: "@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": "npm:^8.1.1" + "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" "@metamask/transaction-controller": "npm:^37.2.0" From bf318762609ee65c5ca3e8e117a1f1ecf1ae3d5c Mon Sep 17 00:00:00 2001 From: Jony Bursztyn <jony.bursztyn@consensys.net> Date: Mon, 14 Oct 2024 16:12:43 +0100 Subject: [PATCH 131/226] feat: remove phishing detection from onboarding Security group (#27819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <img width="620" alt="Screenshot 2024-10-14 at 11 57 53" src="https://github.com/user-attachments/assets/f512cfb8-e770-4ad3-93ef-f4f441fc06cc">` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27819?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3497 ## **Manual testing steps** 1. Onboard 2. Go to "Reminder set!" screen 3. Click on "Manage default settings" 4. Click on "Security" 5. Check that there's no ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 | 15 --------------- .../privacy-settings/privacy-settings.test.js | 11 ++--------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index ca3bd0af2ff4..ee11f63caf2a 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -57,7 +57,6 @@ import { setIpfsGateway, setUseCurrencyRateCheck, setUseMultiAccountBalanceChecker, - setUsePhishDetect, setUse4ByteResolution, setUseTokenDetection, setUseAddressBarEnsResolution, @@ -132,7 +131,6 @@ export default function PrivacySettings() { } = defaultState; const petnamesEnabled = useSelector(getPetnamesEnabled); - const [usePhishingDetection, setUsePhishingDetection] = useState(null); const [turnOn4ByteResolution, setTurnOn4ByteResolution] = useState(use4ByteResolution); const [turnOnTokenDetection, setTurnOnTokenDetection] = @@ -160,17 +158,11 @@ export default function PrivacySettings() { getExternalServicesOnboardingToggleState, ); - const phishingToggleState = - usePhishingDetection === null - ? externalServicesOnboardingToggleState - : usePhishingDetection; - const profileSyncingProps = useProfileSyncingProps( externalServicesOnboardingToggleState, ); const handleSubmit = () => { - dispatch(setUsePhishDetect(phishingToggleState)); dispatch(setUse4ByteResolution(turnOn4ByteResolution)); dispatch(setUseTokenDetection(turnOnTokenDetection)); dispatch( @@ -199,7 +191,6 @@ export default function PrivacySettings() { is_profile_syncing_enabled: profileSyncingProps.isProfileSyncingEnabled, is_basic_functionality_enabled: externalServicesOnboardingToggleState, show_incoming_tx: incomingTransactionsPreferences, - use_phising_detection: usePhishingDetection, turnon_token_detection: turnOnTokenDetection, }, }); @@ -720,12 +711,6 @@ export default function PrivacySettings() { ) : null} {selectedItem && selectedItem.id === 3 ? ( <> - <Setting - value={phishingToggleState} - setValue={setUsePhishingDetection} - title={t('usePhishingDetection')} - description={t('usePhishingDetectionDescription')} - /> <Setting value={turnOn4ByteResolution} setValue={setTurnOn4ByteResolution} diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js index ec8b88fc52e9..81943ee71f49 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js @@ -42,7 +42,6 @@ describe('Privacy Settings Onboarding View', () => { [CHAIN_IDS.LINEA_GOERLI]: true, [CHAIN_IDS.LINEA_SEPOLIA]: true, }, - usePhishDetect: true, use4ByteResolution: true, useTokenDetection: false, useCurrencyRateCheck: true, @@ -59,7 +58,6 @@ describe('Privacy Settings Onboarding View', () => { const store = configureMockStore([thunk])(mockStore); const setFeatureFlagStub = jest.fn(); - const setUsePhishDetectStub = jest.fn(); const setUse4ByteResolutionStub = jest.fn(); const setUseTokenDetectionStub = jest.fn(); const setUseCurrencyRateCheckStub = jest.fn(); @@ -79,7 +77,6 @@ describe('Privacy Settings Onboarding View', () => { setBackgroundConnection({ setFeatureFlag: setFeatureFlagStub, - setUsePhishDetect: setUsePhishDetectStub, setUse4ByteResolution: setUse4ByteResolutionStub, setUseTokenDetection: setUseTokenDetectionStub, setUseCurrencyRateCheck: setUseCurrencyRateCheckStub, @@ -104,7 +101,6 @@ describe('Privacy Settings Onboarding View', () => { ); // All settings are initialized toggled to be same as default expect(toggleExternalServicesStub).toHaveBeenCalledTimes(0); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(0); expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(0); expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(0); expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(0); @@ -148,9 +144,8 @@ describe('Privacy Settings Onboarding View', () => { toggles = container.querySelectorAll('input[type=checkbox]'); - fireEvent.click(toggles[0]); // setUsePhishDetectStub - fireEvent.click(toggles[1]); // setUse4ByteResolutionStub - fireEvent.click(toggles[2]); // setPreferenceStub + fireEvent.click(toggles[0]); // setUse4ByteResolutionStub + fireEvent.click(toggles[1]); // setPreferenceStub fireEvent.click(backButton); @@ -179,8 +174,6 @@ describe('Privacy Settings Onboarding View', () => { false, ); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); - expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(1); expect(setUse4ByteResolutionStub.mock.calls[0][0]).toStrictEqual(false); expect(setPreferenceStub).toHaveBeenCalledTimes(1); From d9d6fab1be802871131ce772a570ee7b0a81f4e9 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari <nidhi.kumari@consensys.net> Date: Mon, 14 Oct 2024 16:20:57 +0100 Subject: [PATCH 132/226] fix: no connected state for permissions page (#27660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to add padding to the permissions page when no site or snap is connected ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to All permissions page 2. Disconnect all sites and snaps 3. Check the copy is center aligned when not connected ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-07 at 2 02 28 PM](https://github.com/user-attachments/assets/b3a46aba-a5f0-4ad3-a1dc-4e59d6d70c88) ### **After** ![Screenshot 2024-10-07 at 2 02 10 PM](https://github.com/user-attachments/assets/07ccf534-3c8e-4ee4-904a-77c6ed0a606a) ## **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. --- .../multichain/pages/permissions-page/permissions-page.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.js b/ui/components/multichain/pages/permissions-page/permissions-page.js index 491e041d7ac5..8cdeae0ed57d 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.js @@ -120,6 +120,7 @@ export const PermissionsPage = () => { justifyContent={JustifyContent.center} height={BlockSize.Full} gap={2} + padding={4} > <Text variant={TextVariant.bodyMdMedium} From 90873fd37ee36ccd03085aba27ff47c39a023b77 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari <nidhi.kumari@consensys.net> Date: Mon, 14 Oct 2024 16:36:42 +0100 Subject: [PATCH 133/226] feat: Added metrics for edit networks and accounts (#27820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to add metrics for edit networks and accounts screen ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/3263](https://github.com/MetaMask/MetaMask-planning/issues/3263) ## **Manual testing steps** 1. Go to ui/contexts/metametrics.js 2. In trackEvent in this file, add a log to see the payload 3. Run extension with yarn start. Go to Permissions Page, click on edit button for accounts and networks and check the console to verify the payload. In edit modals, if we update accounts or networks. Check the payload as well ## **Screenshots/Recordings** ### **Before** NA ### **After** NA ## **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 | 6 ++++ .../edit-accounts-modal.tsx | 34 ++++++++++++++++++- .../edit-networks-modal.js | 31 +++++++++++++++-- .../site-cell/site-cell.tsx | 31 ++++++++++++++--- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index b3e6f252d23d..544d24ce1271 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -562,6 +562,10 @@ export enum MetaMetricsEventName { NavConnectedSitesOpened = 'Connected Sites Opened', NavMainMenuOpened = 'Main Menu Opened', NavPermissionsOpened = 'Permissions Opened', + UpdatePermissionedNetworks = 'Update Permissioned Networks', + UpdatePermissionedAccounts = 'Update Permissioned Accounts', + ViewPermissionedNetworks = 'View Permissioned Networks', + ViewPermissionedAccounts = 'View Permissioned Accounts', NavNetworkMenuOpened = 'Network Menu Opened', NavSettingsOpened = 'Settings Opened', NavAccountSwitched = 'Account Switched', @@ -782,6 +786,8 @@ export enum MetaMetricsEventCategory { NotificationsActivationFlow = 'Notifications Activation Flow', NotificationSettings = 'Notification Settings', Petnames = 'Petnames', + // eslint-disable-next-line @typescript-eslint/no-shadow + Permissions = 'Permissions', Phishing = 'Phishing', ProfileSyncing = 'Profile Syncing', PushNotifications = 'Notifications', 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 d9303951af2d..ba842efc6a11 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { Modal, @@ -30,6 +30,11 @@ import { } from '../../../helpers/constants/design-system'; import { getURLHost } from '../../../helpers/utils/util'; import { MergedInternalAccount } from '../../../selectors/selectors.types'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; type EditAccountsModalProps = { activeTabOrigin: string; @@ -47,6 +52,8 @@ export const EditAccountsModal: React.FC<EditAccountsModalProps> = ({ onSubmit, }) => { const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const [showAddNewAccounts, setShowAddNewAccounts] = useState(false); const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( @@ -85,6 +92,9 @@ export const EditAccountsModal: React.FC<EditAccountsModalProps> = ({ const hostName = getURLHost(activeTabOrigin); + const defaultSet = new Set(defaultSelectedAccountAddresses); + const selectedSet = new Set(selectedAccountAddresses); + return ( <> <Modal @@ -181,7 +191,29 @@ export const EditAccountsModal: React.FC<EditAccountsModalProps> = ({ <ButtonPrimary data-testid="connect-more-accounts-button" onClick={() => { + // 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} 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 8f599d149689..e4a7c391b4df 100644 --- a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js +++ b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { AlignItems, @@ -28,6 +28,11 @@ import { import { NetworkListItem } from '..'; import { getURLHost } from '../../../helpers/utils/util'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; export const EditNetworksModal = ({ activeTabOrigin, @@ -38,7 +43,7 @@ export const EditNetworksModal = ({ onSubmit, }) => { const t = useI18nContext(); - + const trackEvent = useContext(MetaMetricsContext); const allNetworks = [...nonTestNetworks, ...testNetworks]; const [selectedChainIds, setSelectedChainIds] = useState( @@ -77,6 +82,9 @@ export const EditNetworksModal = ({ const hostName = getURLHost(activeTabOrigin); + const defaultChainIdsSet = new Set(defaultSelectedChainIds); + const selectedChainIdsSet = new Set(selectedChainIds); + return ( <Modal isOpen @@ -180,6 +188,25 @@ export const EditNetworksModal = ({ data-testid="connect-more-chains-button" onClick={() => { onSubmit(selectedChainIds); + // Get networks that are in `selectedChainIds` but not in `defaultSelectedChainIds` + const addedNetworks = selectedChainIds.filter( + (chainId) => !defaultChainIdsSet.has(chainId), + ); + + // Get networks that are in `defaultSelectedChainIds` but not in `selectedChainIds` + const removedNetworks = defaultSelectedChainIds.filter( + (chainId) => !selectedChainIdsSet.has(chainId), + ); + + trackEvent({ + category: MetaMetricsEventCategory.Permissions, + event: MetaMetricsEventName.UpdatePermissionedNetworks, + properties: { + addedNetworks: addedNetworks.length, + removedNetworks: removedNetworks.length, + location: 'Edit Networks Modal', + }, + }); onClose(); }} size={ButtonPrimarySize.Lg} 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 4bc42604adf3..bb3a14a8f5e8 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 @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { Hex } from '@metamask/utils'; import { BackgroundColor, @@ -14,6 +14,11 @@ import { } from '../../../../component-library'; import { EditAccountsModal, EditNetworksModal } from '../../..'; import { MergedInternalAccount } from '../../../../../selectors/selectors.types'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../../../shared/constants/metametrics'; import { SiteCellTooltip } from './site-cell-tooltip'; import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; @@ -47,7 +52,7 @@ export const SiteCell: React.FC<SiteCellProps> = ({ isConnectFlow, }) => { const t = useI18nContext(); - + const trackEvent = useContext(MetaMetricsContext); const allNetworks = [...nonTestNetworks, ...testNetworks]; const [showEditAccountsModal, setShowEditAccountsModal] = useState(false); @@ -90,7 +95,16 @@ export const SiteCell: React.FC<SiteCellProps> = ({ connectedMessage={accountMessageConnectedState} unconnectedMessage={accountMessageNotConnectedState} isConnectFlow={isConnectFlow} - onClick={() => setShowEditAccountsModal(true)} + onClick={() => { + setShowEditAccountsModal(true); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'Connect view, Permissions toast, Permissions (dapp)', + }, + }); + }} paddingBottomValue={2} paddingTopValue={0} content={ @@ -114,7 +128,16 @@ export const SiteCell: React.FC<SiteCellProps> = ({ ])} unconnectedMessage={t('requestingFor')} isConnectFlow={isConnectFlow} - onClick={() => setShowEditNetworksModal(true)} + onClick={() => { + setShowEditNetworksModal(true); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'Connect view, Permissions toast, Permissions (dapp)', + }, + }); + }} paddingTopValue={2} paddingBottomValue={0} content={<SiteCellTooltip networks={selectedNetworks} />} From acbb17e26382bbd86f3407ac85c5ff2e38a75034 Mon Sep 17 00:00:00 2001 From: jiexi <jiexiluan@gmail.com> Date: Mon, 14 Oct 2024 09:53:18 -0700 Subject: [PATCH 134/226] revert: use networkClientId to resolve chainId in PPOM Middleware (#27570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Reverts [this PPOM change](https://github.com/MetaMask/metamask-extension/pull/27263) due to [issue with network configuration for the newly added rpc endpoint not being available when queried immediately after being added](https://github.com/MetaMask/metamask-extension/issues/27447) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27570?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/ppom/ppom-middleware.test.ts | 139 +++++++++++-------- app/scripts/lib/ppom/ppom-middleware.ts | 21 ++- app/scripts/metamask-controller.js | 1 - 3 files changed, 89 insertions(+), 72 deletions(-) diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index aafde70f2072..d0adbefb264b 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -8,6 +8,7 @@ import { BlockaidResultType, } from '../../../../shared/constants/security-provider'; import { flushPromises } from '../../../../test/lib/timer-helpers'; +import { mockNetworkState } from '../../../../test/stub/networks'; import { createPPOMMiddleware, PPOMMiddlewareRequest } from './ppom-middleware'; import { generateSecurityAlertId, @@ -36,18 +37,22 @@ const REQUEST_MOCK = { params: [], id: '', jsonrpc: '2.0' as const, - origin: 'test.com', - networkClientId: 'networkClientId', }; const createMiddleware = ( options: { - chainId?: Hex; + chainId?: Hex | null; error?: Error; securityAlertsEnabled?: boolean; - } = {}, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateSecurityAlertResponse?: any; + } = { + updateSecurityAlertResponse: () => undefined, + }, ) => { - const { chainId, error, securityAlertsEnabled } = options; + const { chainId, error, securityAlertsEnabled, updateSecurityAlertResponse } = + options; const ppomController = {}; @@ -66,9 +71,10 @@ const createMiddleware = ( } const networkController = { - getNetworkConfigurationByNetworkClientId: jest - .fn() - .mockReturnValue({ chainId: chainId || CHAIN_IDS.MAINNET }), + state: { + ...mockNetworkState({ chainId: chainId || CHAIN_IDS.MAINNET }), + ...(chainId === null ? { providerConfig: {} } : undefined), + }, }; const appStateController = { @@ -79,9 +85,7 @@ const createMiddleware = ( listAccounts: () => [{ address: INTERNAL_ACCOUNT_ADDRESS }], }; - const updateSecurityAlertResponse = jest.fn(); - - const middleware = createPPOMMiddleware( + return createPPOMMiddleware( // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any ppomController as any, @@ -98,16 +102,6 @@ const createMiddleware = ( accountsController as any, updateSecurityAlertResponse, ); - - return { - middleware, - ppomController, - preferenceController, - networkController, - appStateController, - accountsController, - updateSecurityAlertResponse, - }; }; describe('PPOMMiddleware', () => { @@ -135,29 +129,12 @@ describe('PPOMMiddleware', () => { }; }); - it('gets the network configuration for the request networkClientId', async () => { - const { middleware, networkController } = createMiddleware(); - - const req = { - ...REQUEST_MOCK, - method: 'eth_sendTransaction', - securityAlertResponse: undefined, - }; - - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); - - await flushPromises(); - - expect( - networkController.getNetworkConfigurationByNetworkClientId, - ).toHaveBeenCalledTimes(1); - expect( - networkController.getNetworkConfigurationByNetworkClientId, - ).toHaveBeenCalledWith('networkClientId'); - }); - it('updates alert response after validating request', async () => { - const { middleware, updateSecurityAlertResponse } = createMiddleware(); + const updateSecurityAlertResponse = jest.fn(); + + const middlewareFunction = createMiddleware({ + updateSecurityAlertResponse, + }); const req = { ...REQUEST_MOCK, @@ -165,7 +142,11 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); await flushPromises(); @@ -178,7 +159,7 @@ describe('PPOMMiddleware', () => { }); it('adds loading response to confirmation requests while validation is in progress', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const req: PPOMMiddlewareRequest<(string | { to: string })[]> = { ...REQUEST_MOCK, @@ -186,7 +167,11 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse?.reason).toBe(BlockaidReason.inProgress); expect(req.securityAlertResponse?.result_type).toBe( @@ -195,7 +180,7 @@ describe('PPOMMiddleware', () => { }); it('does not do validation if the user has not enabled the preference', async () => { - const { middleware } = createMiddleware({ + const middlewareFunction = createMiddleware({ securityAlertsEnabled: false, }); @@ -206,7 +191,29 @@ describe('PPOMMiddleware', () => { }; // @ts-expect-error Passing in invalid input for testing purposes - await middleware(req, undefined, () => undefined); + await middlewareFunction(req, undefined, () => undefined); + + expect(req.securityAlertResponse).toBeUndefined(); + expect(validateRequestWithPPOM).not.toHaveBeenCalled(); + }); + + it('does not do validation if unable to get the chainId from the network provider config', async () => { + isChainSupportedMock.mockResolvedValue(false); + const middlewareFunction = createMiddleware({ + chainId: null, + }); + + const req = { + ...REQUEST_MOCK, + method: 'eth_sendTransaction', + securityAlertResponse: undefined, + }; + + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); @@ -214,7 +221,7 @@ describe('PPOMMiddleware', () => { it('does not do validation if user is not on a supported network', async () => { isChainSupportedMock.mockResolvedValue(false); - const { middleware } = createMiddleware({ + const middlewareFunction = createMiddleware({ chainId: '0x2', }); @@ -224,14 +231,18 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('does not do validation when request is not for confirmation method', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const req = { ...REQUEST_MOCK, @@ -239,14 +250,18 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('does not do validation when request is send to users own account', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const req = { ...REQUEST_MOCK, @@ -255,14 +270,18 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('does not do validation for SIWE signature', async () => { - const { middleware } = createMiddleware({ + const middlewareFunction = createMiddleware({ securityAlertsEnabled: true, }); @@ -283,17 +302,17 @@ describe('PPOMMiddleware', () => { detectSIWEMock.mockReturnValue({ isSIWEMessage: true } as SIWEMessage); // @ts-expect-error Passing invalid input for testing purposes - await middleware(req, undefined, () => undefined); + await middlewareFunction(req, undefined, () => undefined); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('calls next method', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const nextMock = jest.fn(); - await middleware( + await middlewareFunction( { ...REQUEST_MOCK, method: 'eth_sendTransaction' }, { ...JsonRpcResponseStruct.TYPE }, nextMock, @@ -308,7 +327,7 @@ describe('PPOMMiddleware', () => { const nextMock = jest.fn(); - const { middleware } = createMiddleware({ error }); + const middlewareFunction = createMiddleware({ error }); const req = { ...REQUEST_MOCK, @@ -316,7 +335,7 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, nextMock); + await middlewareFunction(req, { ...JsonRpcResponseStruct.TYPE }, nextMock); expect(req.securityAlertResponse).toStrictEqual( SECURITY_ALERT_RESPONSE_MOCK, diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 5b9107337a05..1bad576e3881 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -1,9 +1,6 @@ import { AccountsController } from '@metamask/accounts-controller'; import { PPOMController } from '@metamask/ppom-validator'; -import { - NetworkClientId, - NetworkController, -} from '@metamask/network-controller'; +import { NetworkController } from '@metamask/network-controller'; import { Json, JsonRpcParams, @@ -17,6 +14,8 @@ import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import PreferencesController from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state'; 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'; import { trace, TraceContext, TraceName } from '../../../../shared/lib/trace'; import { generateSecurityAlertId, @@ -35,7 +34,6 @@ const CONFIRMATION_METHODS = Object.freeze([ export type PPOMMiddlewareRequest< Params extends JsonRpcParams = JsonRpcParams, > = Required<JsonRpcRequest<Params>> & { - networkClientId: NetworkClientId; securityAlertResponse?: SecurityAlertResponse | undefined; traceContext?: TraceContext; }; @@ -81,13 +79,14 @@ export function createPPOMMiddleware< const securityAlertsEnabled = preferencesController.store.getState()?.securityAlertsEnabled; - // This will always exist as the SelectedNetworkMiddleware - // adds networkClientId to the request before this middleware runs const { chainId } = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - networkController.getNetworkConfigurationByNetworkClientId( - req.networkClientId, - )!; + getProviderConfig({ + metamask: networkController.state, + }) ?? {}; + if (!chainId) { + return; + } + const isSupportedChain = await isChainSupported(chainId); if ( diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 83edd7ade418..98af29fe38f1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -5624,7 +5624,6 @@ export default class MetamaskController extends EventEmitter { engine.push(createTracingMiddleware()); - // PPOMMiddleware come after the SelectedNetworkMiddleware engine.push( createPPOMMiddleware( this.ppomController, From cedabc62e45601c77871689425320c54d717275e Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane <kanthesha.devaramane@consensys.net> Date: Mon, 14 Oct 2024 18:29:15 +0100 Subject: [PATCH 135/226] feat: preferences controller to base controller v2 (#27398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** In this PR, we want to bring PreferencesController up to date with our latest controller patterns by upgrading to BaseControllerV2. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27398?quickstart=1) ## **Related issues** Fixes: #25917 ## **Manual testing steps** Use case 1 1. Install the previous release 2. Complete user onboarding 3. Go to settings and change couple of user settings. For example language, currency and theme. 4. Close and disable MM in the extension 5. Checkout the version with these changes 6. Build and login 7. Make sure, the user preferences set earlier are still there Use case 2 1. Disable all the MM extensions 2. Install the version with these changes 3. When you click on MM, the default language should be English 4. Complete user onboarding 5. Go to settings and change couple of user settings. For example language, currency and theme. 6. Close and disable and enable the MM in extension. which forces user to login MM in the extension 7. Once you login again, make sure, the user preferences set earlier are still there ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com> Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> --- .eslintrc.js | 2 +- app/scripts/background.js | 5 +- .../account-tracker-controller.test.ts | 11 +- .../controllers/account-tracker-controller.ts | 18 +- app/scripts/controllers/app-state.js | 23 +- app/scripts/controllers/app-state.test.js | 7 +- app/scripts/controllers/metametrics.js | 17 +- app/scripts/controllers/metametrics.test.js | 49 +- .../controllers/mmi-controller.test.ts | 13 +- app/scripts/controllers/mmi-controller.ts | 2 +- .../preferences-controller.test.ts | 653 ++++++++++++------ .../controllers/preferences-controller.ts | 642 +++++++++++------ app/scripts/lib/backup.js | 6 +- app/scripts/lib/backup.test.js | 24 +- .../createRPCMethodTrackingMiddleware.test.js | 10 +- app/scripts/lib/ppom/ppom-middleware.test.ts | 14 +- app/scripts/lib/ppom/ppom-middleware.ts | 5 +- app/scripts/metamask-controller.js | 156 ++--- app/scripts/metamask-controller.test.js | 26 +- .../files-to-convert.json | 2 - lavamoat/browserify/beta/policy.json | 6 + lavamoat/browserify/flask/policy.json | 6 + lavamoat/browserify/main/policy.json | 6 + lavamoat/browserify/mmi/policy.json | 6 + package.json | 1 + shared/constants/mmi-controller.ts | 2 +- test/e2e/default-fixture.js | 26 + test/e2e/fixture-builder.js | 28 + ...rs-after-init-opt-in-background-state.json | 4 +- .../errors-after-init-opt-in-ui-state.json | 2 + ...s-before-init-opt-in-background-state.json | 4 +- .../errors-before-init-opt-in-ui-state.json | 4 +- yarn.lock | 13 + 33 files changed, 1147 insertions(+), 646 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 64c51bd1e503..a53619b179ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -316,7 +316,7 @@ module.exports = { 'app/scripts/controllers/swaps/**/*.test.ts', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', - 'app/scripts/controllers/preferences.test.js', + 'app/scripts/controllers/preferences-controller.test.ts', 'app/scripts/lib/**/*.test.js', 'app/scripts/metamask-controller.test.js', 'app/scripts/migrations/*.test.js', diff --git a/app/scripts/background.js b/app/scripts/background.js index 4fbbee449160..7d9d0f5684a6 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -235,7 +235,7 @@ function maybeDetectPhishing(theController) { return {}; } - const prefState = theController.preferencesController.store.getState(); + const prefState = theController.preferencesController.state; if (!prefState.usePhishDetect) { return {}; } @@ -758,8 +758,7 @@ export function setupController( controller.preferencesController, ), getUseAddressBarEnsResolution: () => - controller.preferencesController.store.getState() - .useAddressBarEnsResolution, + controller.preferencesController.state.useAddressBarEnsResolution, provider: controller.provider, }); diff --git a/app/scripts/controllers/account-tracker-controller.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts index dbabb927fa71..ad33541fb5b6 100644 --- a/app/scripts/controllers/account-tracker-controller.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -5,7 +5,6 @@ import { BlockTracker, Provider } from '@metamask/network-controller'; import { flushPromises } from '../../../test/lib/timer-helpers'; import { createTestProviderTools } from '../../../test/stub/provider'; -import PreferencesController from './preferences-controller'; import type { AccountTrackerControllerOptions, AllowedActions, @@ -166,13 +165,9 @@ function withController<ReturnValue>( provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), - preferencesController: { - store: { - getState: () => ({ - useMultiAccountBalanceChecker, - }), - }, - } as PreferencesController, + preferencesControllerState: { + useMultiAccountBalanceChecker, + }, messenger: controllerMessenger.getRestricted({ name: 'AccountTrackerController', allowedActions: [ diff --git a/app/scripts/controllers/account-tracker-controller.ts b/app/scripts/controllers/account-tracker-controller.ts index e2c78ea3f3f9..ec4789189a0c 100644 --- a/app/scripts/controllers/account-tracker-controller.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -45,7 +45,7 @@ import type { OnboardingControllerGetStateAction, OnboardingControllerStateChangeEvent, } from './onboarding'; -import PreferencesController from './preferences-controller'; +import { PreferencesControllerState } from './preferences-controller'; // Unique name for the controller const controllerName = 'AccountTrackerController'; @@ -170,7 +170,7 @@ export type AccountTrackerControllerOptions = { provider: Provider; blockTracker: BlockTracker; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; - preferencesController: PreferencesController; + preferencesControllerState: Partial<PreferencesControllerState>; }; /** @@ -198,7 +198,7 @@ export default class AccountTrackerController extends BaseController< #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesController: AccountTrackerControllerOptions['preferencesController']; + #preferencesControllerState: AccountTrackerControllerOptions['preferencesControllerState']; #selectedAccount: InternalAccount; @@ -209,7 +209,7 @@ export default class AccountTrackerController extends BaseController< * @param options.provider - An EIP-1193 provider instance that uses the current global network * @param options.blockTracker - A block tracker, which emits events for each new block * @param options.getNetworkIdentifier - A function that returns the current network or passed network configuration - * @param options.preferencesController - The preferences controller + * @param options.preferencesControllerState - The state of preferences controller */ constructor(options: AccountTrackerControllerOptions) { super({ @@ -226,7 +226,7 @@ export default class AccountTrackerController extends BaseController< this.#blockTracker = options.blockTracker; this.#getNetworkIdentifier = options.getNetworkIdentifier; - this.#preferencesController = options.preferencesController; + this.#preferencesControllerState = options.preferencesControllerState; // subscribe to account removal this.messagingSystem.subscribe( @@ -257,7 +257,7 @@ export default class AccountTrackerController extends BaseController< 'AccountsController:selectedEvmAccountChange', (newAccount) => { const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + this.#preferencesControllerState; if ( this.#selectedAccount.id !== newAccount.id && @@ -672,8 +672,7 @@ export default class AccountTrackerController extends BaseController< const { chainId, provider, identifier } = this.#getCorrectNetworkClient(networkClientId); - const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; let addresses = []; if (useMultiAccountBalanceChecker) { @@ -724,8 +723,7 @@ export default class AccountTrackerController extends BaseController< provider: Provider, chainId: Hex, ): Promise<void> { - const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; let balance = '0x0'; diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 3d8f9d176fb6..9dabf2313e57 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -29,7 +29,7 @@ export default class AppStateController extends EventEmitter { isUnlocked, initState, onInactiveTimeout, - preferencesStore, + preferencesController, messenger, extension, } = opts; @@ -86,12 +86,18 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock = []; addUnlockListener(this.handleUnlock.bind(this)); - preferencesStore.subscribe(({ preferences }) => { - const currentState = this.store.getState(); - if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { - this._setInactiveTimeout(preferences.autoLockTimeLimit); - } - }); + messenger.subscribe( + 'PreferencesController:stateChange', + ({ preferences }) => { + const currentState = this.store.getState(); + if ( + preferences && + currentState.timeoutMinutes !== preferences.autoLockTimeLimit + ) { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + }, + ); messenger.subscribe( 'KeyringController:qrKeyringStateChange', @@ -101,7 +107,8 @@ export default class AppStateController extends EventEmitter { }), ); - const { preferences } = preferencesStore.getState(); + const { preferences } = preferencesController.state; + this._setInactiveTimeout(preferences.autoLockTimeLimit); this.messagingSystem = messenger; diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js index c9ce8243b05c..46fe87d29add 100644 --- a/app/scripts/controllers/app-state.test.js +++ b/app/scripts/controllers/app-state.test.js @@ -13,13 +13,12 @@ describe('AppStateController', () => { initState, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + preferencesController: { + state: { preferences: { autoLockTimeLimit: 0, }, - })), + }, }, messenger: { call: jest.fn(() => ({ diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 15f4fa9b7788..aa5546ef7899 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -118,8 +118,9 @@ export default class MetaMetricsController { * @param {object} options * @param {object} options.segment - an instance of analytics for tracking * events that conform to the new MetaMetrics tracking plan. - * @param {object} options.preferencesStore - The preferences controller store, used - * to access and subscribe to preferences that will be attached to events + * @param {object} options.preferencesControllerState - The state of preferences controller + * @param {Function} options.onPreferencesStateChange - Used to attach a listener to the + * stateChange event emitted by the PreferencesController * @param {Function} options.onNetworkDidChange - Used to attach a listener to the * networkDidChange event emitted by the networkController * @param {Function} options.getCurrentChainId - Gets the current chain id from the @@ -132,7 +133,8 @@ export default class MetaMetricsController { */ constructor({ segment, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, onNetworkDidChange, getCurrentChainId, version, @@ -148,16 +150,15 @@ export default class MetaMetricsController { captureException(err); } }; - const prefState = preferencesStore.getState(); this.chainId = getCurrentChainId(); - this.locale = prefState.currentLocale.replace('_', '-'); + this.locale = preferencesControllerState.currentLocale.replace('_', '-'); this.version = environment === 'production' ? version : `${version}-${environment}`; this.extension = extension; this.environment = environment; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - this.selectedAddress = prefState.selectedAddress; + this.selectedAddress = preferencesControllerState.selectedAddress; ///: END:ONLY_INCLUDE_IF const abandonedFragments = omitBy(initState?.fragments, 'persist'); @@ -181,8 +182,8 @@ export default class MetaMetricsController { }, }); - preferencesStore.subscribe(({ currentLocale }) => { - this.locale = currentLocale.replace('_', '-'); + onPreferencesStateChange(({ currentLocale }) => { + this.locale = currentLocale?.replace('_', '-'); }); onNetworkDidChange(() => { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index a0505700ef01..ca5602de33c8 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -74,22 +74,6 @@ const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, }; -function getMockPreferencesStore({ currentLocale = LOCALE } = {}) { - let preferencesStore = { - currentLocale, - }; - const subscribe = jest.fn(); - const updateState = (newState) => { - preferencesStore = { ...preferencesStore, ...newState }; - subscribe.mock.calls[0][0](preferencesStore); - }; - return { - getState: jest.fn().mockReturnValue(preferencesStore), - updateState, - subscribe, - }; -} - const SAMPLE_PERSISTED_EVENT = { id: 'testid', persist: true, @@ -117,7 +101,10 @@ function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, marketingCampaignCookieId = null, - preferencesStore = getMockPreferencesStore(), + preferencesControllerState = { currentLocale: LOCALE }, + onPreferencesStateChange = () => { + // do nothing + }, getCurrentChainId = () => FAKE_CHAIN_ID, onNetworkDidChange = () => { // do nothing @@ -128,7 +115,8 @@ function getMetaMetricsController({ segment: segmentInstance || segment, getCurrentChainId, onNetworkDidChange, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, version: '0.0.1', environment: 'test', initState: { @@ -209,11 +197,16 @@ describe('MetaMetricsController', function () { }); it('should update when preferences changes', function () { - const preferencesStore = getMockPreferencesStore(); + let subscribeListener; + const onPreferencesStateChange = (listener) => { + subscribeListener = listener; + }; const metaMetricsController = getMetaMetricsController({ - preferencesStore, + preferencesControllerState: { currentLocale: LOCALE }, + onPreferencesStateChange, }); - preferencesStore.updateState({ currentLocale: 'en_UK' }); + + subscribeListener({ currentLocale: 'en_UK' }); expect(metaMetricsController.locale).toStrictEqual('en-UK'); }); }); @@ -732,9 +725,11 @@ describe('MetaMetricsController', function () { it('should track a page view if isOptInPath is true and user not yet opted in', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -746,6 +741,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith( { @@ -765,9 +761,11 @@ describe('MetaMetricsController', function () { it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -790,6 +788,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith( { diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 348ccd40916b..dbef190a5573 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -203,10 +203,10 @@ describe('MMIController', function () { }); metaMetricsController = new MetaMetricsController({ - preferencesStore: { - getState: jest.fn().mockReturnValue({ currentLocale: 'en' }), - subscribe: jest.fn(), + preferencesControllerState: { + currentLocale: 'en' }, + onPreferencesStateChange: jest.fn(), getCurrentChainId: jest.fn(), onNetworkDidChange: jest.fn(), }); @@ -245,13 +245,12 @@ describe('MMIController', function () { initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + preferencesController: { + state: { preferences: { autoLockTimeLimit: 0, }, - })), + }, }, messenger: mockMessenger, }), diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index d0e905d673d8..2373484d4a6e 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -44,8 +44,8 @@ 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 PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; type UpdateCustodianTransactionsParameters = { diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index f825c1eb5aee..9c28ed7c43a0 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -3,13 +3,7 @@ */ import { ControllerMessenger } from '@metamask/base-controller'; import { AccountsController } from '@metamask/accounts-controller'; -import { - KeyringControllerGetAccountsAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetKeyringForAccountAction, - KeyringControllerStateChangeEvent, - KeyringControllerAccountRemovedEvent, -} from '@metamask/keyring-controller'; +import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { Hex } from '@metamask/utils'; import { CHAIN_IDS } from '../../../shared/constants/network'; @@ -18,10 +12,10 @@ import { ThemeType } from '../../../shared/constants/preferences'; import type { AllowedActions, AllowedEvents, - PreferencesControllerActions, - PreferencesControllerEvents, + PreferencesControllerMessenger, + PreferencesControllerState, } from './preferences-controller'; -import PreferencesController from './preferences-controller'; +import { PreferencesController } from './preferences-controller'; const NETWORK_CONFIGURATION_DATA = mockNetworkState( { @@ -40,102 +34,104 @@ const NETWORK_CONFIGURATION_DATA = mockNetworkState( }, ).networkConfigurationsByChainId; -describe('preferences controller', () => { - let controllerMessenger: ControllerMessenger< - | PreferencesControllerActions - | AllowedActions - | KeyringControllerGetAccountsAction - | KeyringControllerGetKeyringsByTypeAction - | KeyringControllerGetKeyringForAccountAction, - | PreferencesControllerEvents +const setupController = ({ + state, +}: { + state?: Partial<PreferencesControllerState>; +}) => { + const controllerMessenger = new ControllerMessenger< + AllowedActions, + | AllowedEvents | KeyringControllerStateChangeEvent - | KeyringControllerAccountRemovedEvent | SnapControllerStateChangeEvent - | AllowedEvents - >; - let preferencesController: PreferencesController; - let accountsController: AccountsController; - - beforeEach(() => { - controllerMessenger = new ControllerMessenger(); - - const accountsControllerMessenger = controllerMessenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); - - const mockAccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }; - accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: mockAccountsControllerState, - }); - - const preferencesMessenger = controllerMessenger.getRestricted({ + >(); + const preferencesControllerMessenger: PreferencesControllerMessenger = + controllerMessenger.getRestricted({ name: 'PreferencesController', allowedActions: [ - `AccountsController:setSelectedAccount`, - `AccountsController:getAccountByAddress`, - `AccountsController:setAccountName`, + 'AccountsController:getAccountByAddress', + 'AccountsController:setAccountName', + 'AccountsController:getSelectedAccount', + 'AccountsController:setSelectedAccount', + 'NetworkController:getState', ], - allowedEvents: [`AccountsController:stateChange`], + allowedEvents: ['AccountsController:stateChange'], }); - preferencesController = new PreferencesController({ - initLangCode: 'en_US', + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue({ networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - messenger: preferencesMessenger, - }); + }), + ); + const controller = new PreferencesController({ + messenger: preferencesControllerMessenger, + state, + }); + + const accountsControllerMessenger = controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'KeyringController:stateChange', + 'SnapController:stateChange', + ], + allowedActions: [], }); + const mockAccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; + const accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: mockAccountsControllerState, + }); + + return { + controller, + messenger: controllerMessenger, + accountsController, + }; +}; +describe('preferences controller', () => { describe('useBlockie', () => { it('defaults useBlockie to false', () => { - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - false, - ); + const { controller } = setupController({}); + expect(controller.state.useBlockie).toStrictEqual(false); }); it('setUseBlockie to true', () => { - preferencesController.setUseBlockie(true); - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - true, - ); + const { controller } = setupController({}); + controller.setUseBlockie(true); + expect(controller.state.useBlockie).toStrictEqual(true); }); }); describe('setCurrentLocale', () => { it('checks the default currentLocale', () => { - const { currentLocale } = preferencesController.store.getState(); - expect(currentLocale).toStrictEqual('en_US'); + const { controller } = setupController({}); + const { currentLocale } = controller.state; + expect(currentLocale).toStrictEqual(''); }); it('sets current locale in preferences controller', () => { - preferencesController.setCurrentLocale('ja'); - const { currentLocale } = preferencesController.store.getState(); + const { controller } = setupController({}); + controller.setCurrentLocale('ja'); + const { currentLocale } = controller.state; expect(currentLocale).toStrictEqual('ja'); }); }); describe('setAccountLabel', () => { + const { controller, messenger, accountsController } = setupController({}); const mockName = 'mockName'; const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; it('updating name from preference controller will update the name in accounts controller and preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -150,21 +146,20 @@ describe('preferences controller', () => { ); let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; expect(firstAccount.metadata.name).toBe(firstPreferenceAccount.name); expect(secondAccount.metadata.name).toBe(secondPreferenceAccount.name); - preferencesController.setAccountLabel(firstAccount.address, mockName); + controller.setAccountLabel(firstAccount.address, mockName); // refresh state after state changed [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -181,7 +176,7 @@ describe('preferences controller', () => { }); it('updating name from accounts controller updates the name in preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -197,7 +192,7 @@ describe('preferences controller', () => { let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; @@ -210,8 +205,7 @@ describe('preferences controller', () => { [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -229,10 +223,11 @@ describe('preferences controller', () => { }); describe('setSelectedAddress', () => { + const { controller, messenger, accountsController } = setupController({}); it('updating selectedAddress from preferences controller updates the selectedAccount in accounts controller and preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -248,25 +243,26 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); - preferencesController.setSelectedAddress(secondAddress); + controller.setSelectedAddress(secondAddress); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); expect(updatedSelectedAddress).toBe(updatedSelectedAccount.address); + + expect(controller.getSelectedAddress()).toBe(secondAddress); }); it('updating selectedAccount from accounts controller updates the selectedAddress in preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -283,15 +279,14 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); const accounts = accountsController.listAccounts(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); accountsController.setSelectedAccount(accounts[1].id); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); @@ -300,173 +295,142 @@ describe('preferences controller', () => { }); describe('setPasswordForgotten', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(false); + expect(controller.state.forgottenPassword).toStrictEqual(false); }); it('should set the forgottenPassword property in state', () => { - preferencesController.setPasswordForgotten(true); - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(true); + controller.setPasswordForgotten(true); + expect(controller.state.forgottenPassword).toStrictEqual(true); }); }); describe('setUsePhishDetect', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); }); it('should set the usePhishDetect property in state', () => { - preferencesController.setUsePhishDetect(false); - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(false); + controller.setUsePhishDetect(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); }); }); describe('setUseMultiAccountBalanceChecker', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(true); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + true, + ); }); it('should set the setUseMultiAccountBalanceChecker property in state', () => { - preferencesController.setUseMultiAccountBalanceChecker(false); - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(false); + controller.setUseMultiAccountBalanceChecker(false); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + false, + ); }); }); describe('isRedesignedConfirmationsFeatureEnabled', () => { + const { controller } = setupController({}); it('isRedesignedConfirmationsFeatureEnabled should default to false', () => { expect( - preferencesController.store.getState().preferences - .isRedesignedConfirmationsDeveloperEnabled, + controller.state.preferences.isRedesignedConfirmationsDeveloperEnabled, ).toStrictEqual(false); }); }); describe('setUseSafeChainsListValidation', function () { + const { controller } = setupController({}); it('should default to true', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useSafeChainsListValidation).toStrictEqual(true); }); it('should set the `setUseSafeChainsListValidation` property in state', function () { - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(true); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(true); - preferencesController.setUseSafeChainsListValidation(false); + controller.setUseSafeChainsListValidation(false); - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(false); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(false); }); }); describe('setUseTokenDetection', function () { + const { controller } = setupController({}); it('should default to true for new users', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useTokenDetection).toStrictEqual(true); }); it('should set the useTokenDetection property in state', () => { - preferencesController.setUseTokenDetection(true); - expect( - preferencesController.store.getState().useTokenDetection, - ).toStrictEqual(true); + controller.setUseTokenDetection(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); }); it('should keep initial value of useTokenDetection for existing users', function () { - // TODO: Remove unregisterActionHandler and clearEventSubscriptions once the PreferencesController has been refactored to use the withController pattern. - controllerMessenger.unregisterActionHandler( - 'PreferencesController:getState', - ); - controllerMessenger.clearEventSubscriptions( - 'PreferencesController:stateChange', - ); - const preferencesControllerExistingUser = new PreferencesController({ - messenger: controllerMessenger.getRestricted({ - name: 'PreferencesController', - allowedActions: [], - allowedEvents: ['AccountsController:stateChange'], - }), - initLangCode: 'en_US', - initState: { - useTokenDetection: false, + const { controller: preferencesControllerExistingUser } = setupController( + { + state: { + useTokenDetection: false, + }, }, - networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - }); - const state = preferencesControllerExistingUser.store.getState(); + ); + const { state } = preferencesControllerExistingUser; expect(state.useTokenDetection).toStrictEqual(false); }); }); describe('setUseNftDetection', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); it('should set the useNftDetection property in state', () => { - preferencesController.setOpenSeaEnabled(true); - preferencesController.setUseNftDetection(true); - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + controller.setUseNftDetection(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); }); describe('setUse4ByteResolution', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(true); + expect(controller.state.use4ByteResolution).toStrictEqual(true); }); it('should set the use4ByteResolution property in state', () => { - preferencesController.setUse4ByteResolution(false); - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(false); + controller.setUse4ByteResolution(false); + expect(controller.state.use4ByteResolution).toStrictEqual(false); }); }); describe('setOpenSeaEnabled', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); it('should set the openSeaEnabled property in state', () => { - preferencesController.setOpenSeaEnabled(true); - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); }); describe('setAdvancedGasFee', () => { + const { controller } = setupController({}); it('should default to an empty object', () => { - expect( - preferencesController.store.getState().advancedGasFee, - ).toStrictEqual({}); + expect(controller.state.advancedGasFee).toStrictEqual({}); }); it('should set the setAdvancedGasFee property in state', () => { - preferencesController.setAdvancedGasFee({ + controller.setAdvancedGasFee({ chainId: CHAIN_IDS.GOERLI, gasFeePreferences: { maxBaseFee: '1.5', @@ -474,51 +438,44 @@ describe('preferences controller', () => { }, }); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .maxBaseFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].maxBaseFee, ).toStrictEqual('1.5'); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .priorityFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].priorityFee, ).toStrictEqual('2'); }); }); describe('setTheme', () => { + const { controller } = setupController({}); it('should default to value "OS"', () => { - expect(preferencesController.store.getState().theme).toStrictEqual('os'); + expect(controller.state.theme).toStrictEqual('os'); }); it('should set the setTheme property in state', () => { - preferencesController.setTheme(ThemeType.dark); - expect(preferencesController.store.getState().theme).toStrictEqual( - 'dark', - ); + controller.setTheme(ThemeType.dark); + expect(controller.state.theme).toStrictEqual('dark'); }); }); describe('setUseCurrencyRateCheck', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); }); it('should set the useCurrencyRateCheck property in state', () => { - preferencesController.setUseCurrencyRateCheck(false); - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(false); + controller.setUseCurrencyRateCheck(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); }); }); describe('setIncomingTransactionsPreferences', () => { + const { controller } = setupController({}); const addedNonTestNetworks = Object.keys(NETWORK_CONFIGURATION_DATA); it('should have default value combined', () => { - const state: { - incomingTransactionsPreferences: Record<string, boolean>; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, @@ -533,13 +490,11 @@ describe('preferences controller', () => { }); it('should update incomingTransactionsPreferences with given value set', () => { - preferencesController.setIncomingTransactionsPreferences( + controller.setIncomingTransactionsPreferences( CHAIN_IDS.LINEA_MAINNET, false, ); - const state: { - incomingTransactionsPreferences: Record<string, boolean>; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: false, @@ -555,10 +510,11 @@ describe('preferences controller', () => { }); describe('AccountsController:stateChange subscription', () => { + const { controller, messenger, accountsController } = setupController({}); it('sync the identities with the accounts in the accounts controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -574,7 +530,7 @@ describe('preferences controller', () => { const accounts = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; expect(accounts.map((account) => account.address)).toStrictEqual( Object.keys(identities), @@ -584,68 +540,313 @@ describe('preferences controller', () => { ///: BEGIN:ONLY_INCLUDE_IF(petnames) describe('setUseExternalNameSources', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the useExternalNameSources property in state', () => { - preferencesController.setUseExternalNameSources(false); - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(false); + controller.setUseExternalNameSources(false); + expect(controller.state.useExternalNameSources).toStrictEqual(false); }); }); ///: END:ONLY_INCLUDE_IF describe('setUseTransactionSimulations', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the setUseTransactionSimulations property in state', () => { - preferencesController.setUseTransactionSimulations(false); - expect( - preferencesController.store.getState().useTransactionSimulations, - ).toStrictEqual(false); + controller.setUseTransactionSimulations(false); + expect(controller.state.useTransactionSimulations).toStrictEqual(false); }); }); describe('setServiceWorkerKeepAlivePreference', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(true); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(true); }); it('should set the setServiceWorkerKeepAlivePreference property in state', () => { - preferencesController.setServiceWorkerKeepAlivePreference(false); - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(false); + controller.setServiceWorkerKeepAlivePreference(false); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(false); }); }); describe('setBitcoinSupportEnabled', () => { + const { controller } = setupController({}); it('has the default value as false', () => { - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); }); it('sets the bitcoinSupportEnabled property in state to true and then false', () => { - preferencesController.setBitcoinSupportEnabled(true); + controller.setBitcoinSupportEnabled(true); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(true); + + controller.setBitcoinSupportEnabled(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); + }); + }); + + describe('useNonceField', () => { + it('defaults useNonceField to false', () => { + const { controller } = setupController({}); + expect(controller.state.useNonceField).toStrictEqual(false); + }); + + it('setUseNonceField to true', () => { + const { controller } = setupController({}); + controller.setUseNonceField(true); + expect(controller.state.useNonceField).toStrictEqual(true); + }); + }); + + describe('globalThis.setPreference', () => { + it('setFeatureFlags to true', () => { + const { controller } = setupController({}); + globalThis.setPreference('showFiatInTestnets', true); + expect(controller.state.featureFlags.showFiatInTestnets).toStrictEqual( + true, + ); + }); + }); + + describe('useExternalServices', () => { + it('defaults useExternalServices to true', () => { + const { controller } = setupController({}); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); + }); + + it('useExternalServices to false', () => { + const { controller } = setupController({}); + controller.toggleExternalServices(false); + expect(controller.state.useExternalServices).toStrictEqual(false); + expect(controller.state.useTokenDetection).toStrictEqual(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + expect(controller.state.openSeaEnabled).toStrictEqual(false); + expect(controller.state.useNftDetection).toStrictEqual(false); + }); + }); + + describe('useRequestQueue', () => { + it('defaults useRequestQueue to true', () => { + const { controller } = setupController({}); + expect(controller.state.useRequestQueue).toStrictEqual(true); + }); + + it('setUseRequestQueue to false', () => { + const { controller } = setupController({}); + controller.setUseRequestQueue(false); + expect(controller.state.useRequestQueue).toStrictEqual(false); + }); + }); + + describe('addSnapAccountEnabled', () => { + it('defaults addSnapAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(false); + }); + + it('setAddSnapAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setAddSnapAccountEnabled(true); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(true); + }); + }); + + describe('watchEthereumAccountEnabled', () => { + it('defaults watchEthereumAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(false); + }); + + it('setWatchEthereumAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setWatchEthereumAccountEnabled(true); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(true); + }); + }); + + describe('bitcoinTestnetSupportEnabled', () => { + it('defaults bitcoinTestnetSupportEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual( + false, + ); + }); + + it('setBitcoinTestnetSupportEnabled to true', () => { + const { controller } = setupController({}); + controller.setBitcoinTestnetSupportEnabled(true); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual(true); + }); + }); + + describe('knownMethodData', () => { + it('defaults knownMethodData', () => { + const { controller } = setupController({}); + expect(controller.state.knownMethodData).toStrictEqual({}); + }); + + it('addKnownMethodData', () => { + const { controller } = setupController({}); + controller.addKnownMethodData('0x60806040', 'testMethodName'); + expect(controller.state.knownMethodData).toStrictEqual({ + '0x60806040': 'testMethodName', + }); + }); + }); + + describe('featureFlags', () => { + it('defaults featureFlags', () => { + const { controller } = setupController({}); + expect(controller.state.featureFlags).toStrictEqual({}); + }); + + it('setFeatureFlags', () => { + const { controller } = setupController({}); + controller.setFeatureFlag('showConfirmationAdvancedDetails', true); expect( - preferencesController.store.getState().bitcoinSupportEnabled, + controller.state.featureFlags.showConfirmationAdvancedDetails, ).toStrictEqual(true); + }); + }); - preferencesController.setBitcoinSupportEnabled(false); - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + describe('preferences', () => { + it('defaults preferences', () => { + const { controller } = setupController({}); + expect(controller.state.preferences).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + + it('setPreference', () => { + const { controller } = setupController({}); + controller.setPreference('showConfirmationAdvancedDetails', true); + expect(controller.getPreferences()).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: true, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + }); + + describe('ipfsGateway', () => { + it('defaults ipfsGate to dweb.link', () => { + const { controller } = setupController({}); + expect(controller.state.ipfsGateway).toStrictEqual('dweb.link'); + }); + + it('setIpfsGateway to test.link', () => { + const { controller } = setupController({}); + controller.setIpfsGateway('test.link'); + expect(controller.getIpfsGateway()).toStrictEqual('test.link'); + }); + }); + + describe('isIpfsGatewayEnabled', () => { + it('defaults isIpfsGatewayEnabled to true', () => { + const { controller } = setupController({}); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(true); + }); + + it('set isIpfsGatewayEnabled to false', () => { + const { controller } = setupController({}); + controller.setIsIpfsGatewayEnabled(false); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(false); + }); + }); + + describe('useAddressBarEnsResolution', () => { + it('defaults useAddressBarEnsResolution to true', () => { + const { controller } = setupController({}); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + }); + + it('set useAddressBarEnsResolution to false', () => { + const { controller } = setupController({}); + controller.setUseAddressBarEnsResolution(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + }); + }); + + describe('dismissSeedBackUpReminder', () => { + it('defaults dismissSeedBackUpReminder to false', () => { + const { controller } = setupController({}); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(false); + }); + + it('set dismissSeedBackUpReminder to true', () => { + const { controller } = setupController({}); + controller.setDismissSeedBackUpReminder(true); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(true); + }); + }); + + describe('snapsAddSnapAccountModalDismissed', () => { + it('defaults snapsAddSnapAccountModalDismissed to false', () => { + const { controller } = setupController({}); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + false, + ); + }); + + it('set snapsAddSnapAccountModalDismissed to true', () => { + const { controller } = setupController({}); + controller.setSnapsAddSnapAccountModalDismissed(true); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + true, + ); }); }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index eb126b176a41..a7ede69bb26c 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -1,4 +1,3 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerChangeEvent, AccountsControllerGetAccountByAddressAction, @@ -8,7 +7,18 @@ import { AccountsControllerState, } from '@metamask/accounts-controller'; import { Hex } from '@metamask/utils'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +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, + type PreferencesState, +} from '@metamask/preferences-controller'; import { CHAIN_IDS, IPFS_DEFAULT_GATEWAY_URL, @@ -19,7 +29,7 @@ import { ThemeType } from '../../../shared/constants/preferences'; type AccountIdentityEntry = { address: string; name: string; - lastSelected: number | undefined; + lastSelected?: number; }; const mainNetworks = { @@ -38,10 +48,10 @@ const controllerName = 'PreferencesController'; /** * Returns the state of the {@link PreferencesController}. */ -export type PreferencesControllerGetStateAction = { - type: 'PreferencesController:getState'; - handler: () => PreferencesControllerState; -}; +export type PreferencesControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + PreferencesControllerState +>; /** * Actions exposed by the {@link PreferencesController}. @@ -51,10 +61,10 @@ export type PreferencesControllerActions = PreferencesControllerGetStateAction; /** * Event emitted when the state of the {@link PreferencesController} changes. */ -export type PreferencesControllerStateChangeEvent = { - type: 'PreferencesController:stateChange'; - payload: [PreferencesControllerState, []]; -}; +export type PreferencesControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + PreferencesControllerState +>; /** * Events emitted by {@link PreferencesController}. @@ -68,7 +78,8 @@ export type AllowedActions = | AccountsControllerGetAccountByAddressAction | AccountsControllerSetAccountNameAction | AccountsControllerGetSelectedAccountAction - | AccountsControllerSetSelectedAccountAction; + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction; /** * Events that this controller is allowed to subscribe. @@ -84,9 +95,7 @@ export type PreferencesControllerMessenger = RestrictedControllerMessenger< >; type PreferencesControllerOptions = { - networkConfigurationsByChainId?: Record<Hex, { chainId: Hex }>; - initState?: Partial<PreferencesControllerState>; - initLangCode?: string; + state?: Partial<PreferencesControllerState>; messenger: PreferencesControllerMessenger; }; @@ -114,176 +123,356 @@ export type Preferences = { shouldShowAggregatedBalancePopover: boolean; }; -export type PreferencesControllerState = { - selectedAddress: string; +// Omitting showTestNetworks and smartTransactionsOptInStatus, as they already exists here in Preferences type +export type PreferencesControllerState = Omit< + PreferencesState, + 'showTestNetworks' | 'smartTransactionsOptInStatus' +> & { useBlockie: boolean; useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; - useTokenDetection: boolean; - useNftDetection: boolean; use4ByteResolution: boolean; useCurrencyRateCheck: boolean; useRequestQueue: boolean; - openSeaEnabled: boolean; - securityAlertsEnabled: boolean; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; - addSnapAccountEnabled: boolean; + addSnapAccountEnabled?: boolean; advancedGasFee: Record<string, Record<string, string>>; - featureFlags: Record<string, boolean>; incomingTransactionsPreferences: Record<number, boolean>; knownMethodData: Record<string, string>; currentLocale: string; - identities: Record<string, AccountIdentityEntry>; - lostIdentities: Record<string, object>; forgottenPassword: boolean; preferences: Preferences; - ipfsGateway: string; - isIpfsGatewayEnabled: boolean; useAddressBarEnsResolution: boolean; ledgerTransportType: LedgerTransportTypes; - snapRegistryList: Record<string, object>; + // TODO: Replace `Json` with correct type + snapRegistryList: Record<string, Json>; theme: ThemeType; - snapsAddSnapAccountModalDismissed: boolean; + snapsAddSnapAccountModalDismissed?: boolean; useExternalNameSources: boolean; - useTransactionSimulations: boolean; enableMV3TimestampSave: boolean; useExternalServices: boolean; textDirection?: string; }; -export default class PreferencesController { - store: ObservableStore<PreferencesControllerState>; +/** + * Function to get default state of the {@link PreferencesController}. + */ +export const getDefaultPreferencesControllerState = + (): PreferencesControllerState => ({ + selectedAddress: '', + useBlockie: false, + useNonceField: false, + usePhishDetect: true, + dismissSeedBackUpReminder: false, + useMultiAccountBalanceChecker: true, + useSafeChainsListValidation: true, + // set to true means the dynamic list from the API is being used + // set to false will be using the static list from contract-metadata + useTokenDetection: true, + useNftDetection: true, + use4ByteResolution: true, + useCurrencyRateCheck: true, + useRequestQueue: true, + openSeaEnabled: true, + securityAlertsEnabled: true, + watchEthereumAccountEnabled: false, + bitcoinSupportEnabled: false, + bitcoinTestnetSupportEnabled: false, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + addSnapAccountEnabled: false, + ///: END:ONLY_INCLUDE_IF + advancedGasFee: {}, + featureFlags: {}, + incomingTransactionsPreferences: { + ...mainNetworks, + ...testNetworks, + }, + knownMethodData: {}, + currentLocale: '', + identities: {}, + lostIdentities: {}, + forgottenPassword: false, + preferences: { + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + shouldShowAggregatedBalancePopover: true, // by default user should see popover; + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + // ENS decentralized website resolution + ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, + isIpfsGatewayEnabled: true, + useAddressBarEnsResolution: true, + // Ledger transport type is deprecated. We currently only support webhid + // on chrome, and u2f on firefox. + ledgerTransportType: window.navigator.hid + ? LedgerTransportTypes.webhid + : LedgerTransportTypes.u2f, + snapRegistryList: {}, + theme: ThemeType.os, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + snapsAddSnapAccountModalDismissed: false, + ///: END:ONLY_INCLUDE_IF + useExternalNameSources: true, + useTransactionSimulations: true, + enableMV3TimestampSave: true, + // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. + // Whenever useExternalServices is false, certain features will be disabled. + // The flag is true by Default, meaning the toggle is ON by default. + useExternalServices: true, + // from core PreferencesController + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + }); - private messagingSystem: PreferencesControllerMessenger; +/** + * {@link PreferencesController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + selectedAddress: { + persist: true, + anonymous: false, + }, + useBlockie: { + persist: true, + anonymous: true, + }, + useNonceField: { + persist: true, + anonymous: true, + }, + usePhishDetect: { + persist: true, + anonymous: true, + }, + dismissSeedBackUpReminder: { + persist: true, + anonymous: true, + }, + useMultiAccountBalanceChecker: { + persist: true, + anonymous: true, + }, + useSafeChainsListValidation: { + persist: true, + anonymous: false, + }, + useTokenDetection: { + persist: true, + anonymous: true, + }, + useNftDetection: { + persist: true, + anonymous: true, + }, + use4ByteResolution: { + persist: true, + anonymous: true, + }, + useCurrencyRateCheck: { + persist: true, + anonymous: true, + }, + useRequestQueue: { + persist: true, + anonymous: true, + }, + openSeaEnabled: { + persist: true, + anonymous: true, + }, + securityAlertsEnabled: { + persist: true, + anonymous: false, + }, + watchEthereumAccountEnabled: { + persist: true, + anonymous: false, + }, + bitcoinSupportEnabled: { + persist: true, + anonymous: false, + }, + bitcoinTestnetSupportEnabled: { + persist: true, + anonymous: false, + }, + addSnapAccountEnabled: { + persist: true, + anonymous: false, + }, + advancedGasFee: { + persist: true, + anonymous: true, + }, + featureFlags: { + persist: true, + anonymous: true, + }, + incomingTransactionsPreferences: { + persist: true, + anonymous: true, + }, + knownMethodData: { + persist: true, + anonymous: false, + }, + currentLocale: { + persist: true, + anonymous: true, + }, + identities: { + persist: true, + anonymous: false, + }, + lostIdentities: { + persist: true, + anonymous: false, + }, + forgottenPassword: { + persist: true, + anonymous: true, + }, + preferences: { + persist: true, + anonymous: true, + }, + ipfsGateway: { + persist: true, + anonymous: false, + }, + isIpfsGatewayEnabled: { + persist: true, + anonymous: false, + }, + useAddressBarEnsResolution: { + persist: true, + anonymous: true, + }, + ledgerTransportType: { + persist: true, + anonymous: true, + }, + snapRegistryList: { + persist: true, + anonymous: false, + }, + theme: { + persist: true, + anonymous: true, + }, + snapsAddSnapAccountModalDismissed: { + persist: true, + anonymous: false, + }, + useExternalNameSources: { + persist: true, + anonymous: false, + }, + useTransactionSimulations: { + persist: true, + anonymous: true, + }, + enableMV3TimestampSave: { + persist: true, + anonymous: true, + }, + useExternalServices: { + persist: true, + anonymous: false, + }, + textDirection: { + persist: true, + anonymous: false, + }, + isMultiAccountBalancesEnabled: { persist: true, anonymous: true }, + showIncomingTransactions: { persist: true, anonymous: true }, +}; +export class PreferencesController extends BaseController< + typeof controllerName, + PreferencesControllerState, + PreferencesControllerMessenger +> { /** + * Constructs a Preferences controller. * - * @param opts - Overrides the defaults for the initial state of this.store - * @property messenger - The controller messenger - * @property initState The stored object containing a users preferences, stored in local storage - * @property initState.useBlockie The users preference for blockie identicons within the UI - * @property initState.useNonceField The users preference for nonce field within the UI - * @property initState.featureFlags A key-boolean map, where keys refer to features and booleans to whether the - * user wishes to see that feature. - * - * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior. - * @property initState.knownMethodData Contains all data methods known by the user - * @property initState.currentLocale The preferred language locale key - * @property initState.selectedAddress A hex string that matches the currently selected address in the app + * @param options - the controller options + * @param options.messenger - The controller messenger + * @param options.state - The initial controller state */ - constructor(opts: PreferencesControllerOptions) { + constructor({ messenger, state }: PreferencesControllerOptions) { + const { networkConfigurationsByChainId } = messenger.call( + 'NetworkController:getState', + ); + const addedNonMainNetwork: Record<Hex, boolean> = Object.values( - opts.networkConfigurationsByChainId ?? {}, + networkConfigurationsByChainId ?? {}, ).reduce((acc: Record<Hex, boolean>, element) => { acc[element.chainId] = true; return acc; }, {}); - - const initState: PreferencesControllerState = { - selectedAddress: '', - useBlockie: false, - useNonceField: false, - usePhishDetect: true, - dismissSeedBackUpReminder: false, - useMultiAccountBalanceChecker: true, - useSafeChainsListValidation: true, - // set to true means the dynamic list from the API is being used - // set to false will be using the static list from contract-metadata - useTokenDetection: opts?.initState?.useTokenDetection ?? true, - useNftDetection: opts?.initState?.useTokenDetection ?? true, - use4ByteResolution: true, - useCurrencyRateCheck: true, - useRequestQueue: true, - openSeaEnabled: true, - securityAlertsEnabled: true, - watchEthereumAccountEnabled: false, - bitcoinSupportEnabled: false, - bitcoinTestnetSupportEnabled: false, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - addSnapAccountEnabled: false, - ///: END:ONLY_INCLUDE_IF - advancedGasFee: {}, - - // WARNING: Do not use feature flags for security-sensitive things. - // Feature flag toggling is available in the global namespace - // for convenient testing of pre-release features, and should never - // perform sensitive operations. - featureFlags: {}, - incomingTransactionsPreferences: { - ...mainNetworks, - ...addedNonMainNetwork, - ...testNetworks, - }, - knownMethodData: {}, - currentLocale: opts.initLangCode ?? '', - identities: {}, - lostIdentities: {}, - forgottenPassword: false, - preferences: { - autoLockTimeLimit: undefined, - showExtensionInFullSizeView: false, - showFiatInTestnets: false, - showTestNetworks: false, - smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible - showNativeTokenAsMainBalance: false, - useNativeCurrencyAsPrimaryCurrency: true, - hideZeroBalanceTokens: false, - petnamesEnabled: true, - redesignedConfirmationsEnabled: true, - redesignedTransactionsEnabled: true, - featureNotificationsEnabled: false, - showMultiRpcModal: false, - isRedesignedConfirmationsDeveloperEnabled: false, - showConfirmationAdvancedDetails: false, - tokenSortConfig: { - key: 'tokenFiatAmount', - order: 'dsc', - sortCallback: 'stringNumeric', + super({ + messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultPreferencesControllerState(), + incomingTransactionsPreferences: { + ...mainNetworks, + ...addedNonMainNetwork, + ...testNetworks, }, - shouldShowAggregatedBalancePopover: true, // by default user should see popover; + ...state, }, - // ENS decentralized website resolution - ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - isIpfsGatewayEnabled: true, - useAddressBarEnsResolution: true, - // Ledger transport type is deprecated. We currently only support webhid - // on chrome, and u2f on firefox. - ledgerTransportType: window.navigator.hid - ? LedgerTransportTypes.webhid - : LedgerTransportTypes.u2f, - snapRegistryList: {}, - theme: ThemeType.os, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - snapsAddSnapAccountModalDismissed: false, - ///: END:ONLY_INCLUDE_IF - useExternalNameSources: true, - useTransactionSimulations: true, - enableMV3TimestampSave: true, - // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. - // Whenever useExternalServices is false, certain features will be disabled. - // The flag is true by Default, meaning the toggle is ON by default. - useExternalServices: true, - ...opts.initState, - }; - - this.store = new ObservableStore(initState); - this.store.setMaxListeners(13); - - this.messagingSystem = opts.messenger; - this.messagingSystem.registerActionHandler( - `PreferencesController:getState`, - () => this.store.getState(), - ); - this.messagingSystem.registerInitialEventPayload({ - eventType: `PreferencesController:stateChange`, - getPayload: () => [this.store.getState(), []], }); this.messagingSystem.subscribe( @@ -302,7 +491,9 @@ export default class PreferencesController { * @param forgottenPassword - whether or not the user has forgotten their password */ setPasswordForgotten(forgottenPassword: boolean): void { - this.store.updateState({ forgottenPassword }); + this.update((state) => { + state.forgottenPassword = forgottenPassword; + }); } /** @@ -311,7 +502,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers blockie indicators */ setUseBlockie(val: boolean): void { - this.store.updateState({ useBlockie: val }); + this.update((state) => { + state.useBlockie = val; + }); } /** @@ -320,7 +513,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to set nonce */ setUseNonceField(val: boolean): void { - this.store.updateState({ useNonceField: val }); + this.update((state) => { + state.useNonceField = val; + }); } /** @@ -329,7 +524,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers phishing domain protection */ setUsePhishDetect(val: boolean): void { - this.store.updateState({ usePhishDetect: val }); + this.update((state) => { + state.usePhishDetect = val; + }); } /** @@ -338,7 +535,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on all security settings */ setUseMultiAccountBalanceChecker(val: boolean): void { - this.store.updateState({ useMultiAccountBalanceChecker: val }); + this.update((state) => { + state.useMultiAccountBalanceChecker = val; + }); } /** @@ -347,11 +546,15 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on validation for manually adding networks */ setUseSafeChainsListValidation(val: boolean): void { - this.store.updateState({ useSafeChainsListValidation: val }); + this.update((state) => { + state.useSafeChainsListValidation = val; + }); } toggleExternalServices(useExternalServices: boolean): void { - this.store.updateState({ useExternalServices }); + this.update((state) => { + state.useExternalServices = useExternalServices; + }); this.setUseTokenDetection(useExternalServices); this.setUseCurrencyRateCheck(useExternalServices); this.setUsePhishDetect(useExternalServices); @@ -366,7 +569,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use the static token list or dynamic token list from the API */ setUseTokenDetection(val: boolean): void { - this.store.updateState({ useTokenDetection: val }); + this.update((state) => { + state.useTokenDetection = val; + }); } /** @@ -375,7 +580,9 @@ export default class PreferencesController { * @param useNftDetection - Whether or not the user prefers to autodetect NFTs. */ setUseNftDetection(useNftDetection: boolean): void { - this.store.updateState({ useNftDetection }); + this.update((state) => { + state.useNftDetection = useNftDetection; + }); } /** @@ -384,7 +591,9 @@ export default class PreferencesController { * @param use4ByteResolution - (Privacy) Whether or not the user prefers to have smart contract name details resolved with 4byte.directory */ setUse4ByteResolution(use4ByteResolution: boolean): void { - this.store.updateState({ use4ByteResolution }); + this.update((state) => { + state.use4ByteResolution = use4ByteResolution; + }); } /** @@ -393,7 +602,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use currency rate check for ETH and tokens. */ setUseCurrencyRateCheck(val: boolean): void { - this.store.updateState({ useCurrencyRateCheck: val }); + this.update((state) => { + state.useCurrencyRateCheck = val; + }); } /** @@ -402,7 +613,9 @@ export default class PreferencesController { * @param val - Whether or not the user wants to have requests queued if network change is required. */ setUseRequestQueue(val: boolean): void { - this.store.updateState({ useRequestQueue: val }); + this.update((state) => { + state.useRequestQueue = val; + }); } /** @@ -411,8 +624,8 @@ export default class PreferencesController { * @param openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data. */ setOpenSeaEnabled(openSeaEnabled: boolean): void { - this.store.updateState({ - openSeaEnabled, + this.update((state) => { + state.openSeaEnabled = openSeaEnabled; }); } @@ -422,8 +635,8 @@ export default class PreferencesController { * @param securityAlertsEnabled - Whether or not the user prefers to use the security alerts. */ setSecurityAlertsEnabled(securityAlertsEnabled: boolean): void { - this.store.updateState({ - securityAlertsEnabled, + this.update((state) => { + state.securityAlertsEnabled = securityAlertsEnabled; }); } @@ -435,8 +648,8 @@ export default class PreferencesController { * enable the "Add Snap accounts" button. */ setAddSnapAccountEnabled(addSnapAccountEnabled: boolean): void { - this.store.updateState({ - addSnapAccountEnabled, + this.update((state) => { + state.addSnapAccountEnabled = addSnapAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -449,8 +662,8 @@ export default class PreferencesController { * enable the "Watch Ethereum account (Beta)" button. */ setWatchEthereumAccountEnabled(watchEthereumAccountEnabled: boolean): void { - this.store.updateState({ - watchEthereumAccountEnabled, + this.update((state) => { + state.watchEthereumAccountEnabled = watchEthereumAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -462,8 +675,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Beta)" button. */ setBitcoinSupportEnabled(bitcoinSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinSupportEnabled, + this.update((state) => { + state.bitcoinSupportEnabled = bitcoinSupportEnabled; }); } @@ -474,8 +687,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Testnet)" button. */ setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinTestnetSupportEnabled, + this.update((state) => { + state.bitcoinTestnetSupportEnabled = bitcoinTestnetSupportEnabled; }); } @@ -485,8 +698,8 @@ export default class PreferencesController { * @param useExternalNameSources - Whether or not to use external name providers in the name controller. */ setUseExternalNameSources(useExternalNameSources: boolean): void { - this.store.updateState({ - useExternalNameSources, + this.update((state) => { + state.useExternalNameSources = useExternalNameSources; }); } @@ -496,8 +709,8 @@ export default class PreferencesController { * @param useTransactionSimulations - Whether or not to use simulations in the transaction confirmations. */ setUseTransactionSimulations(useTransactionSimulations: boolean): void { - this.store.updateState({ - useTransactionSimulations, + this.update((state) => { + state.useTransactionSimulations = useTransactionSimulations; }); } @@ -515,12 +728,12 @@ export default class PreferencesController { chainId: string; gasFeePreferences: Record<string, string>; }): void { - const { advancedGasFee } = this.store.getState(); - this.store.updateState({ - advancedGasFee: { + const { advancedGasFee } = this.state; + this.update((state) => { + state.advancedGasFee = { ...advancedGasFee, [chainId]: gasFeePreferences, - }, + }; }); } @@ -530,7 +743,9 @@ export default class PreferencesController { * @param val - 'default' or 'dark' value based on the mode selected by user. */ setTheme(val: ThemeType): void { - this.store.updateState({ theme: val }); + this.update((state) => { + state.theme = val; + }); } /** @@ -540,12 +755,14 @@ export default class PreferencesController { * @param methodData - Corresponding data method */ addKnownMethodData(fourBytePrefix: string, methodData: string): void { - const { knownMethodData } = this.store.getState(); + const { knownMethodData } = this.state; const updatedKnownMethodData = { ...knownMethodData }; updatedKnownMethodData[fourBytePrefix] = methodData; - this.store.updateState({ knownMethodData: updatedKnownMethodData }); + this.update((state) => { + state.knownMethodData = updatedKnownMethodData; + }); } /** @@ -557,9 +774,9 @@ export default class PreferencesController { const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key) ? 'rtl' : 'auto'; - this.store.updateState({ - currentLocale: key, - textDirection, + this.update((state) => { + state.currentLocale = key; + state.textDirection = textDirection; }); return textDirection; } @@ -605,7 +822,7 @@ export default class PreferencesController { * @returns whether this option is on or off. */ getUseRequestQueue(): boolean { - return this.store.getState().useRequestQueue; + return this.state.useRequestQueue; } /** @@ -648,14 +865,15 @@ export default class PreferencesController { * @returns the updated featureFlags object. */ setFeatureFlag(feature: string, activated: boolean): Record<string, boolean> { - const currentFeatureFlags = this.store.getState().featureFlags; + const currentFeatureFlags = this.state.featureFlags; const updatedFeatureFlags = { ...currentFeatureFlags, [feature]: activated, }; - this.store.updateState({ featureFlags: updatedFeatureFlags }); - + this.update((state) => { + state.featureFlags = updatedFeatureFlags; + }); return updatedFeatureFlags; } @@ -677,7 +895,9 @@ export default class PreferencesController { [preference]: value, }; - this.store.updateState({ preferences: updatedPreferences }); + this.update((state) => { + state.preferences = updatedPreferences; + }); return updatedPreferences; } @@ -687,7 +907,7 @@ export default class PreferencesController { * @returns A map of user-selected preferences. */ getPreferences(): Preferences { - return this.store.getState().preferences; + return this.state.preferences; } /** @@ -696,7 +916,7 @@ export default class PreferencesController { * @returns The current IPFS gateway domain */ getIpfsGateway(): string { - return this.store.getState().ipfsGateway; + return this.state.ipfsGateway; } /** @@ -706,7 +926,9 @@ export default class PreferencesController { * @returns the update IPFS gateway domain */ setIpfsGateway(domain: string): string { - this.store.updateState({ ipfsGateway: domain }); + this.update((state) => { + state.ipfsGateway = domain; + }); return domain; } @@ -716,7 +938,9 @@ export default class PreferencesController { * @param enabled - Whether or not IPFS is enabled */ setIsIpfsGatewayEnabled(enabled: boolean): void { - this.store.updateState({ isIpfsGatewayEnabled: enabled }); + this.update((state) => { + state.isIpfsGatewayEnabled = enabled; + }); } /** @@ -725,7 +949,9 @@ export default class PreferencesController { * @param useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains */ setUseAddressBarEnsResolution(useAddressBarEnsResolution: boolean): void { - this.store.updateState({ useAddressBarEnsResolution }); + this.update((state) => { + state.useAddressBarEnsResolution = useAddressBarEnsResolution; + }); } /** @@ -739,7 +965,9 @@ export default class PreferencesController { setLedgerTransportPreference( ledgerTransportType: LedgerTransportTypes, ): string { - this.store.updateState({ ledgerTransportType }); + this.update((state) => { + state.ledgerTransportType = ledgerTransportType; + }); return ledgerTransportType; } @@ -749,8 +977,8 @@ export default class PreferencesController { * @param dismissSeedBackUpReminder - User preference for dismissing the back up reminder. */ setDismissSeedBackUpReminder(dismissSeedBackUpReminder: boolean): void { - this.store.updateState({ - dismissSeedBackUpReminder, + this.update((state) => { + state.dismissSeedBackUpReminder = dismissSeedBackUpReminder; }); } @@ -761,18 +989,24 @@ export default class PreferencesController { * @param value - preference of certain network, true to be enabled */ setIncomingTransactionsPreferences(chainId: Hex, value: boolean): void { - const previousValue = this.store.getState().incomingTransactionsPreferences; + const previousValue = this.state.incomingTransactionsPreferences; const updatedValue = { ...previousValue, [chainId]: value }; - this.store.updateState({ incomingTransactionsPreferences: updatedValue }); + this.update((state) => { + state.incomingTransactionsPreferences = updatedValue; + }); } setServiceWorkerKeepAlivePreference(value: boolean): void { - this.store.updateState({ enableMV3TimestampSave: value }); + this.update((state) => { + state.enableMV3TimestampSave = value; + }); } ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) setSnapsAddSnapAccountModalDismissed(value: boolean): void { - this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); + this.update((state) => { + state.snapsAddSnapAccountModalDismissed = value; + }); } ///: END:ONLY_INCLUDE_IF @@ -783,7 +1017,7 @@ export default class PreferencesController { newAccountsControllerState.internalAccounts; const selectedAccount = accounts[selectedAccountId]; - const { identities, lostIdentities } = this.store.getState(); + const { identities, lostIdentities } = this.state; const addresses = Object.values(accounts).map((account) => account.address.toLowerCase(), @@ -812,10 +1046,10 @@ export default class PreferencesController { {}, ); - this.store.updateState({ - identities: updatedIdentities, - lostIdentities: updatedLostIdentities, - selectedAddress: selectedAccount?.address || '', // it will be an empty string during onboarding + this.update((state) => { + state.identities = updatedIdentities; + state.lostIdentities = updatedLostIdentities; + state.selectedAddress = selectedAccount?.address || ''; // it will be an empty string during onboarding }); } } diff --git a/app/scripts/lib/backup.js b/app/scripts/lib/backup.js index 7c550c1581ab..c9da3628a99c 100644 --- a/app/scripts/lib/backup.js +++ b/app/scripts/lib/backup.js @@ -18,7 +18,7 @@ export default class Backup { } async restoreUserData(jsonString) { - const existingPreferences = this.preferencesController.store.getState(); + const existingPreferences = this.preferencesController.state; const { preferences, addressBook, network, internalAccounts } = JSON.parse(jsonString); if (preferences) { @@ -26,7 +26,7 @@ export default class Backup { preferences.lostIdentities = existingPreferences.lostIdentities; preferences.selectedAddress = existingPreferences.selectedAddress; - this.preferencesController.store.updateState(preferences); + this.preferencesController.update(preferences); } if (addressBook) { @@ -51,7 +51,7 @@ export default class Backup { async backupUserData() { const userData = { - preferences: { ...this.preferencesController.store.getState() }, + preferences: { ...this.preferencesController.state }, internalAccounts: { internalAccounts: this.accountsController.state.internalAccounts, }, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 0d9712ba5be5..7a322148c847 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -7,8 +7,7 @@ import { mockNetworkState } from '../../../test/stub/networks'; import Backup from './backup'; function getMockPreferencesController() { - const mcState = { - getSelectedAddress: jest.fn().mockReturnValue('0x01'), + const state = { selectedAddress: '0x01', identities: { '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': { @@ -24,15 +23,14 @@ function getMockPreferencesController() { name: 'Ledger 1', }, }, - update: (store) => (mcState.store = store), }; + const getSelectedAddress = jest.fn().mockReturnValue('0x01'); - mcState.store = { - getState: jest.fn().mockReturnValue(mcState), - updateState: (store) => (mcState.store = store), + return { + state, + getSelectedAddress, + update: jest.fn(), }; - - return mcState; } function getMockAddressBookController() { @@ -239,30 +237,30 @@ describe('Backup', function () { ).toStrictEqual('network-configuration-id-4'); // make sure identities are not lost after restore expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].lastSelected, ).toStrictEqual(1655380342907); expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].name, ).toStrictEqual('Account 3'); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].lastSelected, ).toStrictEqual(1655379648197); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].name, ).toStrictEqual('Ledger 1'); // make sure selected address is not lost after restore - expect(backup.preferencesController.store.selectedAddress).toStrictEqual( + expect(backup.preferencesController.state.selectedAddress).toStrictEqual( '0x01', ); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index f0b66430ee84..b96c708be2d3 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -58,13 +58,11 @@ const metaMetricsController = new MetaMetricsController({ segment: createSegmentMock(2, 10000), getCurrentChainId: () => '0x1338', onNetworkDidChange: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ - currentLocale: 'en_US', - preferences: {}, - })), + preferencesControllerState: { + currentLocale: 'en_US', + preferences: {}, }, + onPreferencesStateChange: jest.fn(), version: '0.0.1', environment: 'test', initState: { diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index d0adbefb264b..8977c00aa3d7 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -57,17 +57,17 @@ const createMiddleware = ( const ppomController = {}; const preferenceController = { - store: { - getState: () => ({ - securityAlertsEnabled: securityAlertsEnabled ?? true, - }), + state: { + securityAlertsEnabled: securityAlertsEnabled ?? true, }, }; if (error) { - preferenceController.store.getState = () => { - throw error; - }; + Object.defineProperty(preferenceController, 'state', { + get() { + throw error; + }, + }); } const networkController = { diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 1bad576e3881..3b393897b2e0 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -11,7 +11,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 { PreferencesController } from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; // eslint-disable-next-line import/no-restricted-paths @@ -76,8 +76,7 @@ export function createPPOMMiddleware< next: () => void, ) => { try { - const securityAlertsEnabled = - preferencesController.store.getState()?.securityAlertsEnabled; + const { securityAlertsEnabled } = preferencesController.state; const { chainId } = getProviderConfig({ diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 98af29fe38f1..b19c91a232ab 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -290,7 +290,7 @@ import { NetworkOrderController } from './controllers/network-order'; 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 { PreferencesController } from './controllers/preferences-controller'; import AppStateController from './controllers/app-state'; import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; @@ -600,19 +600,20 @@ export default class MetamaskController extends EventEmitter { name: 'PreferencesController', allowedActions: [ 'AccountsController:setSelectedAccount', + 'AccountsController:getSelectedAccount', 'AccountsController:getAccountByAddress', 'AccountsController:setAccountName', + 'NetworkController:getState', ], allowedEvents: ['AccountsController:stateChange'], }); this.preferencesController = new PreferencesController({ - initState: initState.PreferencesController, - initLangCode: opts.initLangCode, + state: { + currentLocale: opts.initLangCode ?? '', + ...initState.PreferencesController, + }, messenger: preferencesMessenger, - provider: this.provider, - networkConfigurationsByChainId: - this.networkController.state.networkConfigurationsByChainId, }); const tokenListMessenger = this.controllerMessenger.getRestricted({ @@ -624,7 +625,7 @@ export default class MetamaskController extends EventEmitter { this.tokenListController = new TokenListController({ chainId: getCurrentChainId({ metamask: this.networkController.state }), preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( - this.preferencesController.store.getState(), + this.preferencesController.state, ), messenger: tokenListMessenger, state: initState.TokenListController, @@ -738,16 +739,19 @@ export default class MetamaskController extends EventEmitter { addNft: this.nftController.addNft.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] - disabled: - this.preferencesController.store.getState().useNftDetection === - undefined - ? false // the detection is enabled by default - : !this.preferencesController.store.getState().useNftDetection, + disabled: !this.preferencesController.state.useNftDetection, }); this.metaMetricsController = new MetaMetricsController({ segment, - preferencesStore: this.preferencesController.store, + onPreferencesStateChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', + ), + preferencesControllerState: { + currentLocale: this.preferencesController.state.currentLocale, + selectedAddress: this.preferencesController.state.selectedAddress, + }, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:networkDidChange', @@ -834,14 +838,17 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - preferencesStore: this.preferencesController.store, + preferencesController: this.preferencesController, messenger: this.controllerMessenger.getRestricted({ name: 'AppStateController', allowedActions: [ `${this.approvalController.name}:addRequest`, `${this.approvalController.name}:acceptRequest`, ], - allowedEvents: [`KeyringController:qrKeyringStateChange`], + allowedEvents: [ + `KeyringController:qrKeyringStateChange`, + 'PreferencesController:stateChange', + ], }), extension: this.extension, }); @@ -860,7 +867,7 @@ export default class MetamaskController extends EventEmitter { this.currencyRateController, ); this.currencyRateController.fetchExchangeRate = (...args) => { - if (this.preferencesController.store.getState().useCurrencyRateCheck) { + if (this.preferencesController.state.useCurrencyRateCheck) { return initialFetchExchangeRate(...args); } return { @@ -898,9 +905,10 @@ export default class MetamaskController extends EventEmitter { state: initState.PPOMController, chainId: getCurrentChainId({ metamask: this.networkController.state }), securityAlertsEnabled: - this.preferencesController.store.getState().securityAlertsEnabled, - onPreferencesChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, + this.preferencesController.state.securityAlertsEnabled, + onPreferencesChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', ), cdnBaseUrl: process.env.BLOCKAID_FILE_CDN, blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY, @@ -986,7 +994,8 @@ export default class MetamaskController extends EventEmitter { tokenPricesService: new CodefiTokenPricesServiceV2(), }); - this.preferencesController.store.subscribe( + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', previousValueComparator((prevState, currState) => { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; @@ -995,7 +1004,7 @@ export default class MetamaskController extends EventEmitter { } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { this.tokenRatesController.stop(); } - }, this.preferencesController.store.getState()), + }, this.preferencesController.state), ); this.ensController = new EnsController({ @@ -1259,9 +1268,13 @@ export default class MetamaskController extends EventEmitter { }), state: initState.SelectedNetworkController, useRequestQueuePreference: - this.preferencesController.store.getState().useRequestQueue, - onPreferencesStateChange: (listener) => - this.preferencesController.store.subscribe(listener), + this.preferencesController.state.useRequestQueue, + onPreferencesStateChange: (listener) => { + preferencesMessenger.subscribe( + 'PreferencesController:stateChange', + listener, + ); + }, domainProxyMap: new WeakRefObjectMap(), }); @@ -1366,8 +1379,7 @@ export default class MetamaskController extends EventEmitter { getFeatureFlags: () => { return { disableSnaps: - this.preferencesController.store.getState().useExternalServices === - false, + this.preferencesController.state.useExternalServices === false, }; }, }); @@ -1686,7 +1698,7 @@ export default class MetamaskController extends EventEmitter { }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, - preferencesController: this.preferencesController, + preferencesControllerState: this.preferencesController.state, }); // start and stop polling for balances based on activeControllerConnections @@ -1769,7 +1781,6 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ initState: initState.AlertController, - preferencesStore: this.preferencesController.store, controllerMessenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], @@ -1852,7 +1863,7 @@ export default class MetamaskController extends EventEmitter { getNetworkState: () => this.networkController.state, getPermittedAccounts: this.getPermittedAccounts.bind(this), getSavedGasFees: () => - this.preferencesController.store.getState().advancedGasFee[ + this.preferencesController.state.advancedGasFee[ getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { @@ -1863,8 +1874,7 @@ export default class MetamaskController extends EventEmitter { includeTokenTransfers: false, isEnabled: () => Boolean( - this.preferencesController.store.getState() - .incomingTransactionsPreferences?.[ + this.preferencesController.state.incomingTransactionsPreferences?.[ getCurrentChainId({ metamask: this.networkController.state }) ] && this.onboardingController.state.completedOnboarding, ), @@ -1873,7 +1883,7 @@ export default class MetamaskController extends EventEmitter { }, isMultichainEnabled: process.env.TRANSACTION_MULTICHAIN, isSimulationEnabled: () => - this.preferencesController.store.getState().useTransactionSimulations, + this.preferencesController.state.useTransactionSimulations, messenger: transactionControllerMessenger, onNetworkStateChange: (listener) => { networkControllerMessenger.subscribe( @@ -2138,7 +2148,7 @@ export default class MetamaskController extends EventEmitter { }); const isExternalNameSourcesEnabled = () => - this.preferencesController.store.getState().useExternalNameSources; + this.preferencesController.state.useExternalNameSources; this.nameController = new NameController({ messenger: this.controllerMessenger.getRestricted({ @@ -2353,7 +2363,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, TransactionController: this.txController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, @@ -2408,7 +2418,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, NetworkController: this.networkController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, @@ -2531,7 +2541,7 @@ export default class MetamaskController extends EventEmitter { } postOnboardingInitialization() { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; this.networkController.lookupNetwork(); @@ -2540,8 +2550,7 @@ export default class MetamaskController extends EventEmitter { } // post onboarding emit detectTokens event - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useTokenDetection, useNftDetection } = preferencesControllerState ?? {}; this.metaMetricsController.trackEvent({ @@ -2565,8 +2574,7 @@ export default class MetamaskController extends EventEmitter { this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2584,8 +2592,7 @@ export default class MetamaskController extends EventEmitter { this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2709,7 +2716,7 @@ export default class MetamaskController extends EventEmitter { * @returns The currently selected locale. */ getLocale() { - const { currentLocale } = this.preferencesController.store.getState(); + const { currentLocale } = this.preferencesController.state; return currentLocale; } @@ -2770,8 +2777,7 @@ export default class MetamaskController extends EventEmitter { 'SnapController:updateSnapState', ), maybeUpdatePhishingList: () => { - const { usePhishDetect } = - this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -2842,11 +2848,23 @@ export default class MetamaskController extends EventEmitter { */ setupControllerEventSubscriptions() { let lastSelectedAddress; + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', + previousValueComparator(async (prevState, currState) => { + const { currentLocale } = currState; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); - this.preferencesController.store.subscribe( - previousValueComparator((prevState, currState) => { - this.#onPreferencesControllerStateChange(currState, prevState); - }, this.preferencesController.store.getState()), + await updateCurrentLocale(currentLocale); + if (currState.incomingTransactionsPreferences?.[chainId]) { + this.txController.startIncomingTransactionPolling(); + } else { + this.txController.stopIncomingTransactionPolling(); + } + + this.#checkTokenListPolling(currState, prevState); + }, this.preferencesController.state), ); this.controllerMessenger.subscribe( @@ -4772,7 +4790,7 @@ export default class MetamaskController extends EventEmitter { const accounts = this.accountsController.listAccounts(); - const { identities } = this.preferencesController.store.getState(); + const { identities } = this.preferencesController.state; return { unlockedAccount, identities, accounts }; } @@ -4971,7 +4989,7 @@ export default class MetamaskController extends EventEmitter { chainId: getCurrentChainId({ metamask: this.networkController.state }), ppomController: this.ppomController, securityAlertsEnabled: - this.preferencesController.store.getState()?.securityAlertsEnabled, + this.preferencesController.state?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), ...otherParams, }; @@ -5143,7 +5161,7 @@ export default class MetamaskController extends EventEmitter { }) { if (sender.url) { if (this.onboardingController.state.completedOnboarding) { - if (this.preferencesController.store.getState().usePhishDetect) { + if (this.preferencesController.state.usePhishDetect) { const { hostname } = new URL(sender.url); this.phishingController.maybeUpdateState(); // Check if new connection is blocked if phishing detection is on @@ -5242,7 +5260,7 @@ export default class MetamaskController extends EventEmitter { * @param {ReadableStream} options.connectionStream - The Duplex stream to connect to. */ setupPhishingCommunication({ connectionStream }) { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -5636,7 +5654,7 @@ export default class MetamaskController extends EventEmitter { ); const isConfirmationRedesignEnabled = () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .redesignedConfirmationsEnabled; }; @@ -6450,7 +6468,7 @@ export default class MetamaskController extends EventEmitter { return null; } const { knownMethodData, use4ByteResolution } = - this.preferencesController.store.getState(); + this.preferencesController.state; const prefixedData = addHexPrefix(data); return getMethodDataName( knownMethodData, @@ -6463,11 +6481,11 @@ export default class MetamaskController extends EventEmitter { ); }, getIsRedesignedConfirmationsDeveloperEnabled: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .isRedesignedConfirmationsDeveloperEnabled; }, getIsConfirmationAdvancedDetailsOpen: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .showConfirmationAdvancedDetails; }, }; @@ -7063,30 +7081,6 @@ export default class MetamaskController extends EventEmitter { }; } - async #onPreferencesControllerStateChange(currentState, previousState) { - const { currentLocale } = currentState; - const chainId = getCurrentChainId({ - metamask: this.networkController.state, - }); - - await updateCurrentLocale(currentLocale); - - if (currentState.incomingTransactionsPreferences?.[chainId]) { - this.txController.startIncomingTransactionPolling(); - } else { - this.txController.stopIncomingTransactionPolling(); - } - - this.#checkTokenListPolling(currentState, previousState); - - // TODO: Remove once the preferences controller has been replaced with the core monorepo implementation - this.controllerMessenger.publish( - 'PreferencesController:stateChange', - currentState, - [], - ); - } - #checkTokenListPolling(currentState, previousState) { const previousEnabled = this.#isTokenListPollingRequired(previousState); const newEnabled = this.#isTokenListPollingRequired(currentState); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index bab66d9bc515..77b062bcfdc7 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -114,22 +114,6 @@ const rpcMethodMiddlewareMock = { }; jest.mock('./lib/rpc-method-middleware', () => rpcMethodMiddlewareMock); -jest.mock( - './controllers/preferences-controller', - () => - function (...args) { - const PreferencesController = jest.requireActual( - './controllers/preferences-controller', - ).default; - const controller = new PreferencesController(...args); - // jest.spyOn gets hoisted to the top of this function before controller is initialized. - // This forces us to replace the function directly with a jest stub instead. - // eslint-disable-next-line jest/prefer-spy-on - controller.store.subscribe = jest.fn(); - return controller; - }, -); - const KNOWN_PUBLIC_KEY = '02065bc80d3d12b3688e4ad5ab1e9eda6adf24aec2518bfc21b87c99d4c5077ab0'; @@ -357,10 +341,10 @@ describe('MetaMaskController', () => { let metamaskController; async function simulatePreferencesChange(preferences) { - metamaskController.preferencesController.store.subscribe.mock.lastCall[0]( + metamaskController.controllerMessenger.publish( + 'PreferencesController:stateChange', preferences, ); - await flushPromises(); } @@ -604,8 +588,7 @@ describe('MetaMaskController', () => { await localMetaMaskController.submitPassword(password); const identities = Object.keys( - localMetaMaskController.preferencesController.store.getState() - .identities, + localMetaMaskController.preferencesController.state.identities, ); const addresses = await localMetaMaskController.keyringController.getAccounts(); @@ -937,8 +920,7 @@ describe('MetaMaskController', () => { expect( Object.keys( - metamaskController.preferencesController.store.getState() - .identities, + metamaskController.preferencesController.state.identities, ), ).not.toContain(hardwareKeyringAccount); expect( diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 7ffbd68472d1..107c1bd7ad14 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -40,8 +40,6 @@ "app/scripts/controllers/permissions/selectors.test.js", "app/scripts/controllers/permissions/specifications.js", "app/scripts/controllers/permissions/specifications.test.js", - "app/scripts/controllers/preferences.js", - "app/scripts/controllers/preferences.test.js", "app/scripts/controllers/swaps.js", "app/scripts/controllers/swaps.test.js", "app/scripts/controllers/transactions/index.js", diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7eaa06a954b0..3df824f29c78 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2115,6 +2115,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/package.json b/package.json index 90da21554bc2..4973fa0da559 100644 --- a/package.json +++ b/package.json @@ -477,6 +477,7 @@ "@metamask/eslint-plugin-design-tokens": "^1.1.0", "@metamask/forwarder": "^1.1.0", "@metamask/phishing-warning": "^4.0.0", + "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", "@metamask/test-dapp": "^8.4.0", "@octokit/core": "^3.6.0", diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index e61d7ed807cd..a57a1eea2109 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -6,7 +6,7 @@ import { SignatureController } from '@metamask/signature-controller'; import { NetworkController } from '@metamask/network-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import PreferencesController from '../../app/scripts/controllers/preferences-controller'; +import { 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'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 2c0dfe9a23cb..5d3883a5e8f5 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -1,3 +1,6 @@ +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); const { FirstTimeFlowType } = require('../../shared/constants/onboarding'); @@ -232,6 +235,29 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, }, QueuedRequestController: { queuedRequestCount: 0, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index f1e9a7e5ae1d..4c802e13bfa0 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -4,6 +4,9 @@ const { } = require('@metamask/snaps-utils'); const { merge, mergeWith } = require('lodash'); const { toHex } = require('@metamask/controller-utils'); +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); @@ -94,6 +97,31 @@ function onboardingFixture() { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + showTestNetworks: false, + smartTransactionsOptInStatus: false, }, QueuedRequestController: { queuedRequestCount: 0, 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 559e8a256d43..4658c175bfd5 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 @@ -228,7 +228,9 @@ "useExternalNameSources": "boolean", "useTransactionSimulations": true, "enableMV3TimestampSave": true, - "useExternalServices": "boolean" + "useExternalServices": "boolean", + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, 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 2df9ee4e2f23..924769a3cb91 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 @@ -45,6 +45,7 @@ "completedOnboarding": true, "knownMethodData": "object", "use4ByteResolution": true, + "showIncomingTransactions": "object", "participateInMetaMetrics": true, "dataCollectionForMarketing": "boolean", "nextNonce": null, @@ -123,6 +124,7 @@ "forgottenPassword": false, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", + "isMultiAccountBalancesEnabled": "boolean", "useAddressBarEnsResolution": true, "ledgerTransportType": "webhid", "snapRegistryList": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index d22b69967027..e2cb7369d88a 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -129,7 +129,9 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 2dfd6ac6ef21..34cc62d3c560 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -129,7 +129,9 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/yarn.lock b/yarn.lock index bc7fc36aabbe..733c94112452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6057,6 +6057,18 @@ __metadata: languageName: node linkType: hard +"@metamask/preferences-controller@npm:^13.0.2": + version: 13.0.3 + resolution: "@metamask/preferences-controller@npm:13.0.3" + dependencies: + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + peerDependencies: + "@metamask/keyring-controller": ^17.0.0 + checksum: 10/d922c2e603c7a1ef0301dcfc7d5b6aa0bbdd9c318f0857fbbc9e95606609ae806e69c46231288953ce443322039781404565a46fe42bdfa731c4f0da20448d32 + 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" @@ -26165,6 +26177,7 @@ __metadata: "@metamask/phishing-warning": "npm:^4.0.0" "@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/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" From 59dc0cd3c2ebe71f920260ed49d2611dc9d2ba31 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:13:48 +0400 Subject: [PATCH 136/226] feat: Create a quality gate for typescript coverage (#27717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27717?quickstart=1) This PR introduces a quality gate for typescript coverage. It updates the existing fitness function to disallow the creation of new js and jsx files in the repository. ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2399 ## **Manual testing steps** 1. After committing a javascript file, the fitness function should fail. ## **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. --- .github/workflows/fitness-functions.yml | 6 +- .../common/constants.test.ts | 86 ++++++++++--------- .../fitness-functions/common/constants.ts | 16 ++-- .../fitness-functions/common/shared.test.ts | 84 ++++++++---------- .../fitness-functions/common/shared.ts | 44 ++++++++-- development/fitness-functions/rules/index.ts | 18 ++-- .../rules/javascript-additions.test.ts | 12 +-- .../rules/javascript-additions.ts | 11 +-- .../rules/sinon-assert-syntax.ts | 4 +- 9 files changed, 154 insertions(+), 127 deletions(-) diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index b4979c8f3e7b..f8e24692e8fe 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -2,12 +2,14 @@ name: Fitness Functions CI on: pull_request: - types: [assigned, opened, synchronize, reopened] + types: + - opened + - reopened + - synchronize jobs: build: runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/development/fitness-functions/common/constants.test.ts b/development/fitness-functions/common/constants.test.ts index 21912d5fa194..e0077f086594 100644 --- a/development/fitness-functions/common/constants.test.ts +++ b/development/fitness-functions/common/constants.test.ts @@ -1,8 +1,34 @@ -import { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX } from './constants'; +import { E2E_TESTS_REGEX, JS_REGEX } from './constants'; describe('Regular Expressions used in Fitness Functions', (): void => { - describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, (): void => { + describe(`E2E_TESTS_REGEX "${E2E_TESTS_REGEX}"`, (): void => { const PATHS_IT_SHOULD_MATCH = [ + // JS, TS, JSX, and TSX files inside the + // test/e2e directory + 'test/e2e/file.js', + 'test/e2e/path/file.ts', + 'test/e2e/much/longer/path/file.jsx', + 'test/e2e/much/longer/path/file.tsx', + // development/fitness-functions directory + 'development/fitness-functions/file.js', + 'development/fitness-functions/path/file.ts', + 'development/fitness-functions/much/longer/path/file.jsx', + 'development/fitness-functions/much/longer/path/file.tsx', + // development/webpack directory + 'development/webpack/file.js', + 'development/webpack/path/file.ts', + 'development/webpack/much/longer/path/file.jsx', + 'development/webpack/much/longer/path/file.tsx', + ]; + + const PATHS_IT_SHOULD_NOT_MATCH = [ + // any files without JS, TS, JSX or TSX extension + 'file', + 'file.extension', + 'path/file.extension', + 'much/longer/path/file.extension', + // JS, TS, JSX, and TSX files outside + // the test/e2e, development/fitness-functions, development/webpack directories 'file.js', 'path/file.js', 'much/longer/path/file.js', @@ -12,39 +38,15 @@ describe('Regular Expressions used in Fitness Functions', (): void => { 'file.jsx', 'path/file.jsx', 'much/longer/path/file.jsx', - ]; - - const PATHS_IT_SHOULD_NOT_MATCH = [ - // any without JS, TS, JSX or TSX extension - 'file', - 'file.extension', - 'path/file.extension', - 'much/longer/path/file.extension', - // any in the test/e2e directory - 'test/e2e/file', - 'test/e2e/file.extension', - 'test/e2e/path/file.extension', - 'test/e2e/much/longer/path/file.extension', - 'test/e2e/file.js', - 'test/e2e/path/file.ts', - 'test/e2e/much/longer/path/file.jsx', - 'test/e2e/much/longer/path/file.tsx', - // any in the development/fitness-functions directory - 'development/fitness-functions/file', - 'development/fitness-functions/file.extension', - 'development/fitness-functions/path/file.extension', - 'development/fitness-functions/much/longer/path/file.extension', - 'development/fitness-functions/file.js', - 'development/fitness-functions/path/file.ts', - 'development/fitness-functions/much/longer/path/file.jsx', - 'development/fitness-functions/much/longer/path/file.tsx', + 'file.tsx', + 'path/file.tsx', + 'much/longer/path/file.tsx', ]; describe('included paths', (): void => { PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { it(`should match "${path}"`, (): void => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - + const result = E2E_TESTS_REGEX.test(path); expect(result).toStrictEqual(true); }); }); @@ -53,22 +55,23 @@ describe('Regular Expressions used in Fitness Functions', (): void => { describe('excluded paths', (): void => { PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { it(`should not match "${path}"`, (): void => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - + const result = E2E_TESTS_REGEX.test(path); expect(result).toStrictEqual(false); }); }); }); }); - describe(`SHARED_FOLDER_JS_REGEX "${SHARED_FOLDER_JS_REGEX}"`, (): void => { + describe(`JS_REGEX "${JS_REGEX}"`, (): void => { const PATHS_IT_SHOULD_MATCH = [ + 'app/much/longer/path/file.js', + 'app/much/longer/path/file.jsx', + 'offscreen/path/file.js', + 'offscreen/path/file.jsx', 'shared/file.js', - 'shared/path/file.js', - 'shared/much/longer/path/file.js', 'shared/file.jsx', - 'shared/path/file.jsx', - 'shared/much/longer/path/file.jsx', + 'ui/much/longer/path/file.js', + 'ui/much/longer/path/file.jsx', ]; const PATHS_IT_SHOULD_NOT_MATCH = [ @@ -80,13 +83,15 @@ describe('Regular Expressions used in Fitness Functions', (): void => { 'file.ts', 'path/file.ts', 'much/longer/path/file.tsx', + // any JS or JSX files outside the app, offscreen, shared, and ui directories + 'test/longer/path/file.js', + 'random/longer/path/file.jsx', ]; describe('included paths', (): void => { PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { it(`should match "${path}"`, (): void => { - const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); - + const result = JS_REGEX.test(path); expect(result).toStrictEqual(true); }); }); @@ -95,8 +100,7 @@ describe('Regular Expressions used in Fitness Functions', (): void => { describe('excluded paths', (): void => { PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { it(`should not match "${path}"`, (): void => { - const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); - + const result = JS_REGEX.test(path); expect(result).toStrictEqual(false); }); }); diff --git a/development/fitness-functions/common/constants.ts b/development/fitness-functions/common/constants.ts index 5758d4e2a6e1..f3996a294a5a 100644 --- a/development/fitness-functions/common/constants.ts +++ b/development/fitness-functions/common/constants.ts @@ -1,10 +1,12 @@ -// include JS, TS, JSX, TSX files only excluding files in the e2e tests and -// fitness functions directories -const EXCLUDE_E2E_TESTS_REGEX = - '^(?!test/e2e)(?!development/fitness|development/webpack).*.(js|ts|jsx|tsx)$'; +// include JS, TS, JSX, TSX files only in the +// test/e2e +// development/fitness-functions +// development/webpack directories +const E2E_TESTS_REGEX = + /^(test\/e2e|development\/fitness-functions|development\/webpack).*\.(js|ts|jsx|tsx)$/u; -// include JS and JSX files in the shared directory only -const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$'; +// include JS and JSX files only in the app, offscreen, shared, and ui directories +const JS_REGEX = /^(app|offscreen|shared|ui)\/.*\.(js|jsx)$/u; enum AUTOMATION_TYPE { CI = 'ci', @@ -12,4 +14,4 @@ enum AUTOMATION_TYPE { PRE_PUSH_HOOK = 'pre-push-hook', } -export { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX, AUTOMATION_TYPE }; +export { E2E_TESTS_REGEX, JS_REGEX, AUTOMATION_TYPE }; diff --git a/development/fitness-functions/common/shared.test.ts b/development/fitness-functions/common/shared.test.ts index 92306b9d4751..66af337fbfc8 100644 --- a/development/fitness-functions/common/shared.test.ts +++ b/development/fitness-functions/common/shared.test.ts @@ -30,13 +30,13 @@ describe('filterDiffFileCreations()', (): void => { const actualResult = filterDiffFileCreations(testFileDiff); expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js - new file mode 100644 - index 000000000..30d74d258 - --- /dev/null - +++ b/old-file.js - @@ -0,0 +1 @@ - +ping" + "diff --git a/old-file.js b/old-file.js + new file mode 100644 + index 000000000..30d74d258 + --- /dev/null + +++ b/old-file.js + @@ -0,0 +1 @@ + +ping" `); }); }); @@ -44,9 +44,9 @@ describe('filterDiffFileCreations()', (): void => { describe('hasNumberOfCodeBlocksIncreased()', (): void => { it('should show which code blocks have increased', (): void => { const testDiffFragment = ` - +foo - +bar - +baz`; + +foo + +bar + +baz`; const testCodeBlocks = ['code block 1', 'foo', 'baz']; const actualResult = hasNumberOfCodeBlocksIncreased( @@ -69,7 +69,7 @@ describe('filterDiffByFilePath()', (): void => { it('should return the right diff for a generic matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, - '.*/.*.(js|ts)$|.*.(js|ts)$', + /^(.*\/)?.*\.(jsx)$/u, // Exclude jsx files ); expect(actualResult).toMatchInlineSnapshot(` @@ -93,35 +93,17 @@ describe('filterDiffByFilePath()', (): void => { }); it('should return the right diff for a specific file in any dir matcher', (): void => { - const actualResult = filterDiffByFilePath(testFileDiff, '.*old-file.js$'); + const actualResult = filterDiffByFilePath(testFileDiff, /.*old-file\.js$/u); // Exclude old-file.js expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js - index 57d5de75c..808d8ba37 100644 - --- a/old-file.js - +++ b/old-file.js - @@ -1,3 +1,8 @@ - +ping - @@ -34,33 +39,4 @@ - -pong" - `); - }); - - it('should return the right diff for a multiple file extension (OR) matcher', (): void => { - const actualResult = filterDiffByFilePath( - testFileDiff, - '^(./)*old-file.(js|ts|jsx)$', - ); - - expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js + "diff --git a/new-file.ts b/new-file.ts index 57d5de75c..808d8ba37 100644 - --- a/old-file.js - +++ b/old-file.js + --- a/new-file.ts + +++ b/new-file.ts @@ -1,3 +1,8 @@ - +ping + +foo @@ -34,33 +39,4 @@ - -pong + -bar diff --git a/old-file.jsx b/old-file.jsx index 57d5de75c..808d8ba37 100644 --- a/old-file.jsx @@ -133,10 +115,10 @@ describe('filterDiffByFilePath()', (): void => { `); }); - it('should return the right diff for a file name negation matcher', (): void => { + it('should return the right diff for a multiple file extension (OR) matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, - '^(?!.*old-file.js$).*.[a-zA-Z]+$', + /^(\.\/)*old-file\.(js|ts|jsx)$/u, // Exclude files named old-file that have js, ts, or jsx extensions ); expect(actualResult).toMatchInlineSnapshot(` @@ -147,15 +129,25 @@ describe('filterDiffByFilePath()', (): void => { @@ -1,3 +1,8 @@ +foo @@ -34,33 +39,4 @@ - -bar - diff --git a/old-file.jsx b/old-file.jsx - index 57d5de75c..808d8ba37 100644 - --- a/old-file.jsx - +++ b/old-file.jsx - @@ -1,3 +1,8 @@ - +yin - @@ -34,33 +39,4 @@ - -yang" + -bar" `); }); + + it('should return the right diff for a file name negation matcher', (): void => { + const actualResult = filterDiffByFilePath( + testFileDiff, + /^(?!.*old-file\.js$).*\.[a-zA-Z]+$/u, // Exclude files that do not end with old-file.js but include all other file extensions + ); + + expect(actualResult).toMatchInlineSnapshot(` + "diff --git a/old-file.js b/old-file.js + index 57d5de75c..808d8ba37 100644 + --- a/old-file.js + +++ b/old-file.js + @@ -1,3 +1,8 @@ + +ping + @@ -34,33 +39,4 @@ + -pong" + `); + }); }); diff --git a/development/fitness-functions/common/shared.ts b/development/fitness-functions/common/shared.ts index f7f22101378d..e96073ab5b27 100644 --- a/development/fitness-functions/common/shared.ts +++ b/development/fitness-functions/common/shared.ts @@ -1,11 +1,11 @@ -function filterDiffByFilePath(diff: string, regex: string): string { +function filterDiffByFilePath(diff: string, regex: RegExp): string { // split by `diff --git` and remove the first element which is empty const diffBlocks = diff.split(`diff --git`).slice(1); const filteredDiff = diffBlocks .map((block) => block.trim()) .filter((block) => { - let didAPathInBlockMatchRegEx = false; + let shouldCheckBlock = false; block // get the first line of the block which has the paths @@ -18,12 +18,13 @@ function filterDiffByFilePath(diff: string, regex: string): string { // if at least one of the two paths matches the regex, filter the // corresponding diff block in .forEach((path) => { - if (new RegExp(regex, 'u').test(path)) { - didAPathInBlockMatchRegEx = true; + if (!regex.test(path)) { + // Not excluded, include in check + shouldCheckBlock = true; } }); - return didAPathInBlockMatchRegEx; + return shouldCheckBlock; }) // prepend `git --diff` to each block .map((block) => `diff --git ${block}`) @@ -32,6 +33,34 @@ function filterDiffByFilePath(diff: string, regex: string): string { return filteredDiff; } +function restrictedFilePresent(diff: string, regex: RegExp): boolean { + // split by `diff --git` and remove the first element which is empty + const diffBlocks = diff.split(`diff --git`).slice(1); + let jsOrJsxFilePresent = false; + diffBlocks + .map((block) => block.trim()) + .filter((block) => { + block + // get the first line of the block which has the paths + .split('\n')[0] + .trim() + // split the two paths + .split(' ') + // remove `a/` and `b/` from the paths + .map((path) => path.substring(2)) + // if at least one of the two paths matches the regex, filter the + // corresponding diff block in + .forEach((path) => { + if (regex.test(path)) { + // Not excluded, include in check + jsOrJsxFilePresent = true; + } + }); + return jsOrJsxFilePresent; + }); + return jsOrJsxFilePresent; +} + // This function returns all lines that are additions to files that are being // modified but that previously already existed. Example: // diff --git a/test.js b/test.js @@ -44,7 +73,9 @@ function filterDiffLineAdditions(diff: string): string { const diffLines = diff.split('\n'); const diffAdditionLines = diffLines.filter((line) => { - const isAdditionLine = line.startsWith('+') && !line.startsWith('+++'); + const trimmedLine = line.trim(); + const isAdditionLine = + trimmedLine.startsWith('+') && !trimmedLine.startsWith('+++'); return isAdditionLine; }); @@ -108,6 +139,7 @@ function hasNumberOfCodeBlocksIncreased( export { filterDiffByFilePath, + restrictedFilePresent, filterDiffFileCreations, filterDiffLineAdditions, hasNumberOfCodeBlocksIncreased, diff --git a/development/fitness-functions/rules/index.ts b/development/fitness-functions/rules/index.ts index cd74d286093d..6ba0f1198684 100644 --- a/development/fitness-functions/rules/index.ts +++ b/development/fitness-functions/rules/index.ts @@ -5,23 +5,25 @@ const RULES: IRule[] = [ { name: "Don't use `sinon` or `assert` in unit tests", fn: preventSinonAssertSyntax, - docURL: - 'https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', + errorMessage: + '`sinon` or `assert` was detected in the diff. Please use Jest instead. For more info: https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', }, { - name: "Don't add JS or JSX files to the `shared` directory", + name: "Don't add JS or JSX files", fn: preventJavaScriptFileAdditions, + errorMessage: + 'The diff includes a newly created JS or JSX file. Please use TS or TSX instead.', }, ]; type IRule = { name: string; fn: (diff: string) => boolean; - docURL?: string; + errorMessage: string; }; function runFitnessFunctionRule(rule: IRule, diff: string): void { - const { name, fn, docURL } = rule; + const { name, fn, errorMessage } = rule; console.log(`Checking rule "${name}"...`); const hasRulePassed: boolean = fn(diff) as boolean; @@ -29,11 +31,7 @@ function runFitnessFunctionRule(rule: IRule, diff: string): void { console.log(`...OK`); } else { console.log(`...FAILED. Changes not accepted by the fitness function.`); - - if (docURL) { - console.log(`For more info: ${docURL}.`); - } - + console.log(errorMessage); process.exit(1); } } diff --git a/development/fitness-functions/rules/javascript-additions.test.ts b/development/fitness-functions/rules/javascript-additions.test.ts index db1803c1d9af..f1ae6e378e37 100644 --- a/development/fitness-functions/rules/javascript-additions.test.ts +++ b/development/fitness-functions/rules/javascript-additions.test.ts @@ -13,11 +13,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(true); }); - it('should pass when receiving a diff with a new TS file on the shared folder', (): void => { + it('should pass when receiving a diff with a new TS file folder', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.ts', 'yada yada yada yada'), + generateCreateFileDiff('app/test.ts', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); @@ -25,11 +25,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(true); }); - it('should not pass when receiving a diff with a new JS file on the shared folder', (): void => { + it('should not pass when receiving a diff with a new JS file', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.js', 'yada yada yada yada'), + generateCreateFileDiff('app/test.js', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); @@ -37,11 +37,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(false); }); - it('should not pass when receiving a diff with a new JSX file on the shared folder', (): void => { + it('should not pass when receiving a diff with a new JSX file', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.jsx', 'yada yada yada yada'), + generateCreateFileDiff('app/test.jsx', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); diff --git a/development/fitness-functions/rules/javascript-additions.ts b/development/fitness-functions/rules/javascript-additions.ts index 3e3705ea30f0..0d7c39b07110 100644 --- a/development/fitness-functions/rules/javascript-additions.ts +++ b/development/fitness-functions/rules/javascript-additions.ts @@ -1,15 +1,12 @@ -import { SHARED_FOLDER_JS_REGEX } from '../common/constants'; +import { JS_REGEX } from '../common/constants'; import { - filterDiffByFilePath, filterDiffFileCreations, + restrictedFilePresent, } from '../common/shared'; function preventJavaScriptFileAdditions(diff: string): boolean { - const sharedFolderDiff = filterDiffByFilePath(diff, SHARED_FOLDER_JS_REGEX); - const sharedFolderCreationDiff = filterDiffFileCreations(sharedFolderDiff); - - const hasCreatedAtLeastOneJSFileInShared = sharedFolderCreationDiff !== ''; - if (hasCreatedAtLeastOneJSFileInShared) { + const diffAdditions = filterDiffFileCreations(diff); + if (restrictedFilePresent(diffAdditions, JS_REGEX)) { return false; } return true; diff --git a/development/fitness-functions/rules/sinon-assert-syntax.ts b/development/fitness-functions/rules/sinon-assert-syntax.ts index 2cc56ec37762..a40c0768ad06 100644 --- a/development/fitness-functions/rules/sinon-assert-syntax.ts +++ b/development/fitness-functions/rules/sinon-assert-syntax.ts @@ -1,4 +1,4 @@ -import { EXCLUDE_E2E_TESTS_REGEX } from '../common/constants'; +import { E2E_TESTS_REGEX } from '../common/constants'; import { filterDiffByFilePath, filterDiffFileCreations, @@ -15,7 +15,7 @@ const codeBlocks = [ ]; function preventSinonAssertSyntax(diff: string): boolean { - const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX); + const diffByFilePath = filterDiffByFilePath(diff, E2E_TESTS_REGEX); const diffAdditions = filterDiffFileCreations(diffByFilePath); const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks); From f523617a9faaa70bbf96537067f06e6f1ec6d4b2 Mon Sep 17 00:00:00 2001 From: Frederik Bolding <frederik.bolding@gmail.com> Date: Tue, 15 Oct 2024 12:50:29 +0200 Subject: [PATCH 137/226] chore: Add react-beautiful-dnd to deprecated packages list (#27856) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Adds `react-beautiful-dnd` to list of deprecated packages that are ignored when using `yarn audit`. This unblocks `develop`. The package is currently in use for the network selection drag and drop functionality and cannot be removed. This PR also removes some packages from the list that were previously ignored, but are no longer in the dependency tree. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27856?quickstart=1) --- .yarnrc.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index 252333917781..f4d8fc7fa471 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -114,15 +114,9 @@ npmAuditIgnoreAdvisories: # upon old versions of ethereumjs-utils. - 'ethereum-cryptography (deprecation)' - # Currently only dependent on deprecated @metamask/types as it is brought in - # by @metamask/keyring-api. Updating the dependency in keyring-api will - # remove this. - - '@metamask/types (deprecation)' - - # @metamask/keyring-api also depends on @metamask/snaps-ui which is - # deprecated. Replacing that dependency with @metamask/snaps-sdk will remove - # this. - - '@metamask/snaps-ui (deprecation)' + # Currently in use for the network list drag and drop functionality. + # Maintenance has stopped and the project will be archived in 2025. + - 'react-beautiful-dnd (deprecation)' npmRegistries: 'https://npm.pkg.github.com': From 670d9cd18c0d23b5704e1009c2f0c1f1ec66da72 Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Tue, 15 Oct 2024 13:40:39 +0200 Subject: [PATCH 138/226] fix(multichain): fix eth send flow (from dapp) when a btc account is selected (#27566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The extension displays an error (with a stacktrace) whenever a user tries to start a "Send ETH" flow from a dapp while having a Bitcoin account being selected in the wallet. Some UI components rely on the currently selected account to display currencies/network logos, and since Eth is using hex-format when formatting amounts (while Bitcoin is using standard decimal numbers), then the "Send ETH" amount's could not be properly displayed since we were expecting a "Bitcoin number format" but the dapp is sending an hex-formatted number. To avoid having similar issues elsewhere, the `UserPreferencedCurrencyDisplay` component will now fallback to the original EVM behavior if the `account` property is omitted. Meaning that, in a multichain context, you will HAVE TO pass the `account` property to be able to display the correct currency for that account. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27566?quickstart=1) ## **Related issues** Fixes: - https://github.com/MetaMask/accounts-planning/issues/616 ## **Manual testing steps** 1. `yarn start:flask` 2. Settings > Experimental > Enable bitcoin support 3. Create a bitcoin account 4. Make sure to have the Bitcoin account being selected in your wallet 5. Go to: https://metamask.github.io/test-dapp/ 6. Connect 1 EVM account 7. Then use "Send" button from the "Send Eth" section 8. You should be able to display a Eth send confirmation on MM ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** ![Screenshot 2024-10-01 at 11 08 24](https://github.com/user-attachments/assets/a71aa68b-f0b7-4151-b1eb-0b83fe599032) ### **After** ![Screenshot 2024-10-02 at 15 29 52](https://github.com/user-attachments/assets/70c0eb70-6423-43e6-a06e-0e1b6afb841f) ## **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: Daniel Rocha <68558152+danroc@users.noreply.github.com> --- test/jest/mocks.ts | 3 + ...referenced-currency-display.component.d.ts | 2 + ...-preferenced-currency-display.component.js | 13 +- .../app/wallet-overview/coin-overview.tsx | 2 + ui/helpers/utils/util.js | 19 +++ ui/helpers/utils/util.test.js | 49 ++++++++ ui/selectors/multichain.ts | 7 +- ui/selectors/selectors.js | 18 +++ ui/selectors/selectors.test.js | 116 +++++++++++++++++- 9 files changed, 225 insertions(+), 4 deletions(-) diff --git a/test/jest/mocks.ts b/test/jest/mocks.ts index ed89b487e3ab..be1120429290 100644 --- a/test/jest/mocks.ts +++ b/test/jest/mocks.ts @@ -180,12 +180,14 @@ export function createMockInternalAccount({ address = MOCK_DEFAULT_ADDRESS, type = EthAccountType.Eoa, keyringType = KeyringTypes.hd, + lastSelected = 0, snapOptions = undefined, }: { name?: string; address?: string; type?: string; keyringType?: string; + lastSelected?: number; snapOptions?: { enabled: boolean; name: string; @@ -228,6 +230,7 @@ export function createMockInternalAccount({ type: keyringType, }, snap: snapOptions, + lastSelected, }, options: {}, methods, diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 3bf65d98d19c..4db61d568f4a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -1,3 +1,4 @@ +import { InternalAccount } from '@metamask/keyring-api'; import type { CurrencyDisplayProps } from '../../ui/currency-display/currency-display.component'; import type { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; @@ -5,6 +6,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< CurrencyDisplayProps, { type?: PRIMARY | SECONDARY; + account?: InternalAccount; currency?: string; showEthLogo?: boolean; ethNumberOfDecimals?: string | number; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 4b5492091288..613b731d0a16 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { EtherDenomination } from '../../../../shared/constants/common'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import CurrencyDisplay from '../../ui/currency-display'; @@ -10,13 +11,14 @@ import { getMultichainCurrentNetwork, } from '../../../selectors/multichain'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { getSelectedEvmInternalAccount } from '../../../selectors'; /* eslint-disable jsdoc/require-param-name */ // eslint-disable-next-line jsdoc/require-param /** @param {PropTypes.InferProps<typeof UserPreferencedCurrencyDisplayPropTypes>>} */ export default function UserPreferencedCurrencyDisplay({ 'data-testid': dataTestId, - account, + account: multichainAccount, ethNumberOfDecimals, fiatNumberOfDecimals, numberOfDecimals: propsNumberOfDecimals, @@ -28,6 +30,15 @@ export default function UserPreferencedCurrencyDisplay({ shouldCheckShowNativeToken, ...restProps }) { + // NOTE: When displaying currencies, we need the actual account to detect whether we're in a + // multichain world or EVM-only world. + // To preserve the original behavior of this component, we default to the lastly selected + // EVM accounts (when used in an EVM-only context). + // The caller has to pass the account in a multichain context to properly display the currency + // here (e.g for Bitcoin). + const evmAccount = useSelector(getSelectedEvmInternalAccount); + const account = multichainAccount ?? evmAccount; + const currentNetwork = useMultichainSelector( getMultichainCurrentNetwork, account, diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index c369ef0e89fd..2de787ef23c0 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -117,6 +117,7 @@ export const CoinOverview = ({ ///: END:ONLY_INCLUDE_IF + const account = useSelector(getSelectedAccount); const showNativeTokenAsMainBalanceRoute = getSpecificSettingsRoute( t, t('general'), @@ -254,6 +255,7 @@ export const CoinOverview = ({ {balanceToDisplay ? ( <UserPreferencedCurrencyDisplay style={{ display: 'contents' }} + account={account} className={classnames( `${classPrefix}-overview__primary-balance`, { diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index fe01bd15f5b1..eafc8e31bfe5 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -303,6 +303,25 @@ export function getAccountByAddress(accounts = [], targetAddress) { return accounts.find(({ address }) => address === targetAddress); } +/** + * Sort the given list of account their selecting order (descending). Meaning the + * first account of the sorted list will be the last selected account. + * + * @param {import('@metamask/keyring-api').InternalAccount[]} accounts - The internal accounts list. + * @returns {import('@metamask/keyring-api').InternalAccount[]} The sorted internal account list. + */ +export function sortSelectedInternalAccounts(accounts) { + // This logic comes from the `AccountsController`: + // TODO: Expose a free function from this controller and use it here + return accounts.sort((accountA, accountB) => { + // Sort by `.lastSelected` in descending order + return ( + (accountB.metadata.lastSelected ?? 0) - + (accountA.metadata.lastSelected ?? 0) + ); + }); +} + /** * Strips the following schemes from URL strings: * - http diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index dd2282efa531..d12a57675343 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -4,6 +4,7 @@ import { CHAIN_IDS } from '../../../shared/constants/network'; import { addHexPrefixToObjectValues } from '../../../shared/lib/swaps-utils'; import { toPrecisionWithoutTrailingZeros } from '../../../shared/lib/transactions-controller-utils'; import { MinPermissionAbstractionDisplayCount } from '../../../shared/constants/permissions'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; import * as util from './util'; describe('util', () => { @@ -1259,4 +1260,52 @@ describe('util', () => { expect(result).toBe(0); }); }); + + describe('sortSelectedInternalAccounts', () => { + const account1 = createMockInternalAccount({ lastSelected: 1 }); + const account2 = createMockInternalAccount({ lastSelected: 2 }); + const account3 = createMockInternalAccount({ lastSelected: 3 }); + // We use a big "gap" here to make sure we're not only sorting with sequential indexes + const accountWithBigSelectedIndexGap = createMockInternalAccount({ + lastSelected: 108912379837, + }); + // We wanna make sure that negative indexes are also being considered properly + const accountWithNegativeSelectedIndex = createMockInternalAccount({ + lastSelected: -1, + }); + + const orderedAccounts = [account3, account2, account1]; + + it.each([ + { accounts: [account1, account2, account3] }, + { accounts: [account2, account3, account1] }, + { accounts: [account3, account2, account1] }, + ])('sorts accounts by descending order: $accounts', ({ accounts }) => { + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount).toStrictEqual(orderedAccounts); + }); + + it('sorts accounts with bigger gap', () => { + const accounts = [account1, accountWithBigSelectedIndexGap, account3]; + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount.length).toBeGreaterThan(0); + expect(sortedAccount).toHaveLength(accounts.length); + expect(sortedAccount[0]).toStrictEqual(accountWithBigSelectedIndexGap); + }); + + it('sorts accounts with negative `lastSelected` index', () => { + const accounts = [account1, accountWithNegativeSelectedIndex, account3]; + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount.length).toBeGreaterThan(0); // Required since we using `length - 1` + expect(sortedAccount).toHaveLength(accounts.length); + expect(sortedAccount[sortedAccount.length - 1]).toStrictEqual( + accountWithNegativeSelectedIndex, + ); + }); + + it('succeed with no accounts', () => { + const sortedAccount = util.sortSelectedInternalAccounts([]); + expect(sortedAccount).toStrictEqual([]); + }); + }); }); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1148e8d86468..b676da209046 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -231,8 +231,11 @@ export function getMultichainProviderConfig( return getMultichainNetwork(state, account).network; } -export function getMultichainCurrentNetwork(state: MultichainState) { - return getMultichainProviderConfig(state); +export function getMultichainCurrentNetwork( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainProviderConfig(state, account); } export function getMultichainNativeCurrency( diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 2059c3a4678d..09c062012731 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -67,6 +67,7 @@ import { shortenAddress, getAccountByAddress, getURLHostName, + sortSelectedInternalAccounts, } from '../helpers/utils/util'; import { @@ -388,6 +389,23 @@ export function getInternalAccount(state, accountId) { return state.metamask.internalAccounts.accounts[accountId]; } +export const getEvmInternalAccounts = createSelector( + getInternalAccounts, + (accounts) => { + return accounts.filter((account) => isEvmAccountType(account.type)); + }, +); + +export const getSelectedEvmInternalAccount = createSelector( + getEvmInternalAccounts, + (accounts) => { + // We should always have 1 EVM account (if not, it would be `undefined`, same + // as `getSelectedInternalAccount` selector. + const [evmAccountSelected] = sortSelectedInternalAccounts(accounts); + return evmAccountSelected; + }, +); + /** * Returns an array of internal accounts sorted by keyring. * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 24b2a2afe125..8d71048e0924 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1,6 +1,10 @@ import { deepClone } from '@metamask/snaps-utils'; import { ApprovalType } from '@metamask/controller-utils'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { + BtcAccountType, + EthAccountType, + EthMethod, +} from '@metamask/keyring-api'; import { TransactionStatus } from '@metamask/transaction-controller'; import mockState from '../../test/data/mock-state.json'; import { KeyringType } from '../../shared/constants/keyring'; @@ -36,6 +40,21 @@ const modifyStateWithHWKeyring = (keyring) => { return modifiedState; }; +const mockAccountsState = (accounts) => { + const accountsMap = accounts.reduce((map, account) => { + map[account.id] = account; + return map; + }, {}); + + return { + metamask: { + internalAccounts: { + accounts: accountsMap, + }, + }, + }; +}; + describe('Selectors', () => { describe('#getSelectedAddress', () => { it('returns undefined if selectedAddress is undefined', () => { @@ -2080,4 +2099,99 @@ describe('#getConnectedSitesList', () => { ).toStrictEqual('INITIALIZED'); }); }); + + describe('getEvmInternalAccounts', () => { + const account1 = createMockInternalAccount({ + keyringType: KeyringType.hd, + }); + const account2 = createMockInternalAccount({ + type: EthAccountType.Erc4337, + keyringType: KeyringType.snap, + }); + const account3 = createMockInternalAccount({ + keyringType: KeyringType.imported, + }); + const account4 = createMockInternalAccount({ + keyringType: KeyringType.ledger, + }); + const account5 = createMockInternalAccount({ + keyringType: KeyringType.trezor, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + + const evmAccounts = [account1, account2, account3, account4, account5]; + + it('returns all EVM accounts when only EVM accounts are present', () => { + const state = mockAccountsState(evmAccounts); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('only returns EVM accounts when there are non-EVM accounts', () => { + const state = mockAccountsState([ + ...evmAccounts, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('returns an empty array when there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual([]); + }); + }); + + describe('getSelectedEvmInternalAccount', () => { + const account1 = createMockInternalAccount({ + lastSelected: 1, + }); + const account2 = createMockInternalAccount({ + lastSelected: 2, + }); + const account3 = createMockInternalAccount({ + lastSelected: 3, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 4, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 5, + }); + + it('returns the last selected EVM account', () => { + const state = mockAccountsState([account1, account2, account3]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns the last selected EVM account when there are non-EVM accounts', () => { + const state = mockAccountsState([ + account1, + account2, + account3, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns `undefined` if there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(undefined); + }); + }); }); From 0793e750c5ce571bc4549df5efd2701d47e1cf6c Mon Sep 17 00:00:00 2001 From: sahar-fehri <sahar.fehri@consensys.net> Date: Tue, 15 Oct 2024 15:30:56 +0200 Subject: [PATCH 139/226] fix: dismiss addToken modal for mmi (#27855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to fix dismissing modal to add suggested tokens when building mmi. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27855?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27854 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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: seaona <54408225+seaona@users.noreply.github.com> --- ui/store/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 91453590791c..9c5ab7ebb45e 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4020,7 +4020,7 @@ export function resolvePendingApproval( // Before closing the current window, check if any additional confirmations // are added as a result of this confirmation being accepted - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) const { pendingApprovals } = await forceUpdateMetamaskState(_dispatch); if (Object.values(pendingApprovals).length === 0) { _dispatch(closeCurrentNotificationWindow()); From f880da8114209a65c6466616fc89fe45587f1362 Mon Sep 17 00:00:00 2001 From: Jyoti Puri <jyotipuri@gmail.com> Date: Tue, 15 Oct 2024 19:02:22 +0530 Subject: [PATCH 140/226] fix: Reset nonce as network is switched (#27789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix issue with nonce being not reset when switching network. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27788 ## **Manual testing steps** 1. Go to test dapp 2. Switch network between submitting transactions 3. Ensure that nonce is correct each time ## **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. --- .../multichain/network-list-menu/network-list-menu.test.js | 6 ++++++ .../multichain/network-list-menu/network-list-menu.tsx | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js index c6be491a1aaa..c140189cbf81 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.test.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js @@ -19,11 +19,15 @@ const mockSetShowTestNetworks = jest.fn(); const mockToggleNetworkMenu = jest.fn(); const mockSetNetworkClientIdForDomain = jest.fn(); const mockSetActiveNetwork = jest.fn(); +const mockUpdateCustomNonce = jest.fn(); +const mockSetNextNonce = jest.fn(); jest.mock('../../../store/actions.ts', () => ({ setShowTestNetworks: () => mockSetShowTestNetworks, setActiveNetwork: () => mockSetActiveNetwork, toggleNetworkMenu: () => mockToggleNetworkMenu, + updateCustomNonce: () => mockUpdateCustomNonce, + setNextNonce: () => mockSetNextNonce, setNetworkClientIdForDomain: (network, id) => mockSetNetworkClientIdForDomain(network, id), })); @@ -206,6 +210,8 @@ describe('NetworkListMenu', () => { fireEvent.click(getByText(MAINNET_DISPLAY_NAME)); expect(mockToggleNetworkMenu).toHaveBeenCalled(); expect(mockSetActiveNetwork).toHaveBeenCalled(); + expect(mockUpdateCustomNonce).toHaveBeenCalled(); + expect(mockSetNextNonce).toHaveBeenCalled(); }); it('shows the correct selected network when networks share the same chain ID', () => { diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 6dc4457cceb5..5376dc17859e 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -26,6 +26,8 @@ import { setEditedNetwork, grantPermittedChain, showPermittedNetworkToast, + updateCustomNonce, + setNextNonce, } from '../../../store/actions'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, @@ -277,6 +279,8 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { network.rpcEndpoints[network.defaultRpcEndpointIndex]; dispatch(setActiveNetwork(networkClientId)); dispatch(toggleNetworkMenu()); + dispatch(updateCustomNonce('')); + dispatch(setNextNonce('')); if (permittedAccountAddresses.length > 0) { grantPermittedChain(selectedTabOrigin, network.chainId); From 85bf4c361cc243713b3aab87a637fb2769501e05 Mon Sep 17 00:00:00 2001 From: Matthew Walsh <matthew.walsh@consensys.net> Date: Tue, 15 Oct 2024 14:56:38 +0100 Subject: [PATCH 141/226] perf: include custom traces in benchmark results (#27701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Extend the `benchmark:chrome` and `benchmark:firefox` scripts to also record the duration of the startup custom traces. This provides a mechanism for developers to more easily test the impact of performance changes with reduced variance due to the ability to gather multiple samples. Sample from this pull request: ``` { "home": { ... "average": { "firstPaint": 1834.9400000002236, "domContentLoaded": 1802.5400000001305, "load": 1835.6650000001305, "domInteractive": 46.270000000298026, "backgroundConnect": 38.23001708984375, "firstReactRender": 86.8, "getState": 10.7, "initialActions": 0.15, "loadScripts": 1354.3799926757813, "setupStore": 23.3, "uiStartup": 2023.185009765625 }, ... } ``` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27701?quickstart=1) ## **Related issues** ## **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 - [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 | 28 +++++++++++++++++++++++----- test/e2e/benchmark.js | 17 ++++++++++++++++- test/e2e/webdriver/driver.js | 5 ++++- types/global.d.ts | 1 + 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index 5ca256371502..ab1deefd1cc5 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -32,6 +32,11 @@ const ID_DEFAULT = 'default'; const OP_DEFAULT = 'custom'; const tracesByKey: Map<string, PendingTrace> = new Map(); +const durationsByName: { [name: string]: number } = {}; + +if (process.env.IN_TEST && globalThis.stateHooks) { + globalThis.stateHooks.getCustomTraces = () => durationsByName; +} type PendingTrace = { end: (timestamp?: number) => void; @@ -155,9 +160,8 @@ export function endTrace(request: EndTraceRequest) { const { request: pendingRequest, startTime } = pendingTrace; const endTime = timestamp ?? getPerformanceTimestamp(); - const duration = endTime - startTime; - log('Finished trace', name, id, duration, { request: pendingRequest }); + logTrace(pendingRequest, startTime, endTime); } function traceCallback<T>(request: TraceRequest, fn: TraceCallback<T>): T { @@ -181,9 +185,7 @@ function traceCallback<T>(request: TraceRequest, fn: TraceCallback<T>): T { }, () => { const end = Date.now(); - const duration = end - start; - - log('Finished trace', name, duration, { error, request }); + logTrace(request, start, end, error); }, ) as T; }; @@ -242,6 +244,22 @@ function startSpan<T>( }); } +function logTrace( + request: TraceRequest, + startTime: number, + endTime: number, + error?: unknown, +) { + const duration = endTime - startTime; + const { name } = request; + + if (process.env.IN_TEST) { + durationsByName[name] = duration; + } + + log('Finished trace', name, duration, { request, error }); +} + function getTraceId(request: TraceRequest) { return request.id ?? ID_DEFAULT; } diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index 738d766f8555..1f24a960d9eb 100755 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -17,6 +17,16 @@ const FixtureBuilder = require('./fixture-builder'); const DEFAULT_NUM_SAMPLES = 20; const ALL_PAGES = Object.values(PAGES); +const CUSTOM_TRACES = { + backgroundConnect: 'Background Connect', + firstReactRender: 'First Render', + getState: 'Get State', + initialActions: 'Initial Actions', + loadScripts: 'Load Scripts', + setupStore: 'Setup Store', + uiStartup: 'UI Startup', +}; + async function measurePage(pageName) { let metrics; await withFixtures( @@ -32,6 +42,7 @@ async function measurePage(pageName) { await driver.findElement('[data-testid="account-menu-icon"]'); await driver.navigate(pageName); await driver.delay(1000); + metrics = await driver.collectMetrics(); }, ); @@ -79,7 +90,7 @@ async function profilePageLoad(pages, numSamples, retries) { runResults.push(result); } - if (runResults.some((result) => result.navigation.lenth > 1)) { + if (runResults.some((result) => result.navigation.length > 1)) { throw new Error(`Multiple navigations not supported`); } else if ( runResults.some((result) => result.navigation[0].type !== 'navigate') @@ -107,6 +118,10 @@ async function profilePageLoad(pages, numSamples, retries) { ), }; + for (const [key, name] of Object.entries(CUSTOM_TRACES)) { + result[key] = runResults.map((metrics) => metrics[name]); + } + results[pageName] = { min: minResult(result), max: maxResult(result), diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index a03a0d1cbd04..fb8aed3d28a6 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -1313,7 +1313,10 @@ function collectMetrics() { }); }); - return results; + return { + ...results, + ...window.stateHooks.getCustomTraces(), + }; } module.exports = { Driver, PAGES }; diff --git a/types/global.d.ts b/types/global.d.ts index 95fb6c98547a..8078a3998bde 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -239,6 +239,7 @@ type HttpProvider = { }; type StateHooks = { + getCustomTraces?: () => { [name: string]: number }; getCleanAppState?: () => Promise<any>; getLogs?: () => any[]; getMostRecentPersistedState?: () => any; From 3452eb915c8eb9fd92047701059bdbd50fda07ae Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:57:33 +0200 Subject: [PATCH 142/226] fix: flaky test `MultiRpc: should select rpc from settings @no-mmi` (#27858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27858?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27851 ## **Manual testing steps** 1. Check ci 2. Run test locally ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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/network/multi-rpc.spec.ts | 27 +++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index c9fa95f986e9..af2ef47e93fb 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -396,12 +396,10 @@ describe('MultiRpc:', function (this: Suite) { await driver.delay(regularDelayMs); // go to advanced settigns - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Manage default settings', }); - await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'General', }); @@ -420,23 +418,18 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Save', tag: 'button', }); - await driver.delay(regularDelayMs); - await driver.waitForSelector('[data-testid="category-back-button"]'); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.waitForSelector( - '[data-testid="privacy-settings-back-button"]', - ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -446,7 +439,7 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -461,7 +454,17 @@ describe('MultiRpc:', function (this: Suite) { '“Arbitrum One” was successfully edited!', ); // Ensures popover backround doesn't kill test - await driver.delay(regularDelayMs); + await driver.assertElementNotPresent('.popover-bg'); + + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ tag: 'h6', text: 'Got it' }); + + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); + await driver.clickElement('[data-testid="network-display"]'); const arbitrumRpcUsed = await driver.findElement({ From 58142243e4a9626113a7a45ccdf48ff3b03797bc Mon Sep 17 00:00:00 2001 From: martahj <marta.hourigan.johnson@gmail.com> Date: Tue, 15 Oct 2024 10:16:18 -0500 Subject: [PATCH 143/226] fix: hackily wait longer for linea swap approval tx to increase chance of success (#27810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Linea seems to be taking longer than other chains to process the approve transaction after it is submitted, so the trade transaction is erroring. This PR adds a hacky workaround where we artificially delay if we're on Linea to give the trade transaction more time. In the future, we'd want to avoid this hack, but for now it should increase the swap success rate on Linea. With the delay, the token symbol also wasn't immediately populating on the awaiting swap page, so this PR also updates how it's retrieved. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27810?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27804 ## **Manual testing steps** 1. Start a swap on Linea with a token that you have not granted approval for 2. Observe that the swap does not fail ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/57cdc5e5-cea7-48ad-ba13-38820ecc9155 ### **After** https://github.com/user-attachments/assets/91bbfbf4-8392-41ea-bfe8-d54813758f5c ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/ducks/swaps/swaps.js | 16 ++++++++++++++ ui/pages/swaps/awaiting-swap/awaiting-swap.js | 21 +++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index efbd781f943f..cf8348243238 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -6,6 +6,7 @@ import { captureMessage } from '@sentry/browser'; import { TransactionType } from '@metamask/transaction-controller'; import { createProjectLogger } from '@metamask/utils'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { addToken, addTransactionAndWaitForPublish, @@ -1284,6 +1285,21 @@ export const signAndSendTransactions = ( }, }, ); + if ( + [ + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.LINEA_GOERLI, + CHAIN_IDS.LINEA_SEPOLIA, + ].includes(chainId) + ) { + debugLog( + 'Delaying submitting trade tx to make Linea confirmation more likely', + ); + const waitPromise = new Promise((resolve) => + setTimeout(resolve, 5000), + ); + await waitPromise; + } } catch (e) { debugLog('Approve transaction failed', e); await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR)); diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index e7f47bc3f006..660f7ef4fcae 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -97,6 +97,9 @@ export default function AwaitingSwap({ const [trackedQuotesExpiredEvent, setTrackedQuotesExpiredEvent] = useState(false); + const destinationTokenSymbol = + usedQuote?.destinationTokenInfo?.symbol || swapMetaData?.token_to; + let feeinUnformattedFiat; if (usedQuote && swapsGasPrice) { @@ -107,7 +110,7 @@ export default function AwaitingSwap({ currentCurrency, conversionRate: usdConversionRate, tradeValue: usedQuote?.trade?.value, - sourceSymbol: swapMetaData?.token_from, + sourceSymbol: usedQuote?.sourceTokenInfo?.symbol, sourceAmount: usedQuote.sourceAmount, chainId, }); @@ -123,13 +126,14 @@ export default function AwaitingSwap({ const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); + const swapSlippage = swapMetaData?.slippage || usedQuote?.slippage; const sensitiveProperties = { - token_from: swapMetaData?.token_from, + token_from: swapMetaData?.token_from || usedQuote?.sourceTokenInfo?.symbol, token_from_amount: swapMetaData?.token_from_amount, - token_to: swapMetaData?.token_to, + token_to: destinationTokenSymbol, request_type: fetchParams?.balanceError ? 'Quote' : 'Order', - slippage: swapMetaData?.slippage, - custom_slippage: swapMetaData?.slippage === 2, + slippage: swapSlippage, + custom_slippage: swapSlippage === 2, gas_fees: feeinUnformattedFiat, is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, @@ -137,7 +141,6 @@ export default function AwaitingSwap({ current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }; - const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? @@ -234,7 +237,7 @@ export default function AwaitingSwap({ className="awaiting-swap__amount-and-symbol" data-testid="awaiting-swap-amount-and-symbol" > - {swapMetaData?.token_to} + {destinationTokenSymbol} </span>, ]); content = blockExplorerUrl && ( @@ -252,7 +255,7 @@ export default function AwaitingSwap({ key="swapTokenAvailable-2" className="awaiting-swap__amount-and-symbol" > - {`${tokensReceived || ''} ${swapMetaData?.token_to}`} + {`${tokensReceived || ''} ${destinationTokenSymbol}`} </span>, ]); content = blockExplorerUrl && ( @@ -317,7 +320,7 @@ export default function AwaitingSwap({ } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)); } else if ( - isSwapsDefaultTokenSymbol(swapMetaData?.token_to, chainId) || + isSwapsDefaultTokenSymbol(destinationTokenSymbol, chainId) || swapComplete ) { history.push(DEFAULT_ROUTE); From 2b45577566518deff4cbacd557660b7eef0657b5 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:27:17 +0200 Subject: [PATCH 144/226] fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi` (#27834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** There is a race condition that after clicking Add Account button, for adding an account, the dialog remains open and the account is not added. Then the test fails when trying to click an element which is below the dialog: `ElementClickInterceptedError: element click intercepted:` ![Screenshot from 2024-10-14 17-40-45](https://github.com/user-attachments/assets/8cb59dfd-d662-460e-a228-a1e033bdecba) ![image](https://github.com/user-attachments/assets/ee45e300-f13b-4daa-a667-b4ea8257483a) Unfortunately there is no UI condition we can wait for, to know when the form is ready after adding our input, given that the Add Account button is enabled from start, so the click will never fail, despite the component not being ready/needing to update. You can see an illustration of this in the video below, despite not being able to reproduce it locally. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27834?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27837 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** See how there is no UI way to tell we can click the button, as it is already enabled. Then, when we update the input and quickly click Add Account, unexpected things can happen: ie in this case the last updated value is not applied https://github.com/user-attachments/assets/982b2d33-1bd0-42e0-99a3-0f13fa571620 ## **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/tests/account/add-account.spec.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js index c1a6136cc47d..04980cf20c3e 100644 --- a/test/e2e/tests/account/add-account.spec.js +++ b/test/e2e/tests/account/add-account.spec.js @@ -42,6 +42,9 @@ describe('Add account', function () { ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.findElement({ css: '[data-testid="account-menu-icon"]', @@ -86,6 +89,9 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); // Check address of 2nd account @@ -109,16 +115,11 @@ describe('Add account', function () { '[data-testid="account-options-menu-button"]', ); - await driver.delay(regularDelayMs); - await driver.waitForSelector('[data-testid="global-menu-lock"]'); await driver.clickElement('[data-testid="global-menu-lock"]'); await driver.waitForSelector('[data-testid="unlock-page"]'); // Recover via SRP in "forget password" option - const restoreSeedLink = await driver.findClickableElement( - '.unlock-page__link', - ); - await restoreSeedLink.click(); + await driver.clickElement('.unlock-page__link'); await driver.pasteIntoField( '[data-testid="import-srp__srp-word-0"]', TEST_SEED_PHRASE, @@ -126,7 +127,6 @@ describe('Add account', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.fill('#confirm-password', 'correct horse battery staple'); - await driver.delay(regularDelayMs); await driver.clickElement( '[data-testid="create-new-vault-submit-button"]', ); @@ -171,6 +171,9 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); // Wait for 2nd account to be created From 581b7fb9cf08552096f4eb08375447b44d057f87 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari <nidhi.kumari@consensys.net> Date: Tue, 15 Oct 2024 16:34:37 +0100 Subject: [PATCH 145/226] fix: updated permissions flow copy changes (#27658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to update the copy changes in Permissions Screen ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/3391](https://github.com/MetaMask/MetaMask-planning/issues/3391) ## **Manual testing steps** 1. Run extension with yarn start 2. For all permissions page, there should be no extra space between "[x] accounts" and the "•" in the list item description 3. Click on connected permission, On Permissions Page check 4px spacing between the Permission label and the secondary description 4. Click Disconnect button and the Modal copy should be "If you disconnect from this site, you’ll need to reconnect your accounts and networks to use this site again." 5. When no account is connected, copy should be updated as well For tooltips, I will file a separate issue and we are not adding any badge for avatars on permissions page ## **Screenshots/Recordings** ### **Before** ### **After** ![Screenshot 2024-10-07 at 1 35 23 PM](https://github.com/user-attachments/assets/d6c4c14a-d514-449e-bc3d-25925335d144) ![Screenshot 2024-10-07 at 1 36 35 PM](https://github.com/user-attachments/assets/75fd34e9-a17a-470f-9788-982a2248c638) ![Screenshot 2024-10-07 at 1 36 47 PM](https://github.com/user-attachments/assets/d94c8768-7892-4759-81e3-3e585e763595) ![Screenshot 2024-10-07 at 1 37 12 PM](https://github.com/user-attachments/assets/08abec2c-3abd-4020-86e9-53815305ac9d) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/de/messages.json | 3 --- app/_locales/el/messages.json | 3 --- app/_locales/en/messages.json | 11 +++++----- app/_locales/es/messages.json | 3 --- app/_locales/fr/messages.json | 3 --- app/_locales/hi/messages.json | 3 --- app/_locales/id/messages.json | 3 --- app/_locales/ja/messages.json | 3 --- app/_locales/ko/messages.json | 3 --- app/_locales/pt/messages.json | 3 --- app/_locales/ru/messages.json | 3 --- app/_locales/tl/messages.json | 3 --- app/_locales/tr/messages.json | 3 --- app/_locales/vi/messages.json | 3 --- app/_locales/zh_CN/messages.json | 3 --- .../disconnect-all-modal.tsx | 3 +-- .../no-connections.test.tsx.snap | 2 +- .../connections/components/no-connection.tsx | 2 +- .../permissions-page.test.js.snap | 2 +- .../permissions-page/connection-list-item.js | 3 +-- .../review-permissions-page.test.tsx.snap | 12 ++--------- .../review-permissions-page.tsx | 20 +++++++++++-------- ...ite-cell-connection-list-item.test.js.snap | 2 +- .../site-cell-connection-list-item.js | 1 + .../__snapshots__/connect-page.test.tsx.snap | 4 ++-- 25 files changed, 28 insertions(+), 76 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 9931e17a83a7..296f1c716297 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Keine Konten für die angegebene Suchanfrage gefunden" }, - "noConnectedAccountDescription": { - "message": "Wählen Sie ein Konto, das Sie auf dieser Website verwenden möchten, um fortzufahren." - }, "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 95e1e43cf51f..6adad0a49176 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Δεν βρέθηκαν λογαριασμοί για το συγκεκριμένο αίτημα αναζήτησης" }, - "noConnectedAccountDescription": { - "message": "Επιλέξτε έναν λογαριασμό που θέλετε να χρησιμοποιήσετε σε αυτόν τον ιστότοπο για να συνεχίσετε." - }, "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 862b761abd8f..6970fbb4473c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1633,9 +1633,8 @@ "disconnectAllAccountsText": { "message": "accounts" }, - "disconnectAllDescription": { - "message": "If you disconnect from $1, you’ll need to reconnect your accounts and networks to use this site again.", - "description": "$1 represents the website hostname" + "disconnectAllDescriptionText": { + "message": "If you disconnect from this site, you’ll need to reconnect your accounts and networks to use this site again." }, "disconnectAllSnapsText": { "message": "Snaps" @@ -3313,12 +3312,12 @@ "noAccountsFound": { "message": "No accounts found for the given search query" }, - "noConnectedAccountDescription": { - "message": "Select an account you want to use on this site to continue." - }, "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, + "noConnectionDescription": { + "message": "To connect to a site, find and select the \"connect\" button. Remember MetaMask can only connect to sites on web3" + }, "noConversionRateAvailable": { "message": "No conversion rate available" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 9fd0f3d20941..03fa1e519ef7 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -3028,9 +3028,6 @@ "noAccountsFound": { "message": "No se encuentran cuentas para la consulta de búsqueda determinada" }, - "noConnectedAccountDescription": { - "message": "Seleccione una cuenta que desee utilizar en este sitio para continuar." - }, "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 05c67e49462f..fccc617dee93 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Aucun compte trouvé pour la demande de recherche effectuée" }, - "noConnectedAccountDescription": { - "message": "Sélectionnez un compte que vous souhaitez utiliser sur ce site pour continuer." - }, "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index e23b10a874f0..7333626d1e30 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "दी गई खोज क्वेरी के लिए कोई अकाउंट नहीं मिला" }, - "noConnectedAccountDescription": { - "message": "जारी रखने के लिए जिस अकाउंट को आप इस साइट पर उपयोग करना चाहते हैं वह अकाउंट चुनें।" - }, "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 12ab926cf9ce..f2d8828e9226 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Tidak ditemukan akun untuk kueri pencarian yang diberikan" }, - "noConnectedAccountDescription": { - "message": "Pilih akun yang ingin Anda gunakan di situs ini untuk melanjutkan." - }, "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 0c3887643691..cadc1ab1e302 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "指定された検索クエリでアカウントが見つかりませんでした" }, - "noConnectedAccountDescription": { - "message": "続行するには、このサイトで使用するアカウントを選択してください。" - }, "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6e4dad181512..760ad7df43dc 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "검색어에 해당하는 계정이 없습니다." }, - "noConnectedAccountDescription": { - "message": "이 사이트에서 계속 사용하고자 하는 계정을 선택하세요." - }, "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 47a53a6ed328..06c9fbe38adf 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Nenhuma conta encontrada para a pesquisa efetuada" }, - "noConnectedAccountDescription": { - "message": "Selecione uma conta que você deseja usar neste site para continuar." - }, "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 0ecd4f0eb8d6..2308eb10721e 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "По данному поисковому запросу счетов не найдено" }, - "noConnectedAccountDescription": { - "message": "Для продолжения выберите счет, который вы хотите использовать на этом сайте." - }, "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index c6614483aa5b..8e3d8fd7fdd0 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Walang nakitang account para sa ibinigay na query sa paghahanap" }, - "noConnectedAccountDescription": { - "message": "Pumili ng account na gusto mong gamitin sa site na ito para magpatuloy." - }, "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 361b92cdd87e..06d2f1de953f 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Belirtilen arama sorgusu için hesap bulunamadı" }, - "noConnectedAccountDescription": { - "message": "Devam etmek için bu sitede kullanmak istediğiniz bir hesap seçin." - }, "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 3be725af9351..89772c1d4eec 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Không tìm thấy tài khoản nào cho cụm từ tìm kiếm đã đưa ra" }, - "noConnectedAccountDescription": { - "message": "Chọn tài khoản mà bạn muốn sử dụng trên trang web này để tiếp tục." - }, "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 58298abdf542..b4816b165545 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "未找到符合给定查询条件的账户" }, - "noConnectedAccountDescription": { - "message": "选择要在此站点上使用的账户以继续。" - }, "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, diff --git a/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx b/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx index 62ca0ed8093a..0170abc79fc7 100644 --- a/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx +++ b/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx @@ -19,7 +19,6 @@ export enum DisconnectType { } export const DisconnectAllModal = ({ - hostname, onClick, onClose, }: { @@ -36,7 +35,7 @@ export const DisconnectAllModal = ({ <ModalContent> <ModalHeader onClose={onClose}>{t('disconnect')}</ModalHeader> <ModalBody> - {<Text>{t('disconnectAllDescription', [hostname])}</Text>} + {<Text>{t('disconnectAllDescriptionText')}</Text>} </ModalBody> <ModalFooter> <Button diff --git a/ui/components/multichain/pages/connections/components/__snapshots__/no-connections.test.tsx.snap b/ui/components/multichain/pages/connections/components/__snapshots__/no-connections.test.tsx.snap index 90139835245a..8d6dd33f9028 100644 --- a/ui/components/multichain/pages/connections/components/__snapshots__/no-connections.test.tsx.snap +++ b/ui/components/multichain/pages/connections/components/__snapshots__/no-connections.test.tsx.snap @@ -13,7 +13,7 @@ exports[`No Connections Content should render correctly 1`] = ` <p class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--color-text-default" > - Select an account you want to use on this site to continue. + To connect to a site, find and select the "connect" button. Remember MetaMask can only connect to sites on web3 </p> </div> </div> diff --git a/ui/components/multichain/pages/connections/components/no-connection.tsx b/ui/components/multichain/pages/connections/components/no-connection.tsx index 3f5e812274da..659c6112b511 100644 --- a/ui/components/multichain/pages/connections/components/no-connection.tsx +++ b/ui/components/multichain/pages/connections/components/no-connection.tsx @@ -28,7 +28,7 @@ export const NoConnectionContent = () => { </Text> <Text variant={TextVariant.bodyMd} textAlign={TextAlign.Center}> - {t('noConnectedAccountDescription')} + {t('noConnectionDescription')} </Text> </Box> ); diff --git a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap index 66dbd90aeea2..c49cd1ba3236 100644 --- a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap +++ b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap @@ -83,7 +83,7 @@ exports[`All Connections render renders correctly 1`] = ` 1 accounts -   •  + •  0 networks diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.js b/ui/components/multichain/pages/permissions-page/connection-list-item.js index 725499b30841..16b2b0976ec6 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.js @@ -88,8 +88,7 @@ export const ConnectionListItem = ({ connection, onClick }) => { color={TextColor.textAlternative} variant={TextVariant.bodyMd} > - {connection.addresses.length} {t('accountsSmallCase')}  - •  + {connection.addresses.length} {t('accountsSmallCase')} •  {connectedNetworks.length} {t('networksSmallCase')} </Text> </Box> diff --git a/ui/components/multichain/pages/review-permissions-page/__snapshots__/review-permissions-page.test.tsx.snap b/ui/components/multichain/pages/review-permissions-page/__snapshots__/review-permissions-page.test.tsx.snap index 25289f1e3b7a..97efc908271e 100644 --- a/ui/components/multichain/pages/review-permissions-page/__snapshots__/review-permissions-page.test.tsx.snap +++ b/ui/components/multichain/pages/review-permissions-page/__snapshots__/review-permissions-page.test.tsx.snap @@ -60,21 +60,13 @@ exports[`ReviewPermissions should render correctly 1`] = ` <p class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--color-text-default" > - Select an account you want to use on this site to continue. + To connect to a site, find and select the "connect" button. Remember MetaMask can only connect to sites on web3 </p> </div> </div> <div class="mm-box multichain-page-footer mm-box--padding-4 mm-box--display-flex mm-box--gap-4 mm-box--width-full" - > - <button - class="mm-box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-primary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" - data-test-id="no-connections-button" - data-theme="light" - > - Connect accounts - </button> - </div> + /> </div> </div> </div> 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 022b508984cd..b12fea776c65 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 @@ -265,14 +265,18 @@ export const ReviewPermissions = () => { </Button> </Box> ) : ( - <ButtonPrimary - size={ButtonPrimarySize.Lg} - block - data-test-id="no-connections-button" - onClick={requestAccountsAndChainPermissions} - > - {t('connectAccounts')} - </ButtonPrimary> + <> + {connectedAccountAddresses.length > 0 ? ( + <ButtonPrimary + size={ButtonPrimarySize.Lg} + block + data-test-id="no-connections-button" + onClick={requestAccountsAndChainPermissions} + > + {t('connectAccounts')} + </ButtonPrimary> + ) : null} + </> )} </> </Footer> diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap index ae198ab79882..fba510aba170 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap @@ -15,7 +15,7 @@ exports[`SiteCellConnectionListItem renders correctly with required props 1`] = /> </div> <div - class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--width-5/12" + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column mm-box--width-5/12" style="align-self: center; flex-grow: 1;" > <p diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js index be2aafb7257b..ccebc77931f6 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell-connection-list-item.js @@ -57,6 +57,7 @@ export const SiteCellConnectionListItem = ({ flexDirection={FlexDirection.Column} width={BlockSize.FiveTwelfths} style={{ alignSelf: 'center', flexGrow: 1 }} + gap={1} > <Text variant={TextVariant.bodyMd} textAlign={TextAlign.Left}> {title} 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 6353df3e96cc..e416011c1b08 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 @@ -51,7 +51,7 @@ exports[`ConnectPage should render correctly 1`] = ` /> </div> <div - class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--width-5/12" + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column mm-box--width-5/12" style="align-self: center; flex-grow: 1;" > <p @@ -132,7 +132,7 @@ exports[`ConnectPage should render correctly 1`] = ` /> </div> <div - class="mm-box mm-box--display-flex mm-box--flex-direction-column mm-box--width-5/12" + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column mm-box--width-5/12" style="align-self: center; flex-grow: 1;" > <p From b410e09081c9885fc616f7be1e83245d172748b4 Mon Sep 17 00:00:00 2001 From: Jony Bursztyn <jony.bursztyn@consensys.net> Date: Tue, 15 Oct 2024 17:07:03 +0100 Subject: [PATCH 146/226] feat: update copy for 'Default settings' (#27821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Updates the copy for the onboarding message screen and the onboarding settings screen. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27821?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3496 ## **Manual testing steps** 1. For the following scenarios: a. Wallet created w backup b. Wallet created w/o backup c. Wallet imported 3. The copy of the success screens and the settings screen should be updated as shown in the Screenshots: ## **Screenshots/Recordings** <img width="638" alt="Screenshot 2024-10-14 at 12 56 40" src="https://github.com/user-attachments/assets/4fd02acf-0587-4917-8760-cd59d5f3f196"> <img width="623" alt="Screenshot 2024-10-14 at 12 57 23" src="https://github.com/user-attachments/assets/32a9cbed-dc63-4170-97f1-5d3ccabbcf51"> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 | 4 ++-- test/e2e/helpers.js | 5 ++++- test/e2e/tests/network/multi-rpc.spec.ts | 2 +- test/e2e/tests/onboarding/onboarding.spec.js | 4 ++-- test/e2e/tests/privacy/basic-functionality.spec.js | 4 ++-- .../creation-successful/creation-successful.test.js | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6970fbb4473c..57dd9152752e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1548,7 +1548,7 @@ "message": "MetaMask uses default settings to best balance safety and ease of use. Change these settings to further increase your privacy." }, "defaultSettingsTitle": { - "message": "Default settings" + "message": "Default privacy settings" }, "delete": { "message": "Delete" @@ -2846,7 +2846,7 @@ "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, "manageDefaultSettings": { - "message": "Manage default settings" + "message": "Manage default privacy settings" }, "marketCap": { "message": "Market cap" diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 926b152e899b..65a405f5325d 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -566,7 +566,10 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); // opt-out from third party API on general section - await driver.clickElement({ text: 'Manage default settings', tag: 'button' }); + await driver.clickElement({ + text: 'Manage default privacy settings', + tag: 'button', + }); await driver.clickElement({ text: 'General', tag: 'p' }); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index af2ef47e93fb..6fc7025f5dbc 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -397,7 +397,7 @@ describe('MultiRpc:', function (this: Suite) { // go to advanced settigns await driver.clickElementAndWaitToDisappear({ - text: 'Manage default settings', + text: 'Manage default privacy settings', }); await driver.clickElement({ diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 1b15dba5ddd7..8d6b00de07ed 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -280,7 +280,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); @@ -402,7 +402,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index 062a0345a39a..b4fc0e138104 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -60,7 +60,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); @@ -130,7 +130,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js index 5349a9f23f9e..9438f3859ff1 100644 --- a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js +++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js @@ -116,9 +116,9 @@ describe('Creation Successful Onboarding View', () => { ).toBeInTheDocument(); }); - it('should redirect to privacy-settings view when "Manage default settings" button is clicked', () => { + it('should redirect to privacy-settings view when "Manage default privacy settings" button is clicked', () => { const { getByText } = renderWithProvider(<CreationSuccessful />, store); - const privacySettingsButton = getByText('Manage default settings'); + const privacySettingsButton = getByText('Manage default privacy settings'); fireEvent.click(privacySettingsButton); expect(mockHistoryPush).toHaveBeenCalledWith( ONBOARDING_PRIVACY_SETTINGS_ROUTE, From bd018b20e77266de009355a125956a3c7f0c3216 Mon Sep 17 00:00:00 2001 From: Harika <153644847+hjetpoluru@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:18:34 -0400 Subject: [PATCH 147/226] fix: "Update Network: should update added rpc url for exis..." flaky tests (#27437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR addresses a flaky test issue caused by a popover dialog that appears. The presence of this popover dialog prevents the test from interacting with the intended element leading to test failures. To resolve this, I have included the clickSafeElement method to safely click the "Got it" button on the popover dialog. ![image](https://github.com/user-attachments/assets/5eb874d5-65bd-423a-aeab-6bf2cb628081) <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27437?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27422 ## **Manual testing steps** Run the test using below commands locally or in codespaces: yarn yarn build:test:webpack ENABLE_MV3=false yarn test:e2e:single test/e2e/tests/network/update-network.spec.ts --browser=chrome ## **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). - [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: seaona <54408225+seaona@users.noreply.github.com> --- test/e2e/tests/network/update-network.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/e2e/tests/network/update-network.spec.ts b/test/e2e/tests/network/update-network.spec.ts index 08b1bc570c83..3f0b9882688f 100644 --- a/test/e2e/tests/network/update-network.spec.ts +++ b/test/e2e/tests/network/update-network.spec.ts @@ -240,7 +240,13 @@ describe('Update Network:', function (this: Suite) { // Re-open the network menu await driver.delay(regularDelayMs); + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed await driver.clickElementSafe({ text: 'Got it', tag: 'h6' }); + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); await driver.clickElement('[data-testid="network-display"]'); // Go back to edit the network @@ -360,6 +366,13 @@ describe('Update Network:', function (this: Suite) { // Re-open the network menu await driver.delay(regularDelayMs); + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ text: 'Got it', tag: 'h6' }); + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); await driver.clickElement('[data-testid="network-display"]'); // Go back to edit the network From 988156b60bae62a2e2e81f04a3f8a8d5ad8c6f2d Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:33:03 +0200 Subject: [PATCH 148/226] feat: use messenger in AccountTracker to get Preferences state (#27711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR updates Account Tracker to retrieve the state from PreferencesController via the messenger, replacing the use of the state callback. All the unit tests were incorrectly passing before, but this issue has now been fixed. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27711?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .eslintrc.js | 1 + .../account-tracker-controller.test.ts | 111 +++++++++++------- .../controllers/account-tracker-controller.ts | 28 ++--- app/scripts/metamask-controller.js | 2 +- 4 files changed, 87 insertions(+), 55 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a53619b179ca..258556239ac3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -317,6 +317,7 @@ module.exports = { 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/controllers/preferences-controller.test.ts', + 'app/scripts/controllers/account-tracker-controller.test.ts', 'app/scripts/lib/**/*.test.js', 'app/scripts/metamask-controller.test.js', 'app/scripts/migrations/*.test.js', diff --git a/app/scripts/controllers/account-tracker-controller.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts index ad33541fb5b6..7456244fc5a4 100644 --- a/app/scripts/controllers/account-tracker-controller.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -32,7 +32,7 @@ const GAS_LIMIT_HOOK = '0x222222'; // The below three values were generated by running MetaMask in the browser // The response to eth_call, which is called via `ethContract.balances` -// in `_updateAccountsViaBalanceChecker` of account-tracker.js, needs to be properly +// in `#updateAccountsViaBalanceChecker` of account-tracker-controller.ts, needs to be properly // formatted or else ethers will throw an error. const ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN = '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800600000000000000000000000000000000000000000000000000000000000186a0'; @@ -85,9 +85,9 @@ type WithControllerArgs<ReturnValue> = | [WithControllerCallback<ReturnValue>] | [WithControllerOptions, WithControllerCallback<ReturnValue>]; -function withController<ReturnValue>( +async function withController<ReturnValue>( ...args: WithControllerArgs<ReturnValue> -): ReturnValue { +): Promise<ReturnValue> { const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; const { completedOnboarding = false, @@ -127,8 +127,8 @@ function withController<ReturnValue>( eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, }, - networkId: '0x1', - chainId: '0x1', + networkId: 'selectedNetworkId', + chainId: currentChainId, }); const getNetworkStateStub = jest.fn().mockReturnValue({ @@ -160,14 +160,19 @@ function withController<ReturnValue>( getOnboardingControllerState, ); + const getPreferencesControllerState = jest.fn().mockReturnValue({ + useMultiAccountBalanceChecker, + }); + controllerMessenger.registerActionHandler( + 'PreferencesController:getState', + getPreferencesControllerState, + ); + const controller = new AccountTrackerController({ state: getDefaultAccountTrackerControllerState(), provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), - preferencesControllerState: { - useMultiAccountBalanceChecker, - }, messenger: controllerMessenger.getRestricted({ name: 'AccountTrackerController', allowedActions: [ @@ -175,6 +180,7 @@ function withController<ReturnValue>( 'NetworkController:getState', 'NetworkController:getNetworkClientById', 'OnboardingController:getState', + 'PreferencesController:getState', ], allowedEvents: [ 'AccountsController:selectedEvmAccountChange', @@ -185,7 +191,7 @@ function withController<ReturnValue>( ...accountTrackerOptions, }); - return fn({ + return await fn({ controller, blockTrackerFromHookStub, blockTrackerStub, @@ -198,7 +204,7 @@ function withController<ReturnValue>( describe('AccountTrackerController', () => { describe('start', () => { it('restarts the subscription to the block tracker and update accounts', async () => { - withController(({ controller, blockTrackerStub }) => { + await withController(({ controller, blockTrackerStub }) => { const updateAccountsSpy = jest .spyOn(controller, 'updateAccounts') .mockResolvedValue(); @@ -238,7 +244,7 @@ describe('AccountTrackerController', () => { describe('stop', () => { it('ends the subscription to the block tracker', async () => { - withController(({ controller, blockTrackerStub }) => { + await withController(({ controller, blockTrackerStub }) => { controller.stop(); expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( @@ -252,7 +258,7 @@ describe('AccountTrackerController', () => { describe('startPollingByNetworkClientId', () => { it('should subscribe to the block tracker and update accounts if not already using the networkClientId', async () => { - withController(({ controller, blockTrackerFromHookStub }) => { + await withController(({ controller, blockTrackerFromHookStub }) => { const updateAccountsSpy = jest .spyOn(controller, 'updateAccounts') .mockResolvedValue(); @@ -278,7 +284,7 @@ describe('AccountTrackerController', () => { const blockTrackerFromHookStub1 = buildMockBlockTracker(); const blockTrackerFromHookStub2 = buildMockBlockTracker(); const blockTrackerFromHookStub3 = buildMockBlockTracker(); - withController( + await withController( { getNetworkClientById: jest .fn() @@ -347,7 +353,7 @@ describe('AccountTrackerController', () => { describe('stopPollingByPollingToken', () => { it('should unsubscribe from the block tracker when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - withController(({ controller, blockTrackerFromHookStub }) => { + await withController(({ controller, blockTrackerFromHookStub }) => { jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); const pollingToken = @@ -363,7 +369,7 @@ describe('AccountTrackerController', () => { }); it('should not unsubscribe from the block tracker if called with one of multiple active polling tokens for a given networkClient', async () => { - withController(({ controller, blockTrackerFromHookStub }) => { + await withController(({ controller, blockTrackerFromHookStub }) => { jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); const pollingToken1 = @@ -378,16 +384,16 @@ describe('AccountTrackerController', () => { }); }); - it('should error if no pollingToken is passed', () => { - withController(({ controller }) => { + it('should error if no pollingToken is passed', async () => { + await withController(({ controller }) => { expect(() => { controller.stopPollingByPollingToken(undefined); }).toThrow('pollingToken required'); }); }); - it('should error if no matching pollingToken is found', () => { - withController(({ controller }) => { + it('should error if no matching pollingToken is found', async () => { + await withController(({ controller }) => { expect(() => { controller.stopPollingByPollingToken('potato'); }).toThrow('pollingToken not found'); @@ -421,7 +427,7 @@ describe('AccountTrackerController', () => { throw new Error('unexpected networkClientId'); } }); - withController( + await withController( { getNetworkClientById: getNetworkClientByIdStub, }, @@ -456,7 +462,7 @@ describe('AccountTrackerController', () => { const blockTrackerStub = buildMockBlockTracker({ shouldStubListeners: false, }); - withController( + await withController( { blockTracker: blockTrackerStub as unknown as BlockTracker, }, @@ -499,14 +505,29 @@ describe('AccountTrackerController', () => { networkId: '0x1', chainId: '0x1', }).provider; - const getNetworkClientByIdStub = jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub, - provider: providerFromHook, - }); - withController( + const getNetworkClientByIdStub = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }; + case 'selectedNetworkClientId': + return { + configuration: { + chainId: currentChainId, + }, + }; + default: + throw new Error('unexpected networkClientId'); + } + }); + await withController( { getNetworkClientById: getNetworkClientByIdStub, }, @@ -540,7 +561,7 @@ describe('AccountTrackerController', () => { describe('updateAccountsAllActiveNetworks', () => { it('updates accounts for the globally selected network and all currently polling networks', async () => { - withController(async ({ controller }) => { + await withController(async ({ controller }) => { const updateAccountsSpy = jest .spyOn(controller, 'updateAccounts') .mockResolvedValue(); @@ -572,7 +593,7 @@ describe('AccountTrackerController', () => { describe('updateAccounts', () => { it('does not update accounts if completedOnBoarding is false', async () => { - withController( + await withController( { completedOnboarding: false, }, @@ -606,11 +627,16 @@ describe('AccountTrackerController', () => { describe('when useMultiAccountBalanceChecker is true', () => { it('updates all accounts directly', async () => { - withController( + await withController( { completedOnboarding: true, useMultiAccountBalanceChecker: true, state: mockInitialState, + getNetworkClientById: jest.fn().mockReturnValue({ + configuration: { + chainId: '0x999', + }, + }), }, async ({ controller }) => { await controller.updateAccounts(); @@ -645,11 +671,16 @@ describe('AccountTrackerController', () => { describe('when useMultiAccountBalanceChecker is false', () => { it('updates only the selectedAddress directly, setting other balances to null', async () => { - withController( + await withController( { completedOnboarding: true, useMultiAccountBalanceChecker: false, state: mockInitialState, + getNetworkClientById: jest.fn().mockReturnValue({ + configuration: { + chainId: '0x999', + }, + }), }, async ({ controller }) => { await controller.updateAccounts(); @@ -683,7 +714,7 @@ describe('AccountTrackerController', () => { describe('chain does have single call balance address and network is not localhost', () => { describe('when useMultiAccountBalanceChecker is true', () => { it('updates all accounts via balance checker', async () => { - withController( + await withController( { completedOnboarding: true, useMultiAccountBalanceChecker: true, @@ -697,7 +728,7 @@ describe('AccountTrackerController', () => { state: { accounts: { ...mockAccounts }, accountsByChainId: { - '0x1': { ...mockAccounts }, + [currentChainId]: { ...mockAccounts }, }, }, }, @@ -718,7 +749,7 @@ describe('AccountTrackerController', () => { expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { - '0x1': accounts, + [currentChainId]: accounts, }, currentBlockGasLimit: '', currentBlockGasLimitByChainId: {}, @@ -731,8 +762,8 @@ describe('AccountTrackerController', () => { }); describe('onAccountRemoved', () => { - it('should remove an account from state', () => { - withController( + it('should remove an account from state', async () => { + await withController( { state: { accounts: { ...mockAccounts }, @@ -772,8 +803,8 @@ describe('AccountTrackerController', () => { }); describe('clearAccounts', () => { - it('should reset state', () => { - withController( + it('should reset state', async () => { + await withController( { state: { accounts: { ...mockAccounts }, diff --git a/app/scripts/controllers/account-tracker-controller.ts b/app/scripts/controllers/account-tracker-controller.ts index ec4789189a0c..5f509a1901bf 100644 --- a/app/scripts/controllers/account-tracker-controller.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -45,7 +45,7 @@ import type { OnboardingControllerGetStateAction, OnboardingControllerStateChangeEvent, } from './onboarding'; -import { PreferencesControllerState } from './preferences-controller'; +import { PreferencesControllerGetStateAction } from './preferences-controller'; // Unique name for the controller const controllerName = 'AccountTrackerController'; @@ -143,7 +143,8 @@ export type AllowedActions = | OnboardingControllerGetStateAction | AccountsControllerGetSelectedAccountAction | NetworkControllerGetStateAction - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | PreferencesControllerGetStateAction; /** * Events that this controller is allowed to subscribe. @@ -170,7 +171,6 @@ export type AccountTrackerControllerOptions = { provider: Provider; blockTracker: BlockTracker; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; - preferencesControllerState: Partial<PreferencesControllerState>; }; /** @@ -198,8 +198,6 @@ export default class AccountTrackerController extends BaseController< #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesControllerState: AccountTrackerControllerOptions['preferencesControllerState']; - #selectedAccount: InternalAccount; /** @@ -226,7 +224,6 @@ export default class AccountTrackerController extends BaseController< this.#blockTracker = options.blockTracker; this.#getNetworkIdentifier = options.getNetworkIdentifier; - this.#preferencesControllerState = options.preferencesControllerState; // subscribe to account removal this.messagingSystem.subscribe( @@ -256,8 +253,9 @@ export default class AccountTrackerController extends BaseController< this.messagingSystem.subscribe( 'AccountsController:selectedEvmAccountChange', (newAccount) => { - const { useMultiAccountBalanceChecker } = - this.#preferencesControllerState; + const { useMultiAccountBalanceChecker } = this.messagingSystem.call( + 'PreferencesController:getState', + ); if ( this.#selectedAccount.id !== newAccount.id && @@ -433,10 +431,8 @@ export default class AccountTrackerController extends BaseController< return; } const { blockTracker } = this.#getCorrectNetworkClient(networkClientId); - const updateForBlock = this.#updateForBlockByNetworkClientId.bind( - this, - networkClientId, - ); + const updateForBlock = (blockNumber: string) => + this.#updateForBlockByNetworkClientId(networkClientId, blockNumber); blockTracker.addListener('latest', updateForBlock); this.#listeners[networkClientId] = updateForBlock; @@ -672,7 +668,9 @@ export default class AccountTrackerController extends BaseController< const { chainId, provider, identifier } = this.#getCorrectNetworkClient(networkClientId); - const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; + const { useMultiAccountBalanceChecker } = this.messagingSystem.call( + 'PreferencesController:getState', + ); let addresses = []; if (useMultiAccountBalanceChecker) { @@ -723,7 +721,9 @@ export default class AccountTrackerController extends BaseController< provider: Provider, chainId: Hex, ): Promise<void> { - const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; + const { useMultiAccountBalanceChecker } = this.messagingSystem.call( + 'PreferencesController:getState', + ); let balance = '0x0'; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b19c91a232ab..5b3693960113 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1681,6 +1681,7 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:getState', 'NetworkController:getNetworkClientById', 'OnboardingController:getState', + 'PreferencesController:getState', ], allowedEvents: [ 'AccountsController:selectedEvmAccountChange', @@ -1698,7 +1699,6 @@ export default class MetamaskController extends EventEmitter { }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, - preferencesControllerState: this.preferencesController.state, }); // start and stop polling for balances based on activeControllerConnections From cd820efca8bf59505f4b4c7fb8ceabb9be7dbfaa Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:40:57 -0400 Subject: [PATCH 149/226] fix: phishing test to not check c2 domains (#27846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR modifies how phishing detection and blocklist checks are handled within the phishingController. Specifically, it ensures that only the isBlockedRequest function checks the blocklist against network requests. The test function will no longer handle for network requests but more specifically only be ran on `main_fame` and `sub_frame` requests. This allowed SEAL to submit C2 domains, however going forward they will need to submit C2 domains directly to us to be ingested. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27846?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. --- app/scripts/background.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 7d9d0f5684a6..b6fe63b9aff1 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -266,13 +266,17 @@ function maybeDetectPhishing(theController) { } theController.phishingController.maybeUpdateState(); - const phishingTestResponse = theController.phishingController.test( - details.url, - ); const blockedRequestResponse = theController.phishingController.isBlockedRequest(details.url); + let phishingTestResponse; + if (details.type === 'main_frame' || details.type === 'sub_frame') { + phishingTestResponse = theController.phishingController.test( + details.url, + ); + } + // if the request is not blocked, and the phishing test is not blocked, return and don't show the phishing screen if (!phishingTestResponse?.result && !blockedRequestResponse.result) { return {}; From cc47ff95a7fbac72a3788f213897680318932209 Mon Sep 17 00:00:00 2001 From: Jyoti Puri <jyotipuri@gmail.com> Date: Tue, 15 Oct 2024 23:25:12 +0530 Subject: [PATCH 150/226] fix: nonce value when there are multiple transactions in parallel (#27874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix issue with nonce not updating when there are multiple transaction created in parallel and once transaction is submitted. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27617 ## **Manual testing steps** 1. Go to testdapp 2. create 2 transactions and submit first one 3. Nonce for second transaction should update ## **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. --- .../confirm-transaction-base.component.js | 7 +++++++ 1 file changed, 7 insertions(+) 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 96fd5315e317..b4d2d6a8def5 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 @@ -215,6 +215,8 @@ export default class ConfirmTransactionBase extends Component { useMaxValue, hasPriorityApprovalRequest, mostRecentOverviewPage, + txData, + getNextNonce, } = this.props; const { @@ -225,6 +227,7 @@ export default class ConfirmTransactionBase extends Component { isEthGasPriceFetched: prevIsEthGasPriceFetched, hexMaximumTransactionFee: prevHexMaximumTransactionFee, hasPriorityApprovalRequest: prevHasPriorityApprovalRequest, + txData: prevTxData, } = prevProps; const statusUpdated = transactionStatus !== prevTxStatus; @@ -232,6 +235,10 @@ export default class ConfirmTransactionBase extends Component { transactionStatus === TransactionStatus.dropped || transactionStatus === TransactionStatus.confirmed; + if (txData.id !== prevTxData.id) { + getNextNonce(); + } + if ( nextNonce !== prevNextNonce || customNonceValue !== prevCustomNonceValue From dc0dc67dd2ef36692c2329ef0109c8d6a9cb32a7 Mon Sep 17 00:00:00 2001 From: sahar-fehri <sahar.fehri@consensys.net> Date: Tue, 15 Oct 2024 20:21:15 +0200 Subject: [PATCH 151/226] feat: upgrade assets-controllers to v38.3.0 (#27755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to upgrade assets-controllers to v38.3.0; [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27755?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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: Howard Braham <howrad@gmail.com> --- ...ts-controllers-npm-38.3.0-57b3d695bb.patch} | 0 package.json | 2 +- yarn.lock | 18 +++++++++--------- 3 files changed, 10 insertions(+), 10 deletions(-) rename .yarn/patches/{@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch => @metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch} (100%) diff --git a/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch b/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch similarity index 100% rename from .yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch rename to .yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch diff --git a/package.json b/package.json index 4973fa0da559..76dfb15c1ba7 100644 --- a/package.json +++ b/package.json @@ -301,7 +301,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch", + "@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/browser-passworder": "^4.3.0", diff --git a/yarn.lock b/yarn.lock index 733c94112452..94ecd006df3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4861,9 +4861,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:38.2.0": - version: 38.2.0 - resolution: "@metamask/assets-controllers@npm:38.2.0" +"@metamask/assets-controllers@npm:38.3.0": + version: 38.3.0 + resolution: "@metamask/assets-controllers@npm:38.3.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4895,13 +4895,13 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^21.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/96ae724a002289e4df97bab568e0bba4d28ef18320298b12d828fc3b58c58ebc54b9f9d659c5e6402aad82088b699e52469d897dd4356e827e35b8f8cebb4483 + checksum: 10/b6e69c9925c50f351b9de1e31cc5d9a4c0ab7cf1abf116c0669611ecb58b3890dd0de53d36bcaaea4f8c45d6ddc2c53eef80c42f93f8f303f1ee9d8df088872b languageName: node linkType: hard -"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch": - version: 38.2.0 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch::version=38.2.0&hash=e14ff8" +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch": + version: 38.3.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch::version=38.3.0&hash=e14ff8" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4933,7 +4933,7 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^21.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/0ba3673bf9c87988d6c569a14512b8c9bb97db3516debfedf24cbcf38110e99afec8d9fc50cb0b627bfbc1d1a62069298e4e27278587197f67812cb38ee2c778 + checksum: 10/1f57289a3a2a88f1f16e00a138b30b9a8e4ac894086732a463e6b47d5e984e0a7e05ef2ec345f0e1cd69857669253260d53d4c37b2b3d9b970999602fc01a21c languageName: node linkType: hard @@ -26126,7 +26126,7 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch" + "@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" From 82e5a457df241e34b87c0bbd9d12286e3699525f Mon Sep 17 00:00:00 2001 From: Derek Brans <dbrans@gmail.com> Date: Tue, 15 Oct 2024 15:22:27 -0400 Subject: [PATCH 152/226] test(TXL-308): initial e2e for stx using swaps (#27215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** * Initial e2e test for stx <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27215?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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-snapshot.json | 1 + test/e2e/default-fixture.js | 2 + test/e2e/fixture-builder.js | 8 + test/e2e/mock-e2e.js | 58 ++- ...rs-after-init-opt-in-background-state.json | 2 + .../errors-after-init-opt-in-ui-state.json | 2 + ...s-before-init-opt-in-background-state.json | 19 + .../errors-before-init-opt-in-ui-state.json | 19 + .../mock-requests-for-swap-test.ts | 346 ++++++++++++++++++ .../smart-transactions.spec.ts | 97 +++++ test/e2e/tests/swaps/{shared.js => shared.ts} | 98 +++-- .../{swap-eth.spec.js => swap-eth.spec.ts} | 28 +- ...ns.spec.js => swaps-notifications.spec.ts} | 23 +- test/e2e/webdriver/driver.js | 6 +- .../smart-transaction-status.js | 2 + 15 files changed, 617 insertions(+), 94 deletions(-) create mode 100644 test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts create mode 100644 test/e2e/tests/smart-transactions/smart-transactions.spec.ts rename test/e2e/tests/swaps/{shared.js => shared.ts} (71%) rename test/e2e/tests/swaps/{swap-eth.spec.js => swap-eth.spec.ts} (80%) rename test/e2e/tests/swaps/{swaps-notifications.spec.js => swaps-notifications.spec.ts} (92%) diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 2516654f1803..37a05025382d 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -56,6 +56,7 @@ "test.metamask-phishing.io", "token.api.cx.metamask.io", "tokens.api.cx.metamask.io", + "transaction.api.cx.metamask.io", "tx-sentinel-ethereum-mainnet.api.cx.metamask.io", "unresponsive-rpc.test", "unresponsive-rpc.url", diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 5d3883a5e8f5..95f35bf1694c 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -268,7 +268,9 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { SmartTransactionsController: { smartTransactionsState: { fees: {}, + feesByChainId: {}, liveness: true, + livenessByChainId: {}, smartTransactions: { [CHAIN_IDS.MAINNET]: [], }, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 4c802e13bfa0..d73b959946c2 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -654,6 +654,14 @@ class FixtureBuilder { }); } + withPreferencesControllerSmartTransactionsOptedIn() { + return this.withPreferencesController({ + preferences: { + smartTransactionsOptInStatus: true, + }, + }); + } + withPreferencesControllerAndFeatureFlag(flags) { merge(this.fixture.data.PreferencesController, flags); return this; diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index abc536ef6059..209777f32bd7 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -272,35 +272,33 @@ async function setupMocking( .thenCallback(() => { return { statusCode: 200, - json: [ - { - ethereum: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - bsc: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - polygon: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - avalanche: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - smartTransactions: { - mobileActive: false, - extensionActive: false, - }, - updated_at: '2022-03-17T15:54:00.360Z', + json: { + ethereum: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, }, - ], + bsc: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + polygon: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + avalanche: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + smartTransactions: { + mobileActive: false, + extensionActive: true, + }, + updated_at: '2022-03-17T15:54:00.360Z', + }, }; }); @@ -470,7 +468,7 @@ async function setupMocking( decimals: 18, name: 'Dai Stablecoin', iconUrl: - 'https://crypto.com/price/coin-data/icon/DAI/color_icon.png', + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', type: 'erc20', aggregators: [ 'aave', @@ -497,7 +495,7 @@ async function setupMocking( decimals: 6, name: 'USD Coin', iconUrl: - 'https://crypto.com/price/coin-data/icon/USDC/color_icon.png', + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', type: 'erc20', aggregators: [ 'aave', 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 4658c175bfd5..78988fc87cc8 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 @@ -243,7 +243,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, 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 924769a3cb91..f40d36f85aad 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 @@ -173,7 +173,9 @@ "allDetectedTokens": {}, "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" }, "allNftContracts": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index e2cb7369d88a..89b1b29100bb 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -50,6 +50,23 @@ }, "snapsInstallPrivacyWarningShown": true }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { @@ -138,7 +155,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 34cc62d3c560..f13d3e078c64 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -50,6 +50,23 @@ }, "snapsInstallPrivacyWarningShown": true }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { @@ -138,7 +155,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, diff --git a/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts b/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts new file mode 100644 index 000000000000..457d1ea6c0a1 --- /dev/null +++ b/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts @@ -0,0 +1,346 @@ +import { MockttpServer } from 'mockttp'; +import { mockEthDaiTrade } from '../swaps/shared'; + +const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33'; + +const GET_FEES_REQUEST_INCLUDES = { + txs: [ + { + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + value: '0x1bc16d674ec80000', + gas: '0xf4240', + nonce: '0x0', + }, + ], +}; + +const GET_FEES_RESPONSE = { + blockNumber: 20728974, + id: '19d4eea3-8a49-463e-9e9c-099f9d9571ca', + txs: [ + { + cancelFees: [], + return: '0x', + status: 1, + gasUsed: 190780, + gasLimit: 239420, + fees: [ + { + maxFeePerGas: 4667609171, + maxPriorityFeePerGas: 1000000004, + gas: 239420, + balanceNeeded: 1217518987960240, + currentBalance: 751982303082919400, + error: '', + }, + ], + feeEstimate: 627603309182220, + baseFeePerGas: 2289670348, + maxFeeEstimate: 1117518987720820, + }, + ], +}; + +const SUBMIT_TRANSACTIONS_REQUEST_EXACTLY = { + rawTxs: [ + '0x02f91a3b0180843b9aca048501163610538303a73c94881d40237659c251811cec9c364ef91dc08d300c881bc16d674ec80000b919c65f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136b796265725377617046656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000018e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000001b83413f0b3640000000000000000000000000000000000000000000000000f7a8daea356aa92bfe0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000003e2c284391c000000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017a4e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000132000000000000000000000000000000000000000000000000000000000000015200000000000000000000000000000000000000000000000000000000000001260000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000000000066e029cc00000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008200000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000e80000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000400e00deaa000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000c697051d1c6296c24ae3bcef39aca743861d9a81000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae900000000000000000000000000000000000000000000000006e0d04fc2cd90000000000000000000000000000000000000000000000000000000000000000040003c5f890000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000ead050515e10fdb3540ccd6f8236c46790508a7600000000000000000000000000000000000000000000000074b3f935bb79d45400000000000000000000000000000000000000000000000000000000000000e00000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000000000000000000000000000c4000000000000000000000000000000000000000000000000000000000000000000000000000003a4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000028cacd5e26a719f139e2105ca1efc3d9dc892826000000000000000000000000ff8ba4d1fc3762f6154cc942ccf30049a2a0cec6000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000074b3f935bb79d45400000000000000000000000000000000000000000000000074b3f935bb79d4540000000000000000000000000000000000000000000000000000000045aff3b30000000000000000000000000000000000000000000000000000000066e025760000000000000000000000000000000000000000000000000016649acb241b017da53b79cbf14cc2a737cd6469098549000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae80464000000000000000000000000ae4fdcc420f1409c8b9b2af04db150dd986f66a5000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000444b27250000000000000000000000000000000000000000000000000000000000000041031bc9026b766621ebb870691407a8f5b5d222977566d0bb38bbd633459fc9671e24b5c970373555d66f0a46e830ee1605152bd519fed1a9684a097364f8b41f1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413e4923699eff11cb0252c3f8b42793eeac8793bea92843fa4028b80ff3391bbf1df4ddef51732ceeb6f65a8c9dc2651e4b952568d350b4029d4b8b5cae5c1f991c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000045921fcd000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000840f9f95029e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000005354532a0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000840f9f95029e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040301a40330000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000005b6a0771c752e35b2ca2aff4f22a66b1598a2bc50000000000000000000000000000000000000000000000000000000053516d7f000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000005351dd8d000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000004207cfca814f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c806df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000029a7a0aa0000000000000000000000000000000000000000000000000000000000000020000000000000000000108fd5cc11eaa000000000000000fcb6c0091c62637b42000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000001b83413f0b3640000000000000000000000000000000000000000000000000f7a8daea356aa92bfe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002337b22536f75726365223a226d6574616d61736b222c22416d6f756e74496e555344223a22343635382e373832393833313733303632222c22416d6f756e744f7574555344223a22343637362e393836303733303034303236222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2234363631373438303431393032373532373538353934222c2254696d657374616d70223a313732353936353539362c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a22546363534e7837537235376b367242794f5a74344b714472344d544637356b7651527658644230724266386e395864513869634a3830385963355155595a34675a52527645337777433237352f59586a722f34625065662b4a58514b4969556b6334356a4e73556c366e6141387141774d5a48324f4a3234657932647253386c52625551444f67784b4d6979334d413164467472575241306f6d6e664873365044624b6d6f4e494c58674b45416e497a6b6d687a675043346e396d39715043337a457459737875457042772b386356426b684e7761684f56625850635854646977334870437356365555635375522f4a495342386d6a737a494b6d664b46595a716333516c5a714e6e507a50576a3648366e73587050512b6145725338334c3544554b5868364e6a70584855764748314d7a557074584169615634737354795849582f435645685a396e76564845746b2f776b6a42673d3d227d7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ac001a0e067d3acdb151721e7fdb3834cf2563d667aad9b4c18a5afb81390d6288ac2fe9fa0e74a9e40e017bcd926f3c5da4355e0926ae7e45b3b9e1bc474507220cb43', + ], + rawCancelTxs: [], +}; + +const GET_BATCH_STATUS_RESPONSE_PENDING = { + '0d506aaa-5e38-4cab-ad09-2039cb7a0f33': { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: false, + minedTx: 'not_mined', + wouldRevertMessage: null, + minedHash: '', + duplicated: false, + timedOut: false, + proxied: false, + type: 'sentinel', + }, +}; + +const GET_BATCH_STATUS_RESPONSE_SUCCESS = { + '0d506aaa-5e38-4cab-ad09-2039cb7a0f33': { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: true, + minedTx: 'success', + wouldRevertMessage: null, + minedHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + duplicated: true, + timedOut: true, + proxied: false, + type: 'sentinel', + }, +}; + +const GET_TRANSACTION_RECEIPT_RESPONSE = { + id: 2901696354742565, + jsonrpc: '2.0', + result: { + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x2', + contractAddress: null, + cumulativeGasUsed: '0xc138b1', + effectiveGasPrice: '0x1053fcd93', + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + gasUsed: '0x2e93c', + logs: [ + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000005af3107a4000', + logIndex: '0xde', + removed: false, + topics: [ + '0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000005a275669d200', + logIndex: '0xdf', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000033dd7a160e2a300', + logIndex: '0xe0', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000006a3845cef618', + logIndex: '0xe1', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x000000000000000000000000ad30f7eebd9bd5150a256f47da41d4403033cdf0', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xd82fa167727a4dc6d6f55830a2c47abbb4b3a0f8', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000033dd7a160e2a3000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000005a275669d200', + logIndex: '0xe2', + removed: false, + topics: [ + '0xb651f2787ff61b5ab14f3936f2daebdad3d84aeb74438e82870cc3b7aee71e90', + '0x00000000000000000000000000000000000000000000000000000191e0cc96ac', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000000000cbba106e00', + logIndex: '0xe3', + removed: false, + topics: [ + '0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xf326e4de8f66a0bdc0970b79e0924e33c79f1915', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000000000cbba106e00', + logIndex: '0xe4', + removed: false, + topics: [ + '0x3d0ce9bfc3ed7d6862dbb28b2dea94561fe714a1b4d019aa8af39730d1ad7c3d', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000033dd7a160e2a300', + logIndex: '0xe5', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + '0x0000000000000000000000005cfe73b6021e818b776b421b1c4db2474086a7e1', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x881d40237659c251811cec9c364ef91dc08d300c', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x', + logIndex: '0xe6', + removed: false, + topics: [ + '0xbeee1e6e7fe307ddcf84b0a16137a4430ad5e2480fc4f4a8e250ab56ccd7630d', + '0x015123c6e2552626efe611b6c48de60d080a6650860a38f237bc2b6f651f79d1', + '0x0000000000000000000000005cfe73b6021e818b776b421b1c4db2474086a7e1', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + ], + logsBloom: + '0x00000000000000001000000000000000000000000000000000000001000000000000010000000000000010000000000002000000080008000000040000000000a00000000000000000020008000000000000000000540000000004008020000010000000000000000000000000000801000000000000040000000010004010000000021000000000000000000000000000020041000100004020000000000000000000000200000000000040000000000000000000000000000000000000000000000002000400000000000000000000001002000400000000000002000000000020200000000400000000800000000000000000020200400000000000001000', + status: '0x1', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + type: '0x2', + }, +}; + +const GET_TRANSACTION_BY_HASH_RESPONSE = { + id: 2901696354742565, + jsonrpc: '2.0', + result: { + accessList: [], + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x2', + chainId: '0x539', + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + gas: '0x3a73c', + gasPrice: '0x1053fcd93', + hash: '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + input: + '0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001c616972737761704c696768743446656544796e616d696346697865640000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000191e0cc96ac0000000000000000000000000000000000000000000000000000000066e44f2c00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000033dd7a160e2a300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005a275669d200000000000000000000000000000000000000000000000000000000000000001bc1acb8a206598705baeb494a479a8af9dc3a9f9b7bd1ce9818360fd6f603cf0766e7bdc77f9f72e90dcd9157e007291adc6d3947e9b6d89ff412c5b54f9a17f1000000000000000000000000000000000000000000000000000000cbba106e00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000d7', + maxFeePerGas: '0x14bdcd619', + maxPriorityFeePerGas: '0x3b9aca04', + nonce: '0x127', + r: '0x5a5463bfe8e587ee1211be74580c74fa759f8292f37f970033df4b782f5e097d', + s: '0x50e403a70000b106e9f598b1b3f55b6ea9d2ec21d9fc67de63eb1d07df2767dd', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + transactionIndex: '0x2f', + type: '0x2', + v: '0x0', + value: '0x5af3107a4000', + yParity: '0x0', + }, +}; + +export const mockSwapRequests = async (mockServer: MockttpServer) => { + await mockEthDaiTrade(mockServer); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getBalance', + params: ['0x5cfe73b6021e818b776b421b1c4db2474086a7e1'], + }) + .thenJson(200, { + id: 3806592044086814, + jsonrpc: '2.0', + result: '0x1bc16d674ec80000', // 2 ETH + }); + + await mockServer + .forPost('https://transaction.api.cx.metamask.io/networks/1/getFees') + .withJsonBodyIncluding(GET_FEES_REQUEST_INCLUDES) + .thenJson(200, GET_FEES_RESPONSE); + + await mockServer + .forPost( + 'https://transaction.api.cx.metamask.io/networks/1/submitTransactions', + ) + .once() + .withJsonBody(SUBMIT_TRANSACTIONS_REQUEST_EXACTLY) + .thenJson(200, { uuid: STX_UUID }); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, GET_BATCH_STATUS_RESPONSE_PENDING); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, GET_BATCH_STATUS_RESPONSE_SUCCESS); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getTransactionReceipt', + params: [ + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + ], + }) + .thenJson(200, GET_TRANSACTION_RECEIPT_RESPONSE); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getTransactionByHash', + params: [ + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + ], + }) + .thenJson(200, GET_TRANSACTION_BY_HASH_RESPONSE); +}; diff --git a/test/e2e/tests/smart-transactions/smart-transactions.spec.ts b/test/e2e/tests/smart-transactions/smart-transactions.spec.ts new file mode 100644 index 000000000000..210d5abdb034 --- /dev/null +++ b/test/e2e/tests/smart-transactions/smart-transactions.spec.ts @@ -0,0 +1,97 @@ +import { MockttpServer } from 'mockttp'; +import { + buildQuote, + reviewQuote, + checkActivityTransaction, +} from '../swaps/shared'; +import FixtureBuilder from '../../fixture-builder'; +import { unlockWallet, withFixtures } from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { mockSwapRequests } from './mock-requests-for-swap-test'; + +export async function withFixturesForSmartTransactions( + { + title, + testSpecificMock, + }: { + title?: string; + testSpecificMock: (mockServer: MockttpServer) => Promise<void>; + }, + test: (args: { driver: Driver }) => Promise<void>, +) { + const inputChainId = CHAIN_IDS.MAINNET; + await withFixtures( + { + fixtures: new FixtureBuilder({ inputChainId }) + .withPermissionControllerConnectedToTestDapp() + .withPreferencesControllerSmartTransactionsOptedIn() + .withNetworkControllerOnMainnet() + .build(), + title, + testSpecificMock, + dapp: true, + }, + async ({ driver }) => { + await unlockWallet(driver); + await test({ driver }); + }, + ); +} + +export const waitForTransactionToComplete = async ( + driver: Driver, + options: { tokenName: string }, +) => { + await driver.waitForSelector({ + css: '[data-testid="swap-smart-transaction-status-header"]', + text: 'Privately submitting your Swap', + }); + + await driver.waitForSelector( + { + css: '[data-testid="swap-smart-transaction-status-header"]', + text: 'Swap complete!', + }, + { timeout: 30000 }, + ); + + await driver.findElement({ + css: '[data-testid="swap-smart-transaction-status-description"]', + text: `${options.tokenName}`, + }); + + await driver.clickElement({ text: 'Close', tag: 'button' }); + await driver.waitForSelector('[data-testid="account-overview__asset-tab"]'); +}; + +describe('smart transactions @no-mmi', function () { + it('Completes a Swap', async function () { + await withFixturesForSmartTransactions( + { + title: this.test?.fullTitle(), + testSpecificMock: mockSwapRequests, + }, + async ({ driver }) => { + await buildQuote(driver, { + amount: 2, + swapTo: 'DAI', + }); + await reviewQuote(driver, { + amount: 2, + swapFrom: 'ETH', + swapTo: 'DAI', + }); + + await driver.clickElement({ text: 'Swap', tag: 'button' }); + await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); + await checkActivityTransaction(driver, { + index: 0, + amount: '2', + swapFrom: 'ETH', + swapTo: 'DAI', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/swaps/shared.js b/test/e2e/tests/swaps/shared.ts similarity index 71% rename from test/e2e/tests/swaps/shared.js rename to test/e2e/tests/swaps/shared.ts index 3bfdefcf71d7..3f3aff4447e5 100644 --- a/test/e2e/tests/swaps/shared.js +++ b/test/e2e/tests/swaps/shared.ts @@ -1,27 +1,54 @@ -const { strict: assert } = require('assert'); -const FixtureBuilder = require('../../fixture-builder'); -const { regularDelayMs, veryLargeDelayMs } = require('../../helpers'); - -const ganacheOptions = { - accounts: [ - { - secretKey: - '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', - balance: 25000000000000000000, +import { strict as assert } from 'assert'; +import { ServerOptions } from 'ganache'; +import { MockttpServer } from 'mockttp'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { regularDelayMs, veryLargeDelayMs } from '../../helpers'; +import { SWAP_TEST_ETH_DAI_TRADES_MOCK } from '../../../data/mock-data'; + +export async function mockEthDaiTrade(mockServer: MockttpServer) { + return [ + await mockServer + .forGet('https://swap.api.cx.metamask.io/networks/1/trades') + .thenCallback(() => { + return { + statusCode: 200, + json: SWAP_TEST_ETH_DAI_TRADES_MOCK, + }; + }), + ]; +} + +export const ganacheOptions: ServerOptions & { miner: { blockTime?: number } } = + { + wallet: { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000n, + }, + ], }, - ], -}; + miner: {}, + }; -const withFixturesOptions = { +export const withFixturesOptions = { fixtures: new FixtureBuilder().build(), ganacheOptions, }; -const buildQuote = async (driver, options) => { +type SwapOptions = { + amount: number; + swapTo?: string; + swapToContractAddress?: string; +}; + +export const buildQuote = async (driver: Driver, options: SwapOptions) => { await driver.clickElement('[data-testid="token-overview-button-swap"]'); await driver.fill( 'input[data-testid="prepare-swap-page-from-token-amount"]', - options.amount, + options.amount.toString(), ); await driver.delay(veryLargeDelayMs); // Need an extra delay after typing an amount. await driver.clickElement('[data-testid="prepare-swap-page-swap-to"]'); @@ -29,7 +56,7 @@ const buildQuote = async (driver, options) => { await driver.fill( 'input[id="list-with-search__text-search"]', - options.swapTo || options.swapToContractAddress, + options.swapTo || options.swapToContractAddress || '', ); await driver.delay(veryLargeDelayMs); // Need an extra delay after typing an amount. @@ -55,7 +82,15 @@ const buildQuote = async (driver, options) => { ); }; -const reviewQuote = async (driver, options) => { +export const reviewQuote = async ( + driver: Driver, + options: { + swapFrom: string; + swapTo: string; + amount: number; + skipCounter?: boolean; + }, +) => { const summary = await driver.waitForSelector( '[data-testid="exchange-rate-display-quote-rate"]', ); @@ -68,7 +103,7 @@ const reviewQuote = async (driver, options) => { '[data-testid="prepare-swap-page-receive-amount"]', ); const swapToAmount = await elementSwapToAmount.getText(); - const expectedAmount = parseFloat(quote[3]) * options.amount; + const expectedAmount = Number(quote[3]) * options.amount; const dotIndex = swapToAmount.indexOf('.'); const decimals = dotIndex === -1 ? 0 : swapToAmount.length - dotIndex - 1; assert.equal( @@ -91,7 +126,10 @@ const reviewQuote = async (driver, options) => { } }; -const waitForTransactionToComplete = async (driver, options) => { +export const waitForTransactionToComplete = async ( + driver: Driver, + options: { tokenName: string }, +) => { await driver.waitForSelector({ css: '[data-testid="awaiting-swap-header"]', text: 'Processing', @@ -114,7 +152,10 @@ const waitForTransactionToComplete = async (driver, options) => { await driver.waitForSelector('[data-testid="account-overview__asset-tab"]'); }; -const checkActivityTransaction = async (driver, options) => { +export const checkActivityTransaction = async ( + driver: Driver, + options: { index: number; swapFrom: string; swapTo: string; amount: string }, +) => { await driver.clickElement('[data-testid="account-overview__activity-tab"]'); await driver.waitForSelector('.activity-list-item'); @@ -149,7 +190,10 @@ const checkActivityTransaction = async (driver, options) => { await driver.clickElement('[data-testid="popover-close"]'); }; -const checkNotification = async (driver, options) => { +export const checkNotification = async ( + driver: Driver, + options: { title: string; text: string }, +) => { const isExpectedBoxTitlePresentAndVisible = await driver.isElementPresentAndVisible({ css: '[data-testid="swaps-banner-title"]', @@ -171,7 +215,7 @@ const checkNotification = async (driver, options) => { ); }; -const changeExchangeRate = async (driver) => { +export const changeExchangeRate = async (driver: Driver) => { await driver.clickElement('[data-testid="review-quote-view-all-quotes"]'); await driver.waitForSelector({ text: 'Quote details', tag: 'h2' }); @@ -182,13 +226,3 @@ const changeExchangeRate = async (driver) => { await networkFees[random].click(); await driver.clickElement({ text: 'Select', tag: 'button' }); }; - -module.exports = { - withFixturesOptions, - buildQuote, - reviewQuote, - waitForTransactionToComplete, - checkActivityTransaction, - checkNotification, - changeExchangeRate, -}; diff --git a/test/e2e/tests/swaps/swap-eth.spec.js b/test/e2e/tests/swaps/swap-eth.spec.ts similarity index 80% rename from test/e2e/tests/swaps/swap-eth.spec.js rename to test/e2e/tests/swaps/swap-eth.spec.ts index 35847b0ae33a..18d049e5de16 100644 --- a/test/e2e/tests/swaps/swap-eth.spec.js +++ b/test/e2e/tests/swaps/swap-eth.spec.ts @@ -1,35 +1,22 @@ -const { withFixtures, unlockWallet } = require('../../helpers'); -const { SWAP_TEST_ETH_DAI_TRADES_MOCK } = require('../../../data/mock-data'); -const { +import { unlockWallet, withFixtures } from '../../helpers'; +import { withFixturesOptions, buildQuote, reviewQuote, waitForTransactionToComplete, checkActivityTransaction, changeExchangeRate, -} = require('./shared'); - -async function mockEthDaiTrade(mockServer) { - return [ - await mockServer - .forGet('https://swap.api.cx.metamask.io/networks/1/trades') - .thenCallback(() => { - return { - statusCode: 200, - json: SWAP_TEST_ETH_DAI_TRADES_MOCK, - }; - }), - ]; -} + mockEthDaiTrade, +} from './shared'; describe('Swap Eth for another Token @no-mmi', function () { it('Completes second Swaps while first swap is processing', async function () { - withFixturesOptions.ganacheOptions.blockTime = 10; + withFixturesOptions.ganacheOptions.miner.blockTime = 10; await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -70,12 +57,13 @@ describe('Swap Eth for another Token @no-mmi', function () { }, ); }); + it('Completes a Swap between ETH and DAI after changing initial rate', async function () { await withFixtures( { ...withFixturesOptions, testSpecificMock: mockEthDaiTrade, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); diff --git a/test/e2e/tests/swaps/swaps-notifications.spec.js b/test/e2e/tests/swaps/swaps-notifications.spec.ts similarity index 92% rename from test/e2e/tests/swaps/swaps-notifications.spec.js rename to test/e2e/tests/swaps/swaps-notifications.spec.ts index c6dbbd469959..134741d3683c 100644 --- a/test/e2e/tests/swaps/swaps-notifications.spec.js +++ b/test/e2e/tests/swaps/swaps-notifications.spec.ts @@ -1,13 +1,14 @@ -const { withFixtures, unlockWallet } = require('../../helpers'); -const { SWAP_TEST_ETH_USDC_TRADES_MOCK } = require('../../../data/mock-data'); -const { +import { Mockttp } from 'mockttp'; +import { withFixtures, unlockWallet } from '../../helpers'; +import { SWAP_TEST_ETH_USDC_TRADES_MOCK } from '../../../data/mock-data'; +import { withFixturesOptions, buildQuote, reviewQuote, checkNotification, -} = require('./shared'); +} from './shared'; -async function mockSwapsTransactionQuote(mockServer) { +async function mockSwapsTransactionQuote(mockServer: Mockttp) { return [ await mockServer .forGet('https://swap.api.cx.metamask.io/networks/1/trades') @@ -19,7 +20,7 @@ async function mockSwapsTransactionQuote(mockServer) { } describe('Swaps - notifications @no-mmi', function () { - async function mockTradesApiPriceSlippageError(mockServer) { + async function mockTradesApiPriceSlippageError(mockServer: Mockttp) { await mockServer .forGet('https://swap.api.cx.metamask.io/networks/1/trades') .thenCallback(() => { @@ -71,7 +72,7 @@ describe('Swaps - notifications @no-mmi', function () { { ...withFixturesOptions, testSpecificMock: mockTradesApiPriceSlippageError, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -122,7 +123,7 @@ describe('Swaps - notifications @no-mmi', function () { ...withFixturesOptions, ganacheOptions: lowBalanceGanacheOptions, testSpecificMock: mockSwapsTransactionQuote, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -152,7 +153,7 @@ describe('Swaps - notifications @no-mmi', function () { await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -174,12 +175,12 @@ describe('Swaps - notifications @no-mmi', function () { await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); await buildQuote(driver, { - amount: '.0001', + amount: 0.0001, swapTo: 'DAI', }); await driver.clickElement('[title="Transaction settings"]'); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index fb8aed3d28a6..b0648f122fb9 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -319,10 +319,14 @@ class Driver { * Waits for an element that matches the given locator to reach the specified state within the timeout period. * * @param {string | object} rawLocator - Element locator - * @param {number} timeout - optional parameter that specifies the maximum amount of time (in milliseconds) + * @param {object} [options] - parameter object + * @param {number} [options.timeout] - specifies the maximum amount of time (in milliseconds) * to wait for the condition to be met and desired state of the element to wait for. * It defaults to 'visible', indicating that the method will wait until the element is visible on the page. * The other supported state is 'detached', which means waiting until the element is removed from the DOM. + * @param {string} [options.state] - specifies the state of the element to wait for. + * It defaults to 'visible', indicating that the method will wait until the element is visible on the page. + * The other supported state is 'detached', which means waiting until the element is removed from the DOM. * @returns {Promise<WebElement>} promise resolving when the element meets the state or timeout occurs. * @throws {Error} Will throw an error if the element does not reach the specified state within the timeout period. */ 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 b103ead2097c..e6a77f9474fb 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -401,6 +401,7 @@ export default function SmartTransactionStatusPage() { </Box> )} <Text + data-testid="swap-smart-transaction-status-header" color={TextColor.textDefault} variant={TextVariant.headingSm} as="h4" @@ -424,6 +425,7 @@ export default function SmartTransactionStatusPage() { )} {description && ( <Text + data-testid="swap-smart-transaction-status-description" variant={TextVariant.bodySm} as="h6" marginTop={blockExplorerUrl && 1} From a770169bfdfb024bd88ed528aa076d85d3af0081 Mon Sep 17 00:00:00 2001 From: martahj <marta.hourigan.johnson@gmail.com> Date: Tue, 15 Oct 2024 15:01:05 -0500 Subject: [PATCH 153/226] chore: remove unused swaps code (#27679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removed code which is no longer needed because we are moving forward with the swaps redesign. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27679?quickstart=1) ## **Manual testing steps** Test Swaps flows and confirm no regresions. ## **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/de/messages.json | 65 - app/_locales/el/messages.json | 65 - app/_locales/en/messages.json | 65 - app/_locales/en_GB/messages.json | 65 - app/_locales/es/messages.json | 65 - app/_locales/es_419/messages.json | 62 - app/_locales/fr/messages.json | 65 - app/_locales/hi/messages.json | 65 - app/_locales/id/messages.json | 65 - app/_locales/it/messages.json | 46 - app/_locales/ja/messages.json | 65 - app/_locales/ko/messages.json | 65 - app/_locales/ph/messages.json | 62 - app/_locales/pt/messages.json | 65 - app/_locales/pt_BR/messages.json | 62 - app/_locales/ru/messages.json | 65 - app/_locales/tl/messages.json | 65 - app/_locales/tr/messages.json | 65 - app/_locales/vi/messages.json | 65 - app/_locales/zh_CN/messages.json | 65 - app/_locales/zh_TW/messages.json | 7 - .../files-to-convert.json | 27 - .../data/integration-init-state.json | 4 - test/jest/mock-store.js | 4 - .../app/wallet-overview/coin-buttons.tsx | 6 +- .../multichain/app-header/app-header.js | 7 +- ui/ducks/swaps/swaps.js | 15 +- ui/ducks/swaps/swaps.test.js | 19 - ui/helpers/constants/routes.ts | 6 - ui/pages/asset/components/token-buttons.tsx | 6 +- ui/pages/bridge/index.test.tsx | 2 - ui/pages/home/home.component.js | 9 +- ui/pages/routes/routes.component.js | 8 - .../swaps/__snapshots__/index.test.js.snap | 20 +- .../awaiting-signatures.js | 4 +- ui/pages/swaps/awaiting-swap/awaiting-swap.js | 8 +- .../__snapshots__/build-quote.test.js.snap | 33 - ui/pages/swaps/build-quote/build-quote.js | 800 ------------ .../swaps/build-quote/build-quote.stories.js | 35 - .../swaps/build-quote/build-quote.test.js | 223 ---- ui/pages/swaps/build-quote/index.js | 1 - ui/pages/swaps/build-quote/index.scss | 223 ---- .../swaps/create-new-swap/create-new-swap.js | 4 +- .../create-new-swap/create-new-swap.test.js | 12 +- ui/pages/swaps/dropdown-input-pair/README.mdx | 15 - .../dropdown-input-pair.test.js.snap | 20 - .../dropdown-input-pair.js | 177 --- .../dropdown-input-pair.stories.js | 173 --- .../dropdown-input-pair.test.js | 44 - ui/pages/swaps/dropdown-input-pair/index.js | 1 - ui/pages/swaps/dropdown-input-pair/index.scss | 78 -- .../dropdown-search-list.test.js.snap | 46 - .../dropdown-search-list.js | 334 ----- .../dropdown-search-list.stories.js | 147 --- .../dropdown-search-list.test.js | 50 - ui/pages/swaps/dropdown-search-list/index.js | 1 - .../swaps/dropdown-search-list/index.scss | 167 --- ui/pages/swaps/index.js | 220 +--- ui/pages/swaps/index.scss | 35 +- ui/pages/swaps/index.test.js | 4 +- .../loading-swaps-quotes.js | 4 +- ui/pages/swaps/main-quote-summary/README.mdx | 14 - .../main-quote-summary.test.js.snap | 116 -- .../__snapshots__/quote-backdrop.test.js.snap | 74 -- ui/pages/swaps/main-quote-summary/index.js | 1 - ui/pages/swaps/main-quote-summary/index.scss | 125 -- .../main-quote-summary/main-quote-summary.js | 182 --- .../main-quote-summary.stories.js | 67 - .../main-quote-summary.test.js | 39 - .../main-quote-summary/quote-backdrop.js | 89 -- .../main-quote-summary/quote-backdrop.test.js | 23 - .../popover-custom-background/index.scss | 6 - .../popover-custom-background.js | 14 - .../prepare-swap-page.test.js.snap | 3 - ui/pages/swaps/prepare-swap-page/index.scss | 108 +- .../prepare-swap-page.test.js | 3 - .../swaps/prepare-swap-page/review-quote.js | 2 +- .../quote-details/index.scss | 12 - .../__snapshots__/selected-token.test.js.snap | 82 +- ui/pages/swaps/selected-token/index.scss | 142 +++ .../swaps/selected-token/selected-token.js | 86 +- .../selected-token/selected-token.test.js | 2 +- .../slippage-buttons.test.js.snap | 48 - ui/pages/swaps/slippage-buttons/index.js | 1 - ui/pages/swaps/slippage-buttons/index.scss | 111 -- .../slippage-buttons/slippage-buttons.js | 224 ---- .../slippage-buttons.stories.js | 15 - .../slippage-buttons/slippage-buttons.test.js | 99 -- .../swaps/smart-transaction-status/index.scss | 12 + .../smart-transaction-status.js | 12 +- .../view-quote-price-difference.test.js.snap | 233 ---- .../__snapshots__/view-quote.test.js.snap | 145 --- ui/pages/swaps/view-quote/index.js | 1 - ui/pages/swaps/view-quote/index.scss | 179 --- .../view-quote/view-quote-price-difference.js | 111 -- .../view-quote-price-difference.test.js | 132 -- ui/pages/swaps/view-quote/view-quote.js | 1089 ----------------- ui/pages/swaps/view-quote/view-quote.test.js | 100 -- 98 files changed, 361 insertions(+), 7612 deletions(-) delete mode 100644 ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap delete mode 100644 ui/pages/swaps/build-quote/build-quote.js delete mode 100644 ui/pages/swaps/build-quote/build-quote.stories.js delete mode 100644 ui/pages/swaps/build-quote/build-quote.test.js delete mode 100644 ui/pages/swaps/build-quote/index.js delete mode 100644 ui/pages/swaps/build-quote/index.scss delete mode 100644 ui/pages/swaps/dropdown-input-pair/README.mdx delete mode 100644 ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap delete mode 100644 ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/index.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/index.scss delete mode 100644 ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap delete mode 100644 ui/pages/swaps/dropdown-search-list/dropdown-search-list.js delete mode 100644 ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js delete mode 100644 ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js delete mode 100644 ui/pages/swaps/dropdown-search-list/index.js delete mode 100644 ui/pages/swaps/dropdown-search-list/index.scss delete mode 100644 ui/pages/swaps/main-quote-summary/README.mdx delete mode 100644 ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap delete mode 100644 ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap delete mode 100644 ui/pages/swaps/main-quote-summary/index.js delete mode 100644 ui/pages/swaps/main-quote-summary/index.scss delete mode 100644 ui/pages/swaps/main-quote-summary/main-quote-summary.js delete mode 100644 ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js delete mode 100644 ui/pages/swaps/main-quote-summary/main-quote-summary.test.js delete mode 100644 ui/pages/swaps/main-quote-summary/quote-backdrop.js delete mode 100644 ui/pages/swaps/main-quote-summary/quote-backdrop.test.js delete mode 100644 ui/pages/swaps/popover-custom-background/index.scss delete mode 100644 ui/pages/swaps/popover-custom-background/popover-custom-background.js delete mode 100644 ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap create mode 100644 ui/pages/swaps/selected-token/index.scss delete mode 100644 ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap delete mode 100644 ui/pages/swaps/slippage-buttons/index.js delete mode 100644 ui/pages/swaps/slippage-buttons/index.scss delete mode 100644 ui/pages/swaps/slippage-buttons/slippage-buttons.js delete mode 100644 ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js delete mode 100644 ui/pages/swaps/slippage-buttons/slippage-buttons.test.js delete mode 100644 ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap delete mode 100644 ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap delete mode 100644 ui/pages/swaps/view-quote/index.js delete mode 100644 ui/pages/swaps/view-quote/index.scss delete mode 100644 ui/pages/swaps/view-quote/view-quote-price-difference.js delete mode 100644 ui/pages/swaps/view-quote/view-quote-price-difference.test.js delete mode 100644 ui/pages/swaps/view-quote/view-quote.js delete mode 100644 ui/pages/swaps/view-quote/view-quote.test.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 296f1c716297..fe0c84afcfac 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Wir sind bereit, Ihnen die neuesten Angebote zu zeigen, wenn Sie fortfahren möchten." }, - "swapBuildQuotePlaceHolderText": { - "message": "Keine Tokens verfügbar, die mit $1 übereinstimmen.", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Mit Ihrer Hardware-Wallet bestätigen" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Fehler beim Abrufen der Preisangaben" }, - "swapFetchingTokens": { - "message": "Token abrufen..." - }, "swapFromTo": { "message": "Swap von $1 auf $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Hohe Slippage" }, - "swapHighSlippageWarning": { - "message": "Der Slippage-Betrag ist sehr hoch." - }, "swapIncludesMMFee": { "message": "Enthält eine MetaMask-Gebühr von $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Niedrige Slippage" }, - "swapLowSlippageError": { - "message": "Transaktion kann fehlschlagen, maximale Slippage zu niedrig." - }, "swapMaxSlippage": { "message": "Max. Slippage" }, @@ -5344,9 +5331,6 @@ "message": "Preisdifferenz von ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Der Preiseinfluss ist die Differenz zwischen dem aktuellen Marktpreis und dem bei der Ausführung der Transaktion erhaltenen Betrag. Die Preisauswirkung ist eine Funktion der Größe Ihres Geschäfts im Verhältnis zur Größe des Liquiditätspools." - }, "swapPriceUnavailableDescription": { "message": "Die Auswirkungen auf den Preis konnten aufgrund fehlender Marktpreisdaten nicht ermittelt werden. Bitte bestätigen Sie vor dem Tausch, dass Sie mit der Menge der Tokens, die Sie erhalten werden, einverstanden sind." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Angebotsanfrage" }, - "swapReviewSwap": { - "message": "Swap überprüfen" - }, - "swapSearchNameOrAddress": { - "message": "Namen suchen oder Adresse einfügen" - }, "swapSelect": { "message": "Auswählen" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Niedriger Slippage" }, - "swapSlippageNegative": { - "message": "Slippage muss größer oder gleich Null sein" - }, "swapSlippageNegativeDescription": { "message": "Slippage muss größer oder gleich Null sein" }, @@ -5502,20 +5477,6 @@ "message": "$1 mit $2 tauschen", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Dieses Token wurde manuell hinzugefügt." - }, - "swapTokenVerificationMessage": { - "message": "Bestätigen Sie immer die Token-Adresse auf $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Nur an 1 Quelle verifiziert." - }, - "swapTokenVerificationSources": { - "message": "Auf $1 Quellen überprüft.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 wurde nur auf 1 Quelle bestätigt. Ziehen Sie in Betracht, es vor dem Fortfahren auf $2 zu bestätigen.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Unbekannt" }, - "swapVerifyTokenExplanation": { - "message": "Mehrere Token können denselben Namen und dasselbe Symbol verwenden. Überprüfen Sie $1, um sicherzugehen, dass dies der Token ist, den Sie suchen.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 zum Swap verfügbar", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % Slippage" }, - "swapsAdvancedOptions": { - "message": "Erweiterte Optionen" - }, - "swapsExcessiveSlippageWarning": { - "message": "Der Slippage-Betrag ist zu hoch und wird zu einem schlechten Kurs führen. Bitte reduzieren Sie die Slippage-Toleranz auf einen Wert unter 15 %." - }, "swapsMaxSlippage": { "message": "Slippage-Toleranz" }, - "swapsNotEnoughForTx": { - "message": "Nicht genug $1, um diese Transaktion abzuschließen.", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Nicht genügend $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Drittanbieter-Details überprüfen" }, - "verifyThisTokenOn": { - "message": "Diesen Token auf $1 verifizieren", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Überprüfen Sie diesen Token auf $1 und stellen Sie sicher, dass dies der Token ist, den Sie handeln möchten.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 6adad0a49176..5c42a8d829b4 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Είμαστε έτοιμοι να σας δείξουμε τις τελευταίες προσφορές, όποτε θέλετε να συνεχίσετε" }, - "swapBuildQuotePlaceHolderText": { - "message": "Δεν υπάρχουν διαθέσιμα tokens που να αντιστοιχούν σε $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Επιβεβαιώστε με το πορτοφόλι υλικού σας" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Σφάλμα κατά τη λήψη προσφορών" }, - "swapFetchingTokens": { - "message": "Λήψη tokens..." - }, "swapFromTo": { "message": "Η ανταλλαγή από $1 έως $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Υψηλή ολίσθηση" }, - "swapHighSlippageWarning": { - "message": "Το ποσό ολίσθησης είναι πολύ υψηλό." - }, "swapIncludesMMFee": { "message": "Περιλαμβάνει μια χρέωση $1% στο MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Χαμηλή ολίσθηση" }, - "swapLowSlippageError": { - "message": "Η συναλλαγή ενδέχεται να αποτύχει, η μέγιστη ολίσθηση είναι πολύ χαμηλή." - }, "swapMaxSlippage": { "message": "Μέγιστη ολίσθηση" }, @@ -5344,9 +5331,6 @@ "message": "Διαφορά τιμής ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Η επίπτωση στις τιμές είναι η διαφορά μεταξύ της τρέχουσας τιμής αγοράς και του ποσού που εισπράττεται κατά την εκτέλεση της συναλλαγής. Η επίπτωση στις τιμές είναι συνάρτηση του μεγέθους της συναλλαγής σας σε σχέση με το μέγεθος του αποθέματος ρευστότητας." - }, "swapPriceUnavailableDescription": { "message": "Η επίπτωση στις τιμές δεν ήταν δυνατόν να προσδιοριστεί λόγω έλλειψης στοιχείων για τις τιμές της αγοράς. Παρακαλούμε επιβεβαιώστε ότι είστε ικανοποιημένοι με το ποσό των tokens που πρόκειται να λάβετε πριν από την ανταλλαγή." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Αίτημα για προσφορά" }, - "swapReviewSwap": { - "message": "Έλεγχος της ανταλλαγής" - }, - "swapSearchNameOrAddress": { - "message": "Αναζήτηση ονόματος ή επικόλληση διεύθυνσης" - }, "swapSelect": { "message": "Επιλογή" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Χαμηλή απόκλιση" }, - "swapSlippageNegative": { - "message": "Η απόκλιση πρέπει να είναι μεγαλύτερη ή ίση με το μηδέν" - }, "swapSlippageNegativeDescription": { "message": "Η απόκλιση πρέπει να είναι μεγαλύτερη ή ίση με μηδέν" }, @@ -5502,20 +5477,6 @@ "message": "Ανταλλαγή $1 έως $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Αυτό το token έχει προστεθεί χειροκίνητα." - }, - "swapTokenVerificationMessage": { - "message": "Πάντα να επιβεβαιώνετε τη διεύθυνση του token στο $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Επαληθεύτηκε μόνο σε 1 πηγή." - }, - "swapTokenVerificationSources": { - "message": "Επαληθεύτηκε μόνο σε $1 πηγές.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "Το $1 επαληθεύτηκε μόνο από 1 πηγή. Εξετάστε το ενδεχόμενο επαλήθευσης σε $2 πριν προχωρήσετε.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Άγνωστο" }, - "swapVerifyTokenExplanation": { - "message": "Πολλαπλά tokens μπορούν να χρησιμοποιούν το ίδιο όνομα και σύμβολο. Ελέγξτε το $1 για να επιβεβαιώσετε ότι αυτό είναι το token που ψάχνετε.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 διαθέσιμα για ανταλλαγή", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Απόκλιση" }, - "swapsAdvancedOptions": { - "message": "Προηγμένες επιλογές" - }, - "swapsExcessiveSlippageWarning": { - "message": "Το ποσοστό απόκλισης είναι πολύ υψηλό και θα οδηγήσει σε χαμηλή τιμή. Παρακαλούμε μειώστε την ανοχή απόκλισης σε τιμή κάτω του 15%." - }, "swapsMaxSlippage": { "message": "Ανοχή απόκλισης" }, - "swapsNotEnoughForTx": { - "message": "Δεν υπάρχουν αρκετά $1 για να ολοκληρωθεί αυτή η συναλλαγή", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Δεν υπάρχουν αρκετά $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Επαλήθευση στοιχείων τρίτων" }, - "verifyThisTokenOn": { - "message": "Επαλήθευση αυτού του token στο $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Επαληθεύστε αυτό το token στο $1 και βεβαιωθείτε ότι αυτό είναι το token που θέλετε να κάνετε συναλλαγές.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Έκδοση" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 57dd9152752e..49d48b9c71ac 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5592,10 +5592,6 @@ "swapAreYouStillThereDescription": { "message": "We’re ready to show you the latest quotes when you want to continue" }, - "swapBuildQuotePlaceHolderText": { - "message": "No tokens available matching $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -5660,9 +5656,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error fetching quotes" }, - "swapFetchingTokens": { - "message": "Fetching tokens..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5697,9 +5690,6 @@ "swapHighSlippage": { "message": "High slippage" }, - "swapHighSlippageWarning": { - "message": "Slippage amount is very high." - }, "swapIncludesGasAndMetaMaskFee": { "message": "Includes gas and a $1% MetaMask fee", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5725,9 +5715,6 @@ "swapLowSlippage": { "message": "Low slippage" }, - "swapLowSlippageError": { - "message": "Transaction may fail, max slippage too low." - }, "swapMaxSlippage": { "message": "Max slippage" }, @@ -5762,9 +5749,6 @@ "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, @@ -5811,12 +5795,6 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, - "swapReviewSwap": { - "message": "Review swap" - }, - "swapSearchNameOrAddress": { - "message": "Search name or paste address" - }, "swapSelect": { "message": "Select" }, @@ -5849,9 +5827,6 @@ "swapSlippageLowTitle": { "message": "Low slippage" }, - "swapSlippageNegative": { - "message": "Slippage must be greater or equal to zero" - }, "swapSlippageNegativeDescription": { "message": "Slippage must be greater or equal to zero" }, @@ -5920,20 +5895,6 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "This token has been added manually." - }, - "swapTokenVerificationMessage": { - "message": "Always confirm the token address on $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Only verified on 1 source." - }, - "swapTokenVerificationSources": { - "message": "Verified on $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 is only verified on 1 source. Consider verifying it on $2 before proceeding.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5954,30 +5915,12 @@ "swapUnknown": { "message": "Unknown" }, - "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 available to swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Advanced options" - }, - "swapsExcessiveSlippageWarning": { - "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Not enough $1 to complete this transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Not enough $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6492,14 +6435,6 @@ "verifyContractDetails": { "message": "Verify third-party details" }, - "verifyThisTokenOn": { - "message": "Verify this token on $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verify this token on $1 and make sure this is the token you want to trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 25cb6cd3df29..fc635e33a708 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -5341,10 +5341,6 @@ "swapAreYouStillThereDescription": { "message": "We’re ready to show you the latest quotes when you want to continue" }, - "swapBuildQuotePlaceHolderText": { - "message": "No tokens available matching $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -5409,9 +5405,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error fetching quotes" }, - "swapFetchingTokens": { - "message": "Fetching tokens..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5432,9 +5425,6 @@ "swapHighSlippage": { "message": "High slippage" }, - "swapHighSlippageWarning": { - "message": "Slippage amount is very high." - }, "swapIncludesMMFee": { "message": "Includes a $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5456,9 +5446,6 @@ "swapLowSlippage": { "message": "Low slippage" }, - "swapLowSlippageError": { - "message": "Transaction may fail, max slippage too low." - }, "swapMaxSlippage": { "message": "Max slippage" }, @@ -5493,9 +5480,6 @@ "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, @@ -5542,12 +5526,6 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, - "swapReviewSwap": { - "message": "Review swap" - }, - "swapSearchNameOrAddress": { - "message": "Search name or paste address" - }, "swapSelect": { "message": "Select" }, @@ -5580,9 +5558,6 @@ "swapSlippageLowTitle": { "message": "Low slippage" }, - "swapSlippageNegative": { - "message": "Slippage must be greater or equal to zero" - }, "swapSlippageNegativeDescription": { "message": "Slippage must be greater or equal to zero" }, @@ -5651,20 +5626,6 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "This token has been added manually." - }, - "swapTokenVerificationMessage": { - "message": "Always confirm the token address on $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Only verified on 1 source." - }, - "swapTokenVerificationSources": { - "message": "Verified on $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 is only verified on 1 source. Consider verifying it on $2 before proceeding.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5685,30 +5646,12 @@ "swapUnknown": { "message": "Unknown" }, - "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 available to swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Advanced options" - }, - "swapsExcessiveSlippageWarning": { - "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Not enough $1 to complete this transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Not enough $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6255,14 +6198,6 @@ "verifyContractDetails": { "message": "Verify third-party details" }, - "verifyThisTokenOn": { - "message": "Verify this token on $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verify this token on $1 and make sure this is the token you want to trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 03fa1e519ef7..97d6f4be9854 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -5189,10 +5189,6 @@ "swapAreYouStillThereDescription": { "message": "Estamos listos para mostrarle las últimas cotizaciones cuando desee continuar" }, - "swapBuildQuotePlaceHolderText": { - "message": "No hay tokens disponibles que coincidan con $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmar con su monedero físico" }, @@ -5257,9 +5253,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error al capturar cotizaciones" }, - "swapFetchingTokens": { - "message": "Capturando tokens…" - }, "swapFromTo": { "message": "El intercambio de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5280,9 +5273,6 @@ "swapHighSlippage": { "message": "Deslizamiento alto" }, - "swapHighSlippageWarning": { - "message": "El monto del deslizamiento es muy alto." - }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5304,9 +5294,6 @@ "swapLowSlippage": { "message": "Deslizamiento bajo" }, - "swapLowSlippageError": { - "message": "Es posible que la transacción tenga errores, el deslizamiento máximo es demasiado bajo." - }, "swapMaxSlippage": { "message": "Desfase máximo" }, @@ -5341,9 +5328,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." - }, "swapPriceUnavailableDescription": { "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el intercambio, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." }, @@ -5390,12 +5374,6 @@ "swapRequestForQuotation": { "message": "Solicitud de cotización" }, - "swapReviewSwap": { - "message": "Revisar intercambio" - }, - "swapSearchNameOrAddress": { - "message": "Buscar nombre o pegar dirección" - }, "swapSelect": { "message": "Seleccionar" }, @@ -5428,9 +5406,6 @@ "swapSlippageLowTitle": { "message": "Deslizamiento bajo" }, - "swapSlippageNegative": { - "message": "El deslizamiento debe ser mayor o igual que cero" - }, "swapSlippageNegativeDescription": { "message": "El deslizamiento debe ser mayor o igual que cero" }, @@ -5499,20 +5474,6 @@ "message": "Intercambiar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token se añadió de forma manual." - }, - "swapTokenVerificationMessage": { - "message": "Siempre confirme la dirección del token en $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Solo se verificó en una fuente." - }, - "swapTokenVerificationSources": { - "message": "Verificar en $1 fuentes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 solo se verifica en 1 fuente. Considere verificarlo en $2 antes de continuar.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5533,30 +5494,12 @@ "swapUnknown": { "message": "Desconocido" }, - "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibles para intercambio", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de deslizamiento" }, - "swapsAdvancedOptions": { - "message": "Opciones avanzadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "El monto del deslizamiento es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de deslizamiento a un valor menor al 15 %." - }, "swapsMaxSlippage": { "message": "Tolerancia de deslizamiento" }, - "swapsNotEnoughForTx": { - "message": "No hay $1 suficientes para completar esta transacción", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "No hay suficiente $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6052,14 +5995,6 @@ "verifyContractDetails": { "message": "Verificar detalles de terceros" }, - "verifyThisTokenOn": { - "message": "Comprobar este token en $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versión" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index cd980aaa99c2..672823c370ba 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1894,10 +1894,6 @@ "message": "Necesita $1 más $2 para realizar este canje", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "No hay tokens disponibles que coincidan con $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1949,9 +1945,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error al capturar cotizaciones" }, - "swapFetchingTokens": { - "message": "Capturando tokens..." - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1969,16 +1962,10 @@ "message": "Las tarifas de gas se pagan a los mineros de criptomonedas que procesan transacciones en la red $1. MetaMask no se beneficia de las tarifas de gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, - "swapHighSlippageWarning": { - "message": "El monto del desfase es muy alto." - }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapLowSlippageError": { - "message": "Es posible que la transacción tenga errores, el desfase máximo es demasiado bajo." - }, "swapMaxSlippage": { "message": "Desfase máximo" }, @@ -2009,9 +1996,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." - }, "swapPriceUnavailableDescription": { "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el canje, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." }, @@ -2051,9 +2035,6 @@ "swapRequestForQuotation": { "message": "Solicitud de cotización" }, - "swapReviewSwap": { - "message": "Revisar canje" - }, "swapSelect": { "message": "Seleccionar" }, @@ -2066,9 +2047,6 @@ "swapSelectQuotePopoverDescription": { "message": "A continuación se muestran todas las cotizaciones recopiladas de diversas fuentes de liquidez." }, - "swapSlippageNegative": { - "message": "El desfase debe ser mayor o igual que cero" - }, "swapSource": { "message": "Fuente de liquidez" }, @@ -2102,20 +2080,6 @@ "message": "Canjear $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token se añadió de forma manual." - }, - "swapTokenVerificationMessage": { - "message": "Siempre confirme la dirección del token en $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Solo se verificó en una fuente." - }, - "swapTokenVerificationSources": { - "message": "Verificar en $1 fuentes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTooManyDecimalsError": { "message": "$1 permite hasta $2 decimales", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -2129,30 +2093,12 @@ "swapUnknown": { "message": "Desconocido" }, - "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponible para canje", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de desfase" }, - "swapsAdvancedOptions": { - "message": "Opciones avanzadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." - }, "swapsMaxSlippage": { "message": "Tolerancia de desfase" }, - "swapsNotEnoughForTx": { - "message": "No hay $1 suficientes para finalizar esta transacción", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Ver en actividad" }, @@ -2389,14 +2335,6 @@ "userName": { "message": "Nombre de usuario" }, - "verifyThisTokenOn": { - "message": "Verificar este token en $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Ver todos los detalles" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index fccc617dee93..dbaffd44cf38 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Si vous le souhaitez, nous sommes prêts à vous présenter les dernières cotations" }, - "swapBuildQuotePlaceHolderText": { - "message": "Aucun jeton disponible correspondant à $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmez avec votre portefeuille matériel" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erreur lors de la récupération des cotations" }, - "swapFetchingTokens": { - "message": "Récupération des jetons…" - }, "swapFromTo": { "message": "Le swap de $1 vers $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Important effet de glissement" }, - "swapHighSlippageWarning": { - "message": "Le montant du glissement est très élevé." - }, "swapIncludesMMFee": { "message": "Comprend des frais MetaMask à hauteur de $1 %.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Faible effet de glissement" }, - "swapLowSlippageError": { - "message": "La transaction peut échouer, car le glissement maximal est trop faible." - }, "swapMaxSlippage": { "message": "Glissement maximal" }, @@ -5344,9 +5331,6 @@ "message": "Différence de prix de ~$1", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "L’incidence sur les prix correspond à la différence entre le prix actuel du marché et le montant reçu lors de l’exécution de la transaction. Cette répercussion dépend du volume de votre transaction par rapport au volume de la réserve de liquidités." - }, "swapPriceUnavailableDescription": { "message": "L’incidence sur les prix n’a pas pu être déterminée faute de données suffisantes sur les prix du marché. Veuillez confirmer que vous êtes satisfait·e du nombre de jetons que vous êtes sur le point de recevoir avant de procéder au swap." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Demande de cotation" }, - "swapReviewSwap": { - "message": "Vérifier le swap" - }, - "swapSearchNameOrAddress": { - "message": "Rechercher le nom ou coller l’adresse" - }, "swapSelect": { "message": "Sélectionner" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Faible effet de glissement" }, - "swapSlippageNegative": { - "message": "Le glissement doit être supérieur ou égal à zéro" - }, "swapSlippageNegativeDescription": { "message": "Le slippage doit être supérieur ou égal à zéro" }, @@ -5502,20 +5477,6 @@ "message": "Swap de $1 vers $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Ce jeton a été ajouté manuellement." - }, - "swapTokenVerificationMessage": { - "message": "Confirmez toujours l’adresse du jeton sur $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Vérification effectuée uniquement sur 1 source." - }, - "swapTokenVerificationSources": { - "message": "Vérification effectuée sur $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 n’a été vérifié que par 1 source. Envisagez de le vérifier auprès de $2 sources avant de continuer.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Inconnu" }, - "swapVerifyTokenExplanation": { - "message": "Attention, plusieurs jetons peuvent utiliser le même nom et le même symbole. Vérifiez $1 pour vous assurer qu’il s’agit bien du jeton que vous recherchez.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibles pour un swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de glissement" }, - "swapsAdvancedOptions": { - "message": "Options avancées" - }, - "swapsExcessiveSlippageWarning": { - "message": "Le montant du glissement est trop élevé et donnera lieu à un mauvais taux. Veuillez réduire votre tolérance de glissement à une valeur inférieure à 15 %." - }, "swapsMaxSlippage": { "message": "Tolérance au slippage" }, - "swapsNotEnoughForTx": { - "message": "Pas assez de $1 pour effectuer cette transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Pas assez de $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Vérifier les informations relatives aux tiers" }, - "verifyThisTokenOn": { - "message": "Vérifier ce jeton sur $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Vérifiez ce jeton sur $1 et qu’il s’agit bien de celui que vous souhaitez échanger.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 7333626d1e30..0e624b4ba807 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "जब आप जारी रखना चाहते हैं तो हम आपको लेटेस्ट उद्धरण दिखाने के लिए तैयार हैं" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1 के मिलान वाले कोई भी टोकन उपलब्ध नहीं हैं", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "अपने hardware wallet से कन्फर्म करें" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "उद्धरण प्राप्त करने में गड़बड़ी" }, - "swapFetchingTokens": { - "message": "टोकन प्राप्त किए जा रहे हैं..." - }, "swapFromTo": { "message": "$1 से $2 का स्वैप", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "अधिक स्लिपेज" }, - "swapHighSlippageWarning": { - "message": "स्लिपेज अमाउंट बहुत अधिक है।" - }, "swapIncludesMMFee": { "message": "$1% MetaMask फ़ीस शामिल है।", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "कम स्लिपेज" }, - "swapLowSlippageError": { - "message": "ट्रांसेक्शन विफल हो सकता है, अधिकतम स्लिपेज बहुत कम है।" - }, "swapMaxSlippage": { "message": "अधिकतम स्लिपेज" }, @@ -5344,9 +5331,6 @@ "message": "~$1% का मूल्य अंतर", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "मूल्य प्रभाव, वर्तमान बाजार मूल्य और ट्रांसेक्शन निष्पादन के दौरान प्राप्त अमाउंट के बीच का अंतर है। मूल्य प्रभाव चलनिधि पूल के आकार के सापेक्ष आपके व्यापार के आकार का एक कार्य है।" - }, "swapPriceUnavailableDescription": { "message": "बाजार मूल्य डेटा की कमी के कारण मूल्य प्रभाव को निर्धारित नहीं किया जा सका। कृपया कन्फर्म करें कि आप स्वैप करने से पहले प्राप्त होने वाले टोकन की अमाउंट को लेकर सहज हैं।" }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "उद्धरण के लिए अनुरोध" }, - "swapReviewSwap": { - "message": "स्वैप की समीक्षा करें" - }, - "swapSearchNameOrAddress": { - "message": "नाम खोजें या ऐड्रेस पेस्ट करें" - }, "swapSelect": { "message": "चयन करें" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "कम स्लिपेज" }, - "swapSlippageNegative": { - "message": "स्लिपेज शून्य से अधिक या बराबर होना चाहिए" - }, "swapSlippageNegativeDescription": { "message": "स्लिपेज शून्य से अधिक या बराबर होना चाहिए" }, @@ -5502,20 +5477,6 @@ "message": "$1 से $2 में स्वैप करें", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "इस टोकन को मैन्युअल रूप से जोड़ा गया है।" - }, - "swapTokenVerificationMessage": { - "message": "हमेशा $1 पर टोकन एड्रेस की कन्फर्म करें।", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "केवल 1 स्रोत पर वेरीफ़ाई।" - }, - "swapTokenVerificationSources": { - "message": "$1 स्रोतों पर वेरीफ़ाई।", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 केवल 1 स्रोत पर वेरीफ़ाई है। आगे बढ़ने से पहले इसे $2 पर वेरीफ़ाई करने पर विचार करें।", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "अज्ञात" }, - "swapVerifyTokenExplanation": { - "message": "एकाधिक टोकन एक ही नाम और प्रतीक का इस्तेमाल कर सकते हैं। यह वेरीफ़ाई करने के लिए $1 देखें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 स्वैप के लिए उपलब्ध है", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% स्लिपेज" }, - "swapsAdvancedOptions": { - "message": "एडवांस्ड विकल्प" - }, - "swapsExcessiveSlippageWarning": { - "message": "स्लिपेज अमाउंट बहुत अधिक है और इस वजह से खराब दर होगी। कृपया अपने स्लिपेज टॉलरेंस को 15% से नीचे के वैल्यू तक कम करें।" - }, "swapsMaxSlippage": { "message": "स्लिपेज टॉलरेंस" }, - "swapsNotEnoughForTx": { - "message": "इस ट्रांसेक्शन को पूरा करने के लिए $1 कम है", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 कम", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "थर्ड-पार्टी विवरण वेरीफ़ाई करें" }, - "verifyThisTokenOn": { - "message": "इस टोकन को $1 पर वेरीफ़ाई करें", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "इस टोकन को $1 पर वेरीफ़ाई करें और पक्का करें कि यह वही टोकन है जिससे आप ट्रेड करना चाहते हैं।", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "वर्शन" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index f2d8828e9226..68b52556c3f6 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Kami siap menampilkan kuotasi terbaru jika Anda ingin melanjutkan" }, - "swapBuildQuotePlaceHolderText": { - "message": "Tidak ada token yang cocok yang tersedia $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Konfirmasikan dengan dompet perangkat keras Anda" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Terjadi kesalahan saat mengambil kuota" }, - "swapFetchingTokens": { - "message": "Mengambil token..." - }, "swapFromTo": { "message": "Pertukaran dari $1 ke $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Selip tinggi" }, - "swapHighSlippageWarning": { - "message": "Jumlah slippage sangat tinggi." - }, "swapIncludesMMFee": { "message": "Termasuk $1% biaya MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Selip rendah" }, - "swapLowSlippageError": { - "message": "Transaksi berpotensi gagal, selip maks terlalu rendah." - }, "swapMaxSlippage": { "message": "Selipi maks" }, @@ -5344,9 +5331,6 @@ "message": "Perbedaan harga ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Dampak harga adalah selisih antara harga pasar saat ini dan jumlah yang diterima selama terjadinya transaksi. Dampak harga adalah fungsi ukuran dagang relatif terhadap ukuran pul likuiditas." - }, "swapPriceUnavailableDescription": { "message": "Dampak harga tidak dapat ditentukan karena kurangnya data harga pasar. Harap konfirmasi bahwa Anda setuju dengan jumlah token yang akan Anda terima sebelum penukaran." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Minta kuotasi" }, - "swapReviewSwap": { - "message": "Tinjau pertukaran" - }, - "swapSearchNameOrAddress": { - "message": "Cari nama atau tempel alamat" - }, "swapSelect": { "message": "Pilih" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Selip rendah" }, - "swapSlippageNegative": { - "message": "Selip harus lebih besar atau sama dengan nol" - }, "swapSlippageNegativeDescription": { "message": "Selip harus lebih besar atau sama dengan nol" }, @@ -5502,20 +5477,6 @@ "message": "Tukar $1 ke $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Token ini telah ditambahkan secara manual." - }, - "swapTokenVerificationMessage": { - "message": "Selalu konfirmasikan alamat token di $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Hanya diverifikasi di 1 sumber." - }, - "swapTokenVerificationSources": { - "message": "Diverifikasi di $1 sumber.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 hanya diverifikasi di 1 sumber. Pertimbangkan untuk memverifikasinya di $2 sebelum melanjutkan.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Tidak diketahui" }, - "swapVerifyTokenExplanation": { - "message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 tersedia untuk ditukar", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "Selip 0%" }, - "swapsAdvancedOptions": { - "message": "Opsi lanjutan" - }, - "swapsExcessiveSlippageWarning": { - "message": "Jumlah selip terlalu tinggi dan akan mengakibatkan tarif yang buruk. Kurangi toleransi selip Anda ke nilai di bawah 15%." - }, "swapsMaxSlippage": { "message": "Toleransi selip" }, - "swapsNotEnoughForTx": { - "message": "$1 tidak cukup untuk menyelesaikan transaksi ini", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 tidak cukup", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Verifikasikan detail pihak ketiga" }, - "verifyThisTokenOn": { - "message": "Verifikasikan token ini di $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifikasikan token ini di $1 dan pastikan ini adalah token yang ingin Anda perdagangkan.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versi" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 70e81c595852..4dac80c253b3 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1387,10 +1387,6 @@ "message": "Devi avere $1 $2 in più per completare lo scambio", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Non ci sono token disponibile con questo nome $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapCustom": { "message": "personalizza" }, @@ -1419,12 +1415,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Errore recuperando le quotazioni" }, - "swapFetchingTokens": { - "message": "Recuperando i token..." - }, - "swapLowSlippageError": { - "message": "La transazione può fallire, il massimo slippage è troppo basso." - }, "swapMaxSlippage": { "message": "Slippage massimo" }, @@ -1484,9 +1474,6 @@ "swapRequestForQuotation": { "message": "Richiedi quotazione" }, - "swapReviewSwap": { - "message": "Verifica Scambio" - }, "swapSelect": { "message": "Selezione" }, @@ -1519,44 +1506,15 @@ "message": "Scambia da $1 a $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationMessage": { - "message": "Verifica sempre l'indirizzo del token su $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificato solo su una fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificato su $1 fonti.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTransactionComplete": { "message": "Transazione completata" }, "swapUnknown": { "message": "Sconosciuto" }, - "swapVerifyTokenExplanation": { - "message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibili allo scambio", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, - "swapsAdvancedOptions": { - "message": "Impostazioni Avanzate" - }, - "swapsExcessiveSlippageWarning": { - "message": "L'importo di slippage è troppo alto e risulterà in una tariffa sconveniente. Riduci la tolleranza allo slippage ad un valore minore di 15%." - }, "swapsMaxSlippage": { "message": "Tolleranza Slippage" }, - "swapsNotEnoughForTx": { - "message": "Non hai abbastanza $1 per completare la transazione", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Vedi in attività" }, @@ -1691,10 +1649,6 @@ "userName": { "message": "Nome utente" }, - "verifyThisTokenOn": { - "message": "Verifica questo token su $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Vedi tutti i dettagli" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index cadc1ab1e302..73f3f8300646 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "続ける際には、最新のクォートを表示する準備ができています" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1と一致するトークンがありません", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "ハードウェアウォレットで確定" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "見積もり取得エラー" }, - "swapFetchingTokens": { - "message": "トークンを取得中..." - }, "swapFromTo": { "message": "$1から$2へのスワップ", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "高スリッページ" }, - "swapHighSlippageWarning": { - "message": "スリッページが非常に大きいです。" - }, "swapIncludesMMFee": { "message": "$1%のMetaMask手数料が含まれています。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "低スリッページ" }, - "swapLowSlippageError": { - "message": "トランザクションが失敗する可能性があります。最大スリッページが低すぎます。" - }, "swapMaxSlippage": { "message": "最大スリッページ" }, @@ -5344,9 +5331,6 @@ "message": "最大$1%の価格差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "プライスインパクトとは、現在の市場価格と取引の約定時に受け取る金額の差のことです。プライスインパクトは、流動性プールに対する取引の大きさにより発生します。" - }, "swapPriceUnavailableDescription": { "message": "市場価格のデータが不足しているため、プライスインパクトを測定できませんでした。スワップする前に、これから受領するトークンの額に問題がないか確認してください。" }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "見積もりのリクエスト" }, - "swapReviewSwap": { - "message": "スワップの確認" - }, - "swapSearchNameOrAddress": { - "message": "名前を検索するかアドレスを貼り付けてください" - }, "swapSelect": { "message": "選択" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "低スリッページ" }, - "swapSlippageNegative": { - "message": "スリッページは0以上でなければなりません。" - }, "swapSlippageNegativeDescription": { "message": "スリッページは0以上でなければなりません" }, @@ -5502,20 +5477,6 @@ "message": "$1を$2にスワップ", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "このトークンは手動で追加されました。" - }, - "swapTokenVerificationMessage": { - "message": "常に$1のトークンアドレスを確認してください。", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "1つのソースでのみ検証済みです。" - }, - "swapTokenVerificationSources": { - "message": "$1個のソースで検証済みです。", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1は1つのソースでしか検証されていません。進める前に$2で検証することをご検討ください。", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "不明" }, - "swapVerifyTokenExplanation": { - "message": "複数のトークンで同じ名前とシンボルを使用できます。$1をチェックして、これが探しているトークンであることを確認してください。", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2がスワップに使用可能です", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0%スリッページ" }, - "swapsAdvancedOptions": { - "message": "詳細オプション" - }, - "swapsExcessiveSlippageWarning": { - "message": "スリッページ額が非常に大きいので、レートが不利になります。最大スリッページを15%未満の値に減らしてください。" - }, "swapsMaxSlippage": { "message": "最大スリッページ" }, - "swapsNotEnoughForTx": { - "message": "トランザクションを完了させるには、$1が不足しています", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1が不足しています", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "サードパーティの詳細を確認" }, - "verifyThisTokenOn": { - "message": "このトークンを$1で検証", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "このトークンを$1で検証して、取引したいトークンであることを確認してください。", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "バージョン" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 760ad7df43dc..be1de55c51c7 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "계속하기 원하시면 최신 견적을 보여드리겠습니다" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1와(과) 일치하는 토큰이 없습니다.", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "하드웨어 지갑으로 컨펌합니다." }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "견적을 가져오는 중 오류 발생" }, - "swapFetchingTokens": { - "message": "토큰 가져오는 중..." - }, "swapFromTo": { "message": "$1을(를) $2(으)로 스왑", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "높은 슬리피지" }, - "swapHighSlippageWarning": { - "message": "슬리패지 금액이 아주 큽니다." - }, "swapIncludesMMFee": { "message": "$1%의 MetaMask 요금이 포함됩니다.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "낮은 슬리피지" }, - "swapLowSlippageError": { - "message": "트랜잭션이 실패할 수도 있습니다. 최대 슬리패지가 너무 낮습니다." - }, "swapMaxSlippage": { "message": "최대 슬리패지" }, @@ -5344,9 +5331,6 @@ "message": "~$1%의 가격 차이", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "가격 영향은 현재 시장 가격과 트랜잭션 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 트랜잭션의 크기를 나타내는 함수입니다." - }, "swapPriceUnavailableDescription": { "message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수가 만족스러운지 컨펌하시기 바랍니다." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "견적 요청" }, - "swapReviewSwap": { - "message": "스왑 검토" - }, - "swapSearchNameOrAddress": { - "message": "이름 검색 또는 주소 붙여넣기" - }, "swapSelect": { "message": "선택" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "낮은 슬리피지" }, - "swapSlippageNegative": { - "message": "슬리패지는 0보다 크거나 같아야 합니다." - }, "swapSlippageNegativeDescription": { "message": "슬리피지는 0보다 크거나 같아야 합니다." }, @@ -5502,20 +5477,6 @@ "message": "$1에서 $2(으)로 스왑", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "이 토큰은 직접 추가되었습니다." - }, - "swapTokenVerificationMessage": { - "message": "항상 $1에서 토큰 주소를 컨펌하세요.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "1개의 소스에서만 확인됩니다." - }, - "swapTokenVerificationSources": { - "message": "$1개 소스에서 확인되었습니다.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 토큰은 1 소스에서만 확인됩니다. 계속 진행하기 전에 $2에서도 확인하세요.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "알 수 없음" }, - "swapVerifyTokenExplanation": { - "message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1에서 원하는 토큰인지 확인하세요.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 스왑 가능", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% 슬리패지" }, - "swapsAdvancedOptions": { - "message": "고급 옵션" - }, - "swapsExcessiveSlippageWarning": { - "message": "슬리패지 금액이 너무 커서 전환율이 좋지 않습니다. 슬리패지 허용치를 15% 값 이하로 줄이세요." - }, "swapsMaxSlippage": { "message": "슬리피지 허용치" }, - "swapsNotEnoughForTx": { - "message": "$1이(가) 부족하여 이 트랜잭션을 완료할 수 없습니다.", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 부족", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "타사 세부 정보 확인" }, - "verifyThisTokenOn": { - "message": "$1에서 이 토큰 확인", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "$1에서 이 토큰이 트랜잭션할 토큰이 맞는지 확인하세요.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "버전" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index e12eb4379cf1..1687cb7818f0 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1268,10 +1268,6 @@ "message": "Kailangan mo ng $1 pa $2 para makumpleto ang pag-swap na ito", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Walang available na token na tumutugma sa $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin ang iyong hardware wallet" }, @@ -1313,9 +1309,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Nagka-error sa pagkuha ng mga quote" }, - "swapFetchingTokens": { - "message": "Kinukuha ang mga token..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1323,12 +1316,6 @@ "swapGasFeesSplit": { "message": "Hahatiin sa pagitan ng dalawang transaksyon na ito ang mga bayarin sa gas sa nakaraang screen." }, - "swapHighSlippageWarning": { - "message": "Sobrang laki ng halaga ng slippage." - }, - "swapLowSlippageError": { - "message": "Posibleng hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." - }, "swapMaxSlippage": { "message": "Max na slippage" }, @@ -1355,9 +1342,6 @@ "message": "Kaibahan sa presyo na ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Ang epekto sa presyo ay ang pagkakaiba sa kasalukuyang presyo sa merkado at sa halagang natanggap sa pag-execute ng transaksyon. Ang epekto sa presyo ay isang function ng laki ng iyong trade kumpara sa laki ng liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Hindi natukoy ang epekto sa presyo dahil sa kakulangan ng data sa presyo sa merkado. Pakikumpirma na kumportable ka sa dami ng mga token na matatanggap mo bago makipag-swap." }, @@ -1397,9 +1381,6 @@ "swapRequestForQuotation": { "message": "Mag-request ng quotation" }, - "swapReviewSwap": { - "message": "Suriin ang Pag-swap" - }, "swapSelect": { "message": "Piliin" }, @@ -1412,9 +1393,6 @@ "swapSelectQuotePopoverDescription": { "message": "Makikita sa ibaba ang lahat ng quote na nakuha mula sa maraming pinagkukunan ng liquidity." }, - "swapSlippageNegative": { - "message": "Dapat ay mas malaki sa o katumbas ng zero ang slippage" - }, "swapSource": { "message": "Pinagkunan ng liquidity" }, @@ -1442,20 +1420,6 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Manual na idinagdag ang token na ito." - }, - "swapTokenVerificationMessage": { - "message": "Palaging kumpirmahin ang address ng token sa $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Na-verify lang sa 1 source." - }, - "swapTokenVerificationSources": { - "message": "Na-verify sa $1 source.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTransactionComplete": { "message": "Nakumpleto ang transaksyon" }, @@ -1465,30 +1429,12 @@ "swapUnknown": { "message": "Hindi Alam" }, - "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Available ang $1 $2 na i-swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Mga Advanced na Opsyon" - }, - "swapsExcessiveSlippageWarning": { - "message": "Masyadong mataas ang halaga ng slippage at magreresulta ito sa masamang rating. Pakibabaan ang iyong tolerance ng slippage sa value na mas mababa sa 15%." - }, "swapsMaxSlippage": { "message": "Tolerance ng Slippage" }, - "swapsNotEnoughForTx": { - "message": "Hindi sapat ang $1 para makumpleto ang transaksyong ito", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Tingnan sa aktibidad" }, @@ -1648,14 +1594,6 @@ "userName": { "message": "Username" }, - "verifyThisTokenOn": { - "message": "I-verify ang token na ito sa $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "I-verify ang token na ito sa $1 at tiyaking ito ang token na gusto mong i-trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Tingnan ang lahat ng detalye" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 06c9fbe38adf..656733cecf65 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Estamos prontos para exibir as últimas cotações quando quiser continuar" }, - "swapBuildQuotePlaceHolderText": { - "message": "Nenhum token disponível correspondente a $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erro ao obter cotações" }, - "swapFetchingTokens": { - "message": "Obtendo tokens..." - }, "swapFromTo": { "message": "A troca de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Slippage alto" }, - "swapHighSlippageWarning": { - "message": "O valor de slippage está muito alto." - }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Slippage baixo" }, - "swapLowSlippageError": { - "message": "A transação pode falhar; o slippage máximo está baixo demais." - }, "swapMaxSlippage": { "message": "Slippage máximo" }, @@ -5344,9 +5331,6 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "O impacto do preço é a diferença entre o preço de mercado atual e o valor recebido quando é executada a transação. O impacto do preço é resultado do tamanho da sua transação relativo ao tamanho do pool de liquidez." - }, "swapPriceUnavailableDescription": { "message": "O impacto no preço não pôde ser determinado devido à ausência de dados sobre o preço de mercado. Confirme que você está satisfeito com a quantidade de tokens que você está prestes a receber antes de fazer a troca." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Solicitação de cotação" }, - "swapReviewSwap": { - "message": "Revisar troca" - }, - "swapSearchNameOrAddress": { - "message": "Pesquise o nome ou cole o endereço" - }, "swapSelect": { "message": "Selecione" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Slippage baixo" }, - "swapSlippageNegative": { - "message": "O slippage deve ser maior ou igual a zero" - }, "swapSlippageNegativeDescription": { "message": "O slippage deve ser maior ou igual a zero" }, @@ -5502,20 +5477,6 @@ "message": "Trocar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Esse token foi adicionado manualmente." - }, - "swapTokenVerificationMessage": { - "message": "Sempre confirme o endereço do token no $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificado somente em 1 fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificado em $1 fontes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 só foi verificado em 1 fonte. Considere verificá-lo em $2 antes de prosseguir.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Desconhecido" }, - "swapVerifyTokenExplanation": { - "message": "Vários tokens podem usar o mesmo nome e símbolo. Confira $1 para verificar se esse é o token que você está buscando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponível para troca", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% de slippage" }, - "swapsAdvancedOptions": { - "message": "Opções avançadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "O valor de slippage está muito alto e resultará em uma taxa ruim. Reduza sua tolerância a slippage para um valor inferior a 15%." - }, "swapsMaxSlippage": { "message": "Tolerância a slippage" }, - "swapsNotEnoughForTx": { - "message": "Não há $1 suficiente para concluir essa transação", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 insuficiente", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Verificar dados do terceiro" }, - "verifyThisTokenOn": { - "message": "Verifique esse token no $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique esse token no $1 e confirme que é o token que você deseja negociar.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versão" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 2becf1c495a1..6062013d6d6f 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1894,10 +1894,6 @@ "message": "Você precisa de mais $1 $2 para concluir essa troca", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Nenhum token disponível correspondente a $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -1949,9 +1945,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erro ao obter cotações" }, - "swapFetchingTokens": { - "message": "Obtendo tokens..." - }, "swapFromTo": { "message": "A troca de $1 para $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1969,16 +1962,10 @@ "message": "As taxas de gás são pagas aos mineradores de criptoativos que processam as transações na rede de $1. A MetaMask não lucra com taxas de gás.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, - "swapHighSlippageWarning": { - "message": "O valor de slippage está muito alto." - }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapLowSlippageError": { - "message": "A transação pode falhar; o slippage máximo está baixo demais." - }, "swapMaxSlippage": { "message": "Slippage máximo" }, @@ -2009,9 +1996,6 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "O impacto no preço é a diferença entre o preço de mercado atual e o valor recebido durante a execução da transação. O impacto no preço é uma função do porte da sua operação em relação ao porte do pool de liquidez." - }, "swapPriceUnavailableDescription": { "message": "O impacto no preço não pôde ser determinado devido à ausência de dados sobre o preço de mercado. Confirme que você está satisfeito com a quantidade de tokens que você está prestes a receber antes de fazer a troca." }, @@ -2051,9 +2035,6 @@ "swapRequestForQuotation": { "message": "Solicitação de cotação" }, - "swapReviewSwap": { - "message": "Revisar troca" - }, "swapSelect": { "message": "Selecione" }, @@ -2066,9 +2047,6 @@ "swapSelectQuotePopoverDescription": { "message": "Abaixo estão todas as cotações reunidas de diversas fontes de liquidez." }, - "swapSlippageNegative": { - "message": "O slippage deve ser maior ou igual a zero" - }, "swapSource": { "message": "Fonte de liquidez" }, @@ -2102,20 +2080,6 @@ "message": "Trocar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token foi adicionado manualmente." - }, - "swapTokenVerificationMessage": { - "message": "Sempre confirme o endereço do token no $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificado somente em 1 fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificado em $1 fontes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTooManyDecimalsError": { "message": "$1 permite até $2 decimais", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -2129,30 +2093,12 @@ "swapUnknown": { "message": "Desconhecido" }, - "swapVerifyTokenExplanation": { - "message": "Vários tokens podem usar o mesmo nome e símbolo. Confira $1 para verificar se esse é o token que você está buscando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponível para troca", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% de slippage" }, - "swapsAdvancedOptions": { - "message": "Opções avançadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "O valor de slippage está muito alto e resultará em uma taxa ruim. Reduza sua tolerância a slippage para um valor inferior a 15%." - }, "swapsMaxSlippage": { "message": "Tolerância a slippage" }, - "swapsNotEnoughForTx": { - "message": "Não há $1 suficiente para concluir essa transação", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Ver na atividade" }, @@ -2389,14 +2335,6 @@ "userName": { "message": "Nome de usuário" }, - "verifyThisTokenOn": { - "message": "Verifique esse token no $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique esse token no $1 e confirme que é o token que você deseja negociar.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Ver todos os detalhes" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 2308eb10721e..0c2f92821ed2 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Мы готовы показать вам последние котировки, когда вы захотите продолжить" }, - "swapBuildQuotePlaceHolderText": { - "message": "Нет доступных токенов, соответствующих $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Подтвердите с помощью аппаратного кошелька" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Ошибка при получении котировок" }, - "swapFetchingTokens": { - "message": "Получение токенов..." - }, "swapFromTo": { "message": "Своп $1 на $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Высокое проскальзывание" }, - "swapHighSlippageWarning": { - "message": "Сумма проскальзывания очень велика." - }, "swapIncludesMMFee": { "message": "Включает комиссию MetaMask в размере $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Низкое проскальзывание" }, - "swapLowSlippageError": { - "message": "Возможно, не удастся выполнить транзакцию. Ммаксимальное проскальзывание слишком низкое." - }, "swapMaxSlippage": { "message": "Максимальное проскальзывание" }, @@ -5344,9 +5331,6 @@ "message": "Разница в цене составляет ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Колебание цены — это разница между текущей рыночной ценой и суммой, полученной во время исполнения транзакции. Колебание цены зависит от соотношения размера вашей сделки и размера пула ликвидности." - }, "swapPriceUnavailableDescription": { "message": "Не удалось определить колебание цены из-за отсутствия данных о рыночных ценах. Перед свопом убедитесь, что вас устраивает количество токенов, которое вы получите." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Запрос котировки" }, - "swapReviewSwap": { - "message": "Проверить своп" - }, - "swapSearchNameOrAddress": { - "message": "Выполните поиск по имени или вставьте адрес" - }, "swapSelect": { "message": "Выбрать" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Низкое проскальзывание" }, - "swapSlippageNegative": { - "message": "Проскальзывание должно быть больше нуля или равно нулю" - }, "swapSlippageNegativeDescription": { "message": "Проскальзывание должно быть больше или равно нулю" }, @@ -5502,20 +5477,6 @@ "message": "Выполнить своп $1 на $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Этот токен был добавлен вручную." - }, - "swapTokenVerificationMessage": { - "message": "Всегда проверяйте адрес токена на $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Токен проверен только в 1 источнике." - }, - "swapTokenVerificationSources": { - "message": "Токен проверен в $1 источниках.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 проверяется только на 1 источнике. Попробуйте проверить его на $2, прежде чем продолжить.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Неизвестно" }, - "swapVerifyTokenExplanation": { - "message": "Для обозначения нескольких токенов могут использоваться одно и то же имя и символ. Убедитесь, что $1 — это именно тот токен, который вы ищете.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 доступны для свопа", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% проскальзывания" }, - "swapsAdvancedOptions": { - "message": "Дополнительные параметры" - }, - "swapsExcessiveSlippageWarning": { - "message": "Величина проскальзывания очень велика. Сделка будет невыгодной. Снизьте допуск проскальзывания ниже 15%." - }, "swapsMaxSlippage": { "message": "Допуск проскальзывания" }, - "swapsNotEnoughForTx": { - "message": "Недостаточно $1 для выполнения этой транзакции", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Недостаточно $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Проверьте информацию о третьей стороне" }, - "verifyThisTokenOn": { - "message": "Проверить этот токен на $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Проверьте этот токен на $1 и убедитесь, что это тот токен, которым вы хотите торговать.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Версия" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 8e3d8fd7fdd0..61d8ff6e5d8c 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Handa kaming ipakita sa iyo ang mga pinakabagong quote kapag gusto mo ng magpatuloy" }, - "swapBuildQuotePlaceHolderText": { - "message": "Walang available na token na tumutugma sa $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin gamit ang iyong wallet na hardware" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Nagka-error sa pagkuha ng mga quote" }, - "swapFetchingTokens": { - "message": "Kinukuha ang mga token..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Mataas na slippage" }, - "swapHighSlippageWarning": { - "message": "Napakataas ng halaga ng slippage." - }, "swapIncludesMMFee": { "message": "Kasama ang $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Mababang slippage" }, - "swapLowSlippageError": { - "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." - }, "swapMaxSlippage": { "message": "Max na slippage" }, @@ -5344,9 +5331,6 @@ "message": "Deperensya ng presyo ng ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Ang price impact ay ang pagkakaiba sa pagitan ng kasalukuyang market price at ang halagang natanggap sa panahon ng pagpapatupad ng transaksyon. Ang price impact ay isang function ng laki ng iyong trade kaugnay sa laki ng pool ng liquidity." - }, "swapPriceUnavailableDescription": { "message": "Hindi matukoy ang price impact dahil sa kakulangan ng data ng market price. Pakikumpirma na komportable ka sa dami ng mga token na matatanggap mo bago mag-swap." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Humiling ng quotation" }, - "swapReviewSwap": { - "message": "I-review ang pag-swap" - }, - "swapSearchNameOrAddress": { - "message": "Hanapin ang pangalan o i-paste ang address" - }, "swapSelect": { "message": "Piliin" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Mababang slippage" }, - "swapSlippageNegative": { - "message": "Ang slippage ay dapat mas malaki o katumbas ng zero" - }, "swapSlippageNegativeDescription": { "message": "Dapat na mas malaki o katumbas ng zero ang slippage" }, @@ -5502,20 +5477,6 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Manwal na naidagdag ang token na ito." - }, - "swapTokenVerificationMessage": { - "message": "Palaging kumpirmahin ang token address sa $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Na-verify sa 1 pinagmulan lang." - }, - "swapTokenVerificationSources": { - "message": "Na-verify sa $1 na source.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "Na-verify $1 sa 1 pinagmulan lang. Pag-isipang i-verify ito sa $2 bago magpatuloy.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Hindi Alam" }, - "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Available ang $1 $2 na i-swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Mga Advanced na Opsyon" - }, - "swapsExcessiveSlippageWarning": { - "message": "Masyadong mataas ang halaga ng slippage at magreresulta sa masamang rate. Mangyaring bawasan ang iyong slippage tolerance sa halagang mas mababa sa 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Hindi sapat ang $1 para makumpleto ang transaksyong ito", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Hindi sapat ang $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "I-verify ang mga detalye ng third-party" }, - "verifyThisTokenOn": { - "message": "I-verify ang token na ito sa $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "I-verify ang token na ito sa $1 at siguruhin na ito ang token na gusto mong i-trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Bersyon" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 06d2f1de953f..d80d6564b880 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Devam etmek istediğinizde size en yeni kotaları göstermeye hazırız" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1 ile eşleşen token yok", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Donanım cüzdanınızla onaylayın" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Teklifler alınırken hata" }, - "swapFetchingTokens": { - "message": "Tokenler alınıyor..." - }, "swapFromTo": { "message": "$1 ile $2 swap işlemi", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Yüksek kayma" }, - "swapHighSlippageWarning": { - "message": "Kayma tutarı çok yüksek." - }, "swapIncludesMMFee": { "message": "%$1 MetaMask ücreti dahildir.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Düşük kayma" }, - "swapLowSlippageError": { - "message": "İşlem başarısız olabilir, maks. kayma çok düşük." - }, "swapMaxSlippage": { "message": "Maks. kayma" }, @@ -5344,9 +5331,6 @@ "message": "~%$1 fiyat farkı", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Fiyat etkisi, mevcut piyasa fiyatı ile işlem gerçekleştirildiği sırada alınan tutar arasındaki farktır. Fiyat etkisi, likidite havuzunun boyutuna bağlı olarak işleminizin boyutunun bir fonksiyonudur." - }, "swapPriceUnavailableDescription": { "message": "Fiyat etkisi, piyasa fiyat verisinin mevcut olmaması nedeniyle belirlenememiştir. Swap işlemini gerçekleştirmeden önce lütfen almak üzere olduğunuz token tutarının sizin için uygun olduğunu onaylayın." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Teklif talebi" }, - "swapReviewSwap": { - "message": "Swap'ı incele" - }, - "swapSearchNameOrAddress": { - "message": "İsmi arayın veya adresi yapıştırın" - }, "swapSelect": { "message": "Seç" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Düşük kayma" }, - "swapSlippageNegative": { - "message": "Kayma en az sıfır olmalıdır" - }, "swapSlippageNegativeDescription": { "message": "Kayma en az sıfır olmalıdır" }, @@ -5502,20 +5477,6 @@ "message": "$1 ile $2 swap gerçekleştir", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Bu token manuel olarak eklendi." - }, - "swapTokenVerificationMessage": { - "message": "Her zaman token adresini $1 üzerinde onaylayın.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Sadece 1 kaynakta doğrulandı." - }, - "swapTokenVerificationSources": { - "message": "$1 kaynakta doğrulandı.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 sadece 1 kaynakta doğrulandı. İlerlemeden önce $2 üzerinde doğrulamayı deneyin.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Bilinmiyor" }, - "swapVerifyTokenExplanation": { - "message": "Birden fazla token aynı adı ve sembolü kullanabilir. Aradığınız tokenin bu olup olmadığını $1 alanında kontrol edin.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 için swap işlemi yapılabilir", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "%0 Kayma" }, - "swapsAdvancedOptions": { - "message": "Gelişmiş seçenekler" - }, - "swapsExcessiveSlippageWarning": { - "message": "Kayma tutarı çok yüksek ve kötü bir orana neden olacak. Lütfen kayma toleransınızı %15'in altında bir değere düşürün." - }, "swapsMaxSlippage": { "message": "Kayma toleransı" }, - "swapsNotEnoughForTx": { - "message": "Bu işlemi tamamlamak için yeterli $1 yok", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Yeterli $1 yok", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Üçüncü taraf bilgilerini doğrula" }, - "verifyThisTokenOn": { - "message": "Şurada bu tokeni doğrula: $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Bu tokeni $1 ile doğrulayın ve işlem yapmak istediğiniz tokenin bu olduğundan emin olun.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Sürüm" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 89772c1d4eec..0bac5423d1ee 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Chúng tôi sẵn sàng cho bạn xem báo giá mới nhất khi bạn muốn tiếp tục" }, - "swapBuildQuotePlaceHolderText": { - "message": "Không có token nào khớp với $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Xác nhận ví cứng của bạn" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Lỗi tìm nạp báo giá" }, - "swapFetchingTokens": { - "message": "Đang tìm nạp token..." - }, "swapFromTo": { "message": "Giao dịch hoán đổi $1 sang $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Mức trượt giá cao" }, - "swapHighSlippageWarning": { - "message": "Số tiền trượt giá rất cao." - }, "swapIncludesMMFee": { "message": "Bao gồm $1% phí của MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Mức trượt giá thấp" }, - "swapLowSlippageError": { - "message": "Giao dịch có thể không thành công, mức trượt giá tối đa quá thấp." - }, "swapMaxSlippage": { "message": "Mức trượt giá tối đa" }, @@ -5344,9 +5331,6 @@ "message": "Chênh lệch giá ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Tác động về giá là mức chênh lệch giữa giá thị trường hiện tại và số tiền nhận được trong quá trình thực hiện giao dịch. Tác động giá là một hàm trong quy mô giao dịch của bạn so với quy mô của nhóm thanh khoản." - }, "swapPriceUnavailableDescription": { "message": "Không thể xác định tác động giá do thiếu dữ liệu giá thị trường. Vui lòng xác nhận rằng bạn cảm thấy thoải mái với số lượng token bạn sắp nhận được trước khi hoán đổi." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Yêu cầu báo giá" }, - "swapReviewSwap": { - "message": "Xem lại giao dịch hoán đổi" - }, - "swapSearchNameOrAddress": { - "message": "Tìm kiếm tên hoặc dán địa chỉ" - }, "swapSelect": { "message": "Chọn" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Mức trượt giá thấp" }, - "swapSlippageNegative": { - "message": "Mức trượt giá phải lớn hơn hoặc bằng 0" - }, "swapSlippageNegativeDescription": { "message": "Mức trượt giá phải lớn hơn hoặc bằng 0" }, @@ -5502,20 +5477,6 @@ "message": "Hoán đổi $1 sang $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Token này đã được thêm theo cách thủ công." - }, - "swapTokenVerificationMessage": { - "message": "Luôn xác nhận địa chỉ token trên $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Chỉ được xác minh trên 1 nguồn." - }, - "swapTokenVerificationSources": { - "message": "Đã xác minh trên $1 nguồn.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 chỉ được xác minh trên 1 nguồn. Hãy xem xét xác minh nó trên $2 trước khi tiếp tục.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Không xác định" }, - "swapVerifyTokenExplanation": { - "message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Có sẵn $1 $2 để hoán đổi", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "Mức trượt giá 0%" }, - "swapsAdvancedOptions": { - "message": "Tùy chọn nâng cao" - }, - "swapsExcessiveSlippageWarning": { - "message": "Mức trượt giá quá cao và sẽ dẫn đến tỷ giá không sinh lời. Vui lòng giảm giới hạn trượt giá xuống một giá trị thấp hơn 15%." - }, "swapsMaxSlippage": { "message": "Giới hạn trượt giá" }, - "swapsNotEnoughForTx": { - "message": "Không đủ $1 để hoàn thành giao dịch này", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Không đủ $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Xác minh thông tin bên thứ ba" }, - "verifyThisTokenOn": { - "message": "Xác minh token này trên $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Hãy xác minh token này trên $1 và đảm bảo đây là token bạn muốn giao dịch.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Phiên bản" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index b4816b165545..7209fb1c5b44 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "如果您想继续,我们准备好为您显示最新报价" }, - "swapBuildQuotePlaceHolderText": { - "message": "没有与 $1 匹配的代币", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "使用您的硬件钱包确认" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "获取报价出错" }, - "swapFetchingTokens": { - "message": "获取代币中……" - }, "swapFromTo": { "message": "$1 到 $2 的交换", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "高滑点" }, - "swapHighSlippageWarning": { - "message": "滑点金额非常高。" - }, "swapIncludesMMFee": { "message": "包括 $1% 的 MetaMask 费用。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "低滑点" }, - "swapLowSlippageError": { - "message": "交易可能会失败,最大滑点过低。" - }, "swapMaxSlippage": { "message": "最大滑点" }, @@ -5344,9 +5331,6 @@ "message": "~$1% 的价差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "价格影响是当前市场价格与交易执行期间收到的金额之间的差异。价格影响是您的交易规模相对于流动性池规模的一个函数。" - }, "swapPriceUnavailableDescription": { "message": "由于缺乏市场价格数据,无法确定价格影响。在交换之前,请确认您对即将收到的代币数量感到满意。" }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "请求报价" }, - "swapReviewSwap": { - "message": "审查交换" - }, - "swapSearchNameOrAddress": { - "message": "搜索名称或粘贴地址" - }, "swapSelect": { "message": "选择" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "低滑点" }, - "swapSlippageNegative": { - "message": "滑点必须大于或等于0" - }, "swapSlippageNegativeDescription": { "message": "滑点必须大于或等于 0" }, @@ -5502,20 +5477,6 @@ "message": "用 $1 交换 $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "此代币已手动添加。" - }, - "swapTokenVerificationMessage": { - "message": "始终在 $1 上确认代币地址。", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "仅在1个来源上进行了验证。" - }, - "swapTokenVerificationSources": { - "message": "在 $1 个来源上进行了验证。", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 仅在 1 个源上进行了验证。在继续之前,考虑在 $2 上进行验证。", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "未知" }, - "swapVerifyTokenExplanation": { - "message": "多个代币可以使用相同的名称和符号。检查 $1 以确认这是您正在寻找的代币。", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 可用于交换", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0%滑点" }, - "swapsAdvancedOptions": { - "message": "高级选项" - }, - "swapsExcessiveSlippageWarning": { - "message": "滑点金额太高,会导致不良率。请将最大滑点降低到低于15%的值。" - }, "swapsMaxSlippage": { "message": "最大滑点" }, - "swapsNotEnoughForTx": { - "message": "没有足够的 $1 来完成此交易", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 不足", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "验证第三方详情" }, - "verifyThisTokenOn": { - "message": "在 $1 上验证此代币", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "在 $1 上验证此代币,并确保这是您想要交易的代币。", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "版本" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 32e98ed12288..7cdfa8e28add 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -1211,9 +1211,6 @@ "supportCenter": { "message": "造訪我們的協助中心" }, - "swapSearchNameOrAddress": { - "message": "用名稱搜尋或貼上地址" - }, "switchEthereumChainConfirmationDescription": { "message": "這將在 MetaMask 中將目前選擇的網路切換到剛才新增的網路:" }, @@ -1373,10 +1370,6 @@ "userName": { "message": "使用者名稱" }, - "verifyThisTokenOn": { - "message": "在 $1 驗證這個代幣的資訊", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "查看所有詳情" }, diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 107c1bd7ad14..17d68bd0e500 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -1497,10 +1497,6 @@ "ui/pages/swaps/awaiting-swap/swap-failure-icon.test.js", "ui/pages/swaps/awaiting-swap/swap-success-icon.js", "ui/pages/swaps/awaiting-swap/swap-success-icon.test.js", - "ui/pages/swaps/build-quote/build-quote.js", - "ui/pages/swaps/build-quote/build-quote.stories.js", - "ui/pages/swaps/build-quote/build-quote.test.js", - "ui/pages/swaps/build-quote/index.js", "ui/pages/swaps/countdown-timer/countdown-timer.js", "ui/pages/swaps/countdown-timer/countdown-timer.stories.js", "ui/pages/swaps/countdown-timer/countdown-timer.test.js", @@ -1510,14 +1506,6 @@ "ui/pages/swaps/create-new-swap/create-new-swap.js", "ui/pages/swaps/create-new-swap/create-new-swap.test.js", "ui/pages/swaps/create-new-swap/index.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js", - "ui/pages/swaps/dropdown-input-pair/index.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js", - "ui/pages/swaps/dropdown-search-list/index.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.stories.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.test.js", @@ -1542,12 +1530,6 @@ "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.test.js", "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js", "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.test.js", - "ui/pages/swaps/main-quote-summary/index.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.test.js", - "ui/pages/swaps/main-quote-summary/quote-backdrop.js", - "ui/pages/swaps/main-quote-summary/quote-backdrop.test.js", "ui/pages/swaps/searchable-item-list/index.js", "ui/pages/swaps/searchable-item-list/item-list/index.js", "ui/pages/swaps/searchable-item-list/item-list/item-list.component.js", @@ -1568,10 +1550,6 @@ "ui/pages/swaps/select-quote-popover/sort-list/index.js", "ui/pages/swaps/select-quote-popover/sort-list/sort-list.js", "ui/pages/swaps/select-quote-popover/sort-list/sort-list.test.js", - "ui/pages/swaps/slippage-buttons/index.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.test.js", "ui/pages/swaps/smart-transaction-status/arrow-icon.js", "ui/pages/swaps/smart-transaction-status/arrow-icon.test.js", "ui/pages/swaps/smart-transaction-status/canceled-icon.js", @@ -1597,11 +1575,6 @@ "ui/pages/swaps/view-on-block-explorer/index.js", "ui/pages/swaps/view-on-block-explorer/view-on-block-explorer.js", "ui/pages/swaps/view-on-block-explorer/view-on-block-explorer.test.js", - "ui/pages/swaps/view-quote/index.js", - "ui/pages/swaps/view-quote/view-quote-price-difference.js", - "ui/pages/swaps/view-quote/view-quote-price-difference.test.js", - "ui/pages/swaps/view-quote/view-quote.js", - "ui/pages/swaps/view-quote/view-quote.test.js", "ui/pages/token-details/index.js", "ui/pages/token-details/token-details-page.js", "ui/pages/token-details/token-details-page.test.js", diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 82c55c9bd7e0..2d9e50002a18 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -1209,10 +1209,6 @@ "extension_active": true, "mobile_active": true }, - "swapRedesign": { - "extensionActive": true, - "mobileActive": false - }, "zksync": { "extensionActive": true, "extension_active": true, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index a18f2e0b6944..55ffa7f9ba1b 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -397,10 +397,6 @@ export const createSwapsMockStore = () => { mobileActive: true, extensionActive: true, }, - swapRedesign: { - mobileActive: true, - extensionActive: true, - }, }, quotes: { TEST_AGG_1: { diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 0e1947d023f3..63bcdd2f58e6 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -28,7 +28,7 @@ import { import { I18nContext } from '../../../contexts/i18n'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, ///: END:ONLY_INCLUDE_IF SEND_ROUTE, } from '../../../helpers/constants/routes'; @@ -270,10 +270,10 @@ const CoinButtons = ({ dispatch(setSwapsFromToken(defaultSwapsToken)); if (usingHardwareWallet) { if (global.platform.openExtensionInBrowser) { - global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); + global.platform.openExtensionInBrowser(PREPARE_SWAP_ROUTE); } } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } } ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js index 9d30874c6b7b..904862ae7bb8 100644 --- a/ui/components/multichain/app-header/app-header.js +++ b/ui/components/multichain/app-header/app-header.js @@ -9,7 +9,6 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { - BUILD_QUOTE_ROUTE, CONFIRM_TRANSACTION_ROUTE, SWAPS_ROUTE, } from '../../../helpers/constants/routes'; @@ -66,17 +65,13 @@ export const AppHeader = ({ location }) => { const isSwapsPage = Boolean( matchPath(location.pathname, { path: SWAPS_ROUTE, exact: false }), ); - const isSwapsBuildQuotePage = Boolean( - matchPath(location.pathname, { path: BUILD_QUOTE_ROUTE, exact: false }), - ); const unapprovedTransactions = useSelector(getUnapprovedTransactions); const hasUnapprovedTransactions = Object.keys(unapprovedTransactions).length > 0; - const disableAccountPicker = - isConfirmationPage || (isSwapsPage && !isSwapsBuildQuotePage); + const disableAccountPicker = isConfirmationPage || isSwapsPage; const disableNetworkPicker = isSwapsPage || diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index cf8348243238..ece4292fdf14 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -34,11 +34,11 @@ import { import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, - BUILD_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, SWAPS_MAINTENANCE_ROUTE, SMART_TRANSACTION_STATUS_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../helpers/constants/routes'; import { fetchSwapsFeatureFlags, @@ -335,15 +335,6 @@ export const getCurrentSmartTransactionsEnabled = (state) => { return smartTransactionsEnabled && !currentSmartTransactionsError; }; -export const getSwapRedesignEnabled = (state) => { - const swapRedesign = - state.metamask.swapsState?.swapsFeatureFlags?.swapRedesign; - if (swapRedesign === undefined) { - return true; // By default show the redesign if we don't have feature flags returned yet. - } - return swapRedesign.extensionActive; -}; - export const getSwapsQuoteRefreshTime = (state) => state.metamask.swapsState.swapsQuoteRefreshTime; @@ -526,12 +517,12 @@ export { slice as swapsSlice, }; -export const navigateBackToBuildQuote = (history) => { +export const navigateBackToPrepareSwap = (history) => { return async (dispatch) => { // TODO: Ensure any fetch in progress is cancelled await dispatch(setBackgroundSwapRouteState('')); dispatch(navigatedBackToBuildQuote()); - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); }; }; diff --git a/ui/ducks/swaps/swaps.test.js b/ui/ducks/swaps/swaps.test.js index 83c133572c0d..fb2cc5e78a05 100644 --- a/ui/ducks/swaps/swaps.test.js +++ b/ui/ducks/swaps/swaps.test.js @@ -924,24 +924,5 @@ describe('Ducks - Swaps', () => { expect(newState.customGas.limit).toBe(null); }); }); - - describe('getSwapRedesignEnabled', () => { - it('returns true if feature flags are not returned from backend yet', () => { - const state = createSwapsMockStore(); - delete state.metamask.swapsState.swapsFeatureFlags.swapRedesign; - expect(swaps.getSwapRedesignEnabled(state)).toBe(true); - }); - - it('returns false if the extension feature flag for swaps redesign is false', () => { - const state = createSwapsMockStore(); - state.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = false; - expect(swaps.getSwapRedesignEnabled(state)).toBe(false); - }); - - it('returns true if the extension feature flag for swaps redesign is true', () => { - const state = createSwapsMockStore(); - expect(swaps.getSwapRedesignEnabled(state)).toBe(true); - }); - }); }); }); diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 5e4fffe413e2..bf38109ec9d7 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -241,12 +241,6 @@ PATH_NAME_MAP[PREPARE_SWAP_ROUTE] = 'Prepare Swap Page'; export const SWAPS_NOTIFICATION_ROUTE = '/swaps/notification-page'; PATH_NAME_MAP[SWAPS_NOTIFICATION_ROUTE] = 'Swaps Notification Page'; -export const BUILD_QUOTE_ROUTE = '/swaps/build-quote'; -PATH_NAME_MAP[BUILD_QUOTE_ROUTE] = 'Swaps Build Quote Page'; - -export const VIEW_QUOTE_ROUTE = '/swaps/view-quote'; -PATH_NAME_MAP[VIEW_QUOTE_ROUTE] = 'Swaps View Quotes Page'; - export const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'; PATH_NAME_MAP[LOADING_QUOTES_ROUTE] = 'Swaps Loading Quotes Page'; diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index bb3f129bade8..7921af85f2ce 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -6,7 +6,7 @@ import { I18nContext } from '../../../contexts/i18n'; import { SEND_ROUTE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, ///: END:ONLY_INCLUDE_IF } from '../../../helpers/constants/routes'; import { startNewDraftTransaction } from '../../../ducks/send'; @@ -276,12 +276,12 @@ const TokenButtons = ({ ); if (usingHardwareWallet) { global.platform.openExtensionInBrowser?.( - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, undefined, false, ); } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } ///: END:ONLY_INCLUDE_IF }} diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index a73cfa370681..0d0d4c21c71f 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -64,8 +64,6 @@ describe('Bridge', () => { it('renders the component with initial props', async () => { const swapsMockStore = createBridgeMockStore({ extensionSupport: true }); - swapsMockStore.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = - true; const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider( diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 2df3f2907266..37c147427ac5 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -62,8 +62,7 @@ import { CONNECTED_ROUTE, CONNECTED_ACCOUNTS_ROUTE, AWAITING_SWAP_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, CONFIRMATION_V_NEXT_ROUTE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -328,10 +327,8 @@ export default class Home extends PureComponent { const canRedirect = !isNotification && !stayOnHomePage; if (canRedirect && showAwaitingSwapScreen) { history.push(AWAITING_SWAP_ROUTE); - } else if (canRedirect && haveSwapsQuotes) { - history.push(VIEW_QUOTE_ROUTE); - } else if (canRedirect && swapsFetchParams) { - history.push(BUILD_QUOTE_ROUTE); + } else if (canRedirect && (haveSwapsQuotes || swapsFetchParams)) { + history.push(PREPARE_SWAP_ROUTE); } else if (firstPermissionsRequestId) { history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`); } else if (pendingConfirmationsPrioritized.length > 0) { diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 7cfd33f655ac..a27d59e3b33b 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -71,7 +71,6 @@ import { SWAPS_ROUTE, SETTINGS_ROUTE, UNLOCK_ROUTE, - BUILD_QUOTE_ROUTE, CONFIRMATION_V_NEXT_ROUTE, ONBOARDING_ROUTE, ONBOARDING_UNLOCK_ROUTE, @@ -493,13 +492,6 @@ export default class Routes extends Component { ); } - onSwapsBuildQuotePage() { - const { location } = this.props; - return Boolean( - matchPath(location.pathname, { path: BUILD_QUOTE_ROUTE, exact: false }), - ); - } - onHomeScreen() { const { location } = this.props; return location.pathname === DEFAULT_ROUTE; diff --git a/ui/pages/swaps/__snapshots__/index.test.js.snap b/ui/pages/swaps/__snapshots__/index.test.js.snap index 779bb78555d5..c7a58c20dac8 100644 --- a/ui/pages/swaps/__snapshots__/index.test.js.snap +++ b/ui/pages/swaps/__snapshots__/index.test.js.snap @@ -12,17 +12,29 @@ exports[`Swap renders the component with initial props 1`] = ` class="swaps__header" > <div - class="swaps__header-edit" - /> + class="box box--margin-left-4 box--display-flex box--flex-direction-row box--justify-content-center box--width-1/12" + tabindex="0" + > + <span + class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/arrow-2-left.svg'); cursor: pointer;" + title="Cancel" + /> + </div> <div class="swaps__title" > Swap </div> <div - class="swaps__header-cancel" + class="box box--margin-right-4 box--display-flex box--flex-direction-row box--justify-content-center box--width-1/12" + tabindex="0" > - Cancel + <span + class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/setting.svg'); cursor: pointer;" + title="Transaction settings" + /> </div> </div> <div diff --git a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js index 777e253a70fe..a1b4179beefb 100644 --- a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js +++ b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js @@ -20,7 +20,7 @@ import { } from '../../../../shared/modules/selectors'; import { DEFAULT_ROUTE, - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import PulseLoader from '../../../components/ui/pulse-loader'; import Box from '../../../components/ui/box'; @@ -150,7 +150,7 @@ export default function AwaitingSignatures() { // Go to the default route and then to the build quote route in order to clean up // the `inputValue` local state in `pages/swaps/index.js` history.push(DEFAULT_ROUTE); - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); }} submitText={t('cancel')} hideCancel diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index 660f7ef4fcae..b5c2589fbd28 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -33,7 +33,7 @@ import { getApproveTxParams, getUsedSwapsGasPrice, fetchQuotesAndSetQuoteState, - navigateBackToBuildQuote, + navigateBackToPrepareSwap, prepareForRetryGetQuotes, prepareToLeaveSwaps, getCurrentSmartTransactionsEnabled, @@ -318,7 +318,7 @@ export default function AwaitingSwap({ ), ); } else if (errorKey) { - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); } else if ( isSwapsDefaultTokenSymbol(destinationTokenSymbol, chainId) || swapComplete @@ -329,7 +329,9 @@ export default function AwaitingSwap({ history.push(DEFAULT_ROUTE); } }} - onCancel={async () => await dispatch(navigateBackToBuildQuote(history))} + onCancel={async () => + await dispatch(navigateBackToPrepareSwap(history)) + } submitText={submitText} disabled={submittingSwap} hideCancel={errorKey !== QUOTES_EXPIRED_ERROR} diff --git a/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap b/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap deleted file mode 100644 index b0551966d1c6..000000000000 --- a/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BuildQuote renders the component with initial props 1`] = ` -<div - class="button-group slippage-buttons__button-group radio-button-group" - role="radiogroup" -> - <button - aria-checked="true" - class="button-group__button radio-button button-group__button--active radio-button--active" - data-testid="button-group__button0" - role="radio" - > - 2% - </button> - <button - aria-checked="false" - class="button-group__button radio-button" - data-testid="button-group__button1" - role="radio" - > - 3% - </button> - <button - aria-checked="false" - class="button-group__button slippage-buttons__button-group-custom-button radio-button" - data-testid="button-group__button2" - role="radio" - > - custom - </button> -</div> -`; diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js deleted file mode 100644 index 23ca35b3f2e9..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.js +++ /dev/null @@ -1,800 +0,0 @@ -import React, { useContext, useEffect, useState, useCallback } from 'react'; -import BigNumber from 'bignumber.js'; -import PropTypes from 'prop-types'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import classnames from 'classnames'; -import { uniqBy, isEqual } from 'lodash'; -import { useHistory } from 'react-router-dom'; -import { getTokenTrackerLink } from '@metamask/etherscan-link'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - useTokensToSearch, - getRenderableTokenData, -} from '../../../hooks/useTokensToSearch'; -import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; -import { I18nContext } from '../../../contexts/i18n'; -import DropdownInputPair from '../dropdown-input-pair'; -import DropdownSearchList from '../dropdown-search-list'; -import SlippageButtons from '../slippage-buttons'; -import { getTokens, getConversionRate } from '../../../ducks/metamask/metamask'; -import InfoTooltip from '../../../components/ui/info-tooltip'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import { - VIEW_QUOTE_ROUTE, - LOADING_QUOTES_ROUTE, -} from '../../../helpers/constants/routes'; - -import { - fetchQuotesAndSetQuoteState, - setSwapsFromToken, - setSwapToToken, - getFromToken, - getToToken, - getBalanceError, - getTopAssets, - getFetchParams, - getQuotes, - setBalanceError, - setFromTokenInputValue, - setFromTokenError, - setMaxSlippage, - setReviewSwapClickedTimestamp, - getCurrentSmartTransactionsEnabled, - getFromTokenInputValue, - getFromTokenError, - getMaxSlippage, - getIsFeatureFlagLoaded, - getSmartTransactionFees, - getLatestAddedTokenTo, -} from '../../../ducks/swaps/swaps'; -import { - getSwapsDefaultToken, - getTokenExchangeRates, - getCurrentCurrency, - getCurrentChainId, - getRpcPrefsForCurrentProvider, - getTokenList, - isHardwareWallet, - getHardwareWalletType, - getUseCurrencyRateCheck, -} from '../../../selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; - -import { getURLHostName } from '../../../helpers/utils/util'; -import { usePrevious } from '../../../hooks/usePrevious'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; -import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; - -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from '../../../../shared/modules/swaps.utils'; -import { - MetaMetricsEventCategory, - MetaMetricsEventLinkType, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; -import { - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, - TokenBucketPriority, - MAX_ALLOWED_SLIPPAGE, -} from '../../../../shared/constants/swaps'; - -import { - resetSwapsPostFetchState, - ignoreTokens, - setBackgroundSwapRouteState, - clearSwapsQuotes, - stopPollingForQuotes, - clearSmartTransactionFees, -} from '../../../store/actions'; -import { countDecimals, fetchTokenPrice } from '../swaps.util'; -import SwapsFooter from '../swaps-footer'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; -import { fetchTokenBalance } from '../../../../shared/lib/token-util'; -import { shouldEnableDirectWrapping } from '../../../../shared/lib/swaps-utils'; -import { - getValueFromWeiHex, - hexToDecimal, -} from '../../../../shared/modules/conversion.utils'; - -const fuseSearchKeys = [ - { name: 'name', weight: 0.499 }, - { name: 'symbol', weight: 0.499 }, - { name: 'address', weight: 0.002 }, -]; - -let timeoutIdForQuotesPrefetching; - -export default function BuildQuote({ - ethBalance, - selectedAccountAddress, - shuffledTokensList, -}) { - const t = useContext(I18nContext); - const dispatch = useDispatch(); - const history = useHistory(); - const trackEvent = useContext(MetaMetricsContext); - - const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] = - useState(undefined); - const [verificationClicked, setVerificationClicked] = useState(false); - - const isFeatureFlagLoaded = useSelector(getIsFeatureFlagLoaded); - const balanceError = useSelector(getBalanceError); - const fetchParams = useSelector(getFetchParams, isEqual); - const { sourceTokenInfo = {}, destinationTokenInfo = {} } = - fetchParams?.metaData || {}; - const tokens = useSelector(getTokens, isEqual); - const topAssets = useSelector(getTopAssets, isEqual); - const fromToken = useSelector(getFromToken, isEqual); - const fromTokenInputValue = useSelector(getFromTokenInputValue); - const fromTokenError = useSelector(getFromTokenError); - const maxSlippage = useSelector(getMaxSlippage); - const toToken = useSelector(getToToken, isEqual) || destinationTokenInfo; - const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); - const tokenList = useSelector(getTokenList, isEqual); - const quotes = useSelector(getQuotes, isEqual); - const areQuotesPresent = Object.keys(quotes).length > 0; - const latestAddedTokenTo = useSelector(getLatestAddedTokenTo, isEqual); - - const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); - const conversionRate = useSelector(getConversionRate); - const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - const smartTransactionFees = useSelector(getSmartTransactionFees); - const currentCurrency = useSelector(getCurrentCurrency); - - const fetchParamsFromToken = isSwapsDefaultTokenSymbol( - sourceTokenInfo?.symbol, - chainId, - ) - ? defaultSwapsToken - : sourceTokenInfo; - - const { loading, tokensWithBalances } = useTokenTracker({ tokens }); - - // If the fromToken was set in a call to `onFromSelect` (see below), and that from token has a balance - // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that - // the balance of the token can appear in the from token selection dropdown - const fromTokenArray = - !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance - ? [fromToken] - : []; - const usersTokens = uniqBy( - [...tokensWithBalances, ...tokens, ...fromTokenArray], - 'address', - ); - const memoizedUsersTokens = useEqualityCheck(usersTokens); - - const selectedFromToken = getRenderableTokenData( - fromToken || fetchParamsFromToken, - tokenConversionRates, - conversionRate, - currentCurrency, - chainId, - tokenList, - ); - - const tokensToSearchSwapFrom = useTokensToSearch({ - usersTokens: memoizedUsersTokens, - topTokens: topAssets, - shuffledTokensList, - tokenBucketPriority: TokenBucketPriority.owned, - }); - const tokensToSearchSwapTo = useTokensToSearch({ - usersTokens: memoizedUsersTokens, - topTokens: topAssets, - shuffledTokensList, - tokenBucketPriority: TokenBucketPriority.top, - }); - const selectedToToken = - tokensToSearchSwapFrom.find(({ address }) => - isEqualCaseInsensitive(address, toToken?.address), - ) || toToken; - const toTokenIsNotDefault = - selectedToToken?.address && - !isSwapsDefaultTokenAddress(selectedToToken?.address, chainId); - const occurrences = Number( - selectedToToken?.occurances || selectedToToken?.occurrences || 0, - ); - const { - address: fromTokenAddress, - symbol: fromTokenSymbol, - string: fromTokenString, - decimals: fromTokenDecimals, - balance: rawFromTokenBalance, - } = selectedFromToken || {}; - const { address: toTokenAddress } = selectedToToken || {}; - - const fromTokenBalance = - rawFromTokenBalance && - calcTokenAmount(rawFromTokenBalance, fromTokenDecimals).toString(10); - - const prevFromTokenBalance = usePrevious(fromTokenBalance); - - const swapFromTokenFiatValue = useTokenFiatAmount( - fromTokenAddress, - fromTokenInputValue || 0, - fromTokenSymbol, - { - showFiat: useCurrencyRateCheck, - }, - true, - ); - const swapFromEthFiatValue = useEthFiatAmount( - fromTokenInputValue || 0, - { showFiat: useCurrencyRateCheck }, - true, - ); - const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) - ? swapFromEthFiatValue - : swapFromTokenFiatValue; - - const onInputChange = useCallback( - (newInputValue, balance) => { - dispatch(setFromTokenInputValue(newInputValue)); - const newBalanceError = new BigNumber(newInputValue || 0).gt( - balance || 0, - ); - // "setBalanceError" is just a warning, a user can still click on the "Review swap" button. - if (balanceError !== newBalanceError) { - dispatch(setBalanceError(newBalanceError)); - } - dispatch( - setFromTokenError( - fromToken && countDecimals(newInputValue) > fromToken.decimals - ? 'tooManyDecimals' - : null, - ), - ); - }, - [dispatch, fromToken, balanceError], - ); - - const onFromSelect = (token) => { - if ( - token?.address && - !swapFromFiatValue && - fetchedTokenExchangeRate !== null - ) { - fetchTokenPrice(token.address).then((rate) => { - if (rate !== null && rate !== undefined) { - setFetchedTokenExchangeRate(rate); - } - }); - } else { - setFetchedTokenExchangeRate(null); - } - if ( - token?.address && - !memoizedUsersTokens.find((usersToken) => - isEqualCaseInsensitive(usersToken.address, token.address), - ) - ) { - fetchTokenBalance( - token.address, - selectedAccountAddress, - global.ethereumProvider, - ).then((fetchedBalance) => { - if (fetchedBalance?.balance) { - const balanceAsDecString = fetchedBalance.balance.toString(10); - const userTokenBalance = calcTokenAmount( - balanceAsDecString, - token.decimals, - ); - dispatch( - setSwapsFromToken({ - ...token, - string: userTokenBalance.toString(10), - balance: balanceAsDecString, - }), - ); - } - }); - } - dispatch(setSwapsFromToken(token)); - onInputChange( - token?.address ? fromTokenInputValue : '', - token.string, - token.decimals, - ); - }; - - const blockExplorerTokenLink = getTokenTrackerLink( - selectedToToken.address, - chainId, - null, // no networkId - null, // no holderAddress - { - blockExplorerUrl: - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, - }, - ); - - const blockExplorerLabel = rpcPrefs.blockExplorerUrl - ? getURLHostName(blockExplorerTokenLink) - : t('etherscan'); - - const { address: toAddress } = toToken || {}; - const onToSelect = useCallback( - (token) => { - if (latestAddedTokenTo && token.address !== toAddress) { - dispatch( - ignoreTokens({ - tokensToIgnore: toAddress, - dontShowLoadingIndicator: true, - }), - ); - } - dispatch(setSwapToToken(token)); - setVerificationClicked(false); - }, - [dispatch, latestAddedTokenTo, toAddress], - ); - - const hideDropdownItemIf = useCallback( - (item) => isEqualCaseInsensitive(item.address, fromTokenAddress), - [fromTokenAddress], - ); - - const tokensWithBalancesFromToken = tokensWithBalances.find((token) => - isEqualCaseInsensitive(token.address, fromToken?.address), - ); - const previousTokensWithBalancesFromToken = usePrevious( - tokensWithBalancesFromToken, - ); - - useEffect(() => { - const notDefault = !isSwapsDefaultTokenAddress( - tokensWithBalancesFromToken?.address, - chainId, - ); - const addressesAreTheSame = isEqualCaseInsensitive( - tokensWithBalancesFromToken?.address, - previousTokensWithBalancesFromToken?.address, - ); - const balanceHasChanged = - tokensWithBalancesFromToken?.balance !== - previousTokensWithBalancesFromToken?.balance; - if (notDefault && addressesAreTheSame && balanceHasChanged) { - dispatch( - setSwapsFromToken({ - ...fromToken, - balance: tokensWithBalancesFromToken?.balance, - string: tokensWithBalancesFromToken?.string, - }), - ); - } - }, [ - dispatch, - tokensWithBalancesFromToken, - previousTokensWithBalancesFromToken, - fromToken, - chainId, - ]); - - // If the eth balance changes while on build quote, we update the selected from token - useEffect(() => { - if ( - isSwapsDefaultTokenAddress(fromToken?.address, chainId) && - fromToken?.balance !== hexToDecimal(ethBalance) - ) { - dispatch( - setSwapsFromToken({ - ...fromToken, - balance: hexToDecimal(ethBalance), - string: getValueFromWeiHex({ - value: ethBalance, - numberOfDecimals: 4, - toDenomination: 'ETH', - }), - }), - ); - } - }, [dispatch, fromToken, ethBalance, chainId]); - - useEffect(() => { - if (prevFromTokenBalance !== fromTokenBalance) { - onInputChange(fromTokenInputValue, fromTokenBalance); - } - }, [ - onInputChange, - prevFromTokenBalance, - fromTokenInputValue, - fromTokenBalance, - ]); - - const trackBuildQuotePageLoadedEvent = useCallback(() => { - trackEvent({ - event: 'Build Quote Page Loaded', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }, - }); - }, [ - trackEvent, - hardwareWalletUsed, - hardwareWalletType, - smartTransactionsEnabled, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - ]); - - useEffect(() => { - dispatch(resetSwapsPostFetchState()); - dispatch(setReviewSwapClickedTimestamp()); - trackBuildQuotePageLoadedEvent(); - }, [dispatch, trackBuildQuotePageLoadedEvent]); - - useEffect(() => { - if (smartTransactionsEnabled && smartTransactionFees?.tradeTxFees) { - // We want to clear STX fees, because we only want to use fresh ones on the View Quote page. - clearSmartTransactionFees(); - } - }, [smartTransactionsEnabled, smartTransactionFees]); - - const BlockExplorerLink = () => { - return ( - <a - className="build-quote__token-etherscan-link build-quote__underline" - key="build-quote-etherscan-link" - onClick={() => { - /* istanbul ignore next */ - trackEvent({ - event: MetaMetricsEventName.ExternalLinkClicked, - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: MetaMetricsEventLinkType.TokenTracker, - location: 'Swaps Confirmation', - url_domain: getURLHostName(blockExplorerTokenLink), - }, - }); - global.platform.openTab({ - url: blockExplorerTokenLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerLabel} - </a> - ); - }; - - let tokenVerificationDescription = ''; - if (blockExplorerTokenLink) { - if (occurrences === 1) { - tokenVerificationDescription = t('verifyThisTokenOn', [ - <BlockExplorerLink key="block-explorer-link" />, - ]); - } else if (occurrences === 0) { - tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [ - <BlockExplorerLink key="block-explorer-link" />, - ]); - } - } - - const swapYourTokenBalance = t('swapYourTokenBalance', [ - fromTokenString || '0', - fromTokenSymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol || '', - ]); - - const isDirectWrappingEnabled = shouldEnableDirectWrapping( - chainId, - fromTokenAddress, - selectedToToken.address, - ); - const isReviewSwapButtonDisabled = - fromTokenError || - !isFeatureFlagLoaded || - !Number(fromTokenInputValue) || - !selectedToToken?.address || - !fromTokenAddress || - Number(maxSlippage) < 0 || - Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE || - (toTokenIsNotDefault && occurrences < 2 && !verificationClicked); - - // It's triggered every time there is a change in form values (token from, token to, amount and slippage). - useEffect(() => { - dispatch(clearSwapsQuotes()); - dispatch(stopPollingForQuotes()); - const prefetchQuotesWithoutRedirecting = async () => { - const pageRedirectionDisabled = true; - await dispatch( - fetchQuotesAndSetQuoteState( - history, - fromTokenInputValue, - maxSlippage, - trackEvent, - pageRedirectionDisabled, - ), - ); - }; - // Delay fetching quotes until a user is done typing an input value. If they type a new char in less than a second, - // we will cancel previous setTimeout call and start running a new one. - timeoutIdForQuotesPrefetching = setTimeout(() => { - timeoutIdForQuotesPrefetching = null; - if (!isReviewSwapButtonDisabled) { - // Only do quotes prefetching if the Review swap button is enabled. - prefetchQuotesWithoutRedirecting(); - } - }, 1000); - return () => clearTimeout(timeoutIdForQuotesPrefetching); - }, [ - dispatch, - history, - maxSlippage, - trackEvent, - isReviewSwapButtonDisabled, - fromTokenInputValue, - fromTokenAddress, - toTokenAddress, - smartTransactionsOptInStatus, - ]); - - return ( - <div className="build-quote"> - <div className="build-quote__content"> - <div className="build-quote__dropdown-input-pair-header"> - <div className="build-quote__input-label">{t('swapSwapFrom')}</div> - {!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && ( - <div - className="build-quote__max-button" - data-testid="build-quote__max-button" - onClick={() => - onInputChange(fromTokenBalance || '0', fromTokenBalance) - } - > - {t('max')} - </div> - )} - </div> - <DropdownInputPair - onSelect={onFromSelect} - itemsToSearch={tokensToSearchSwapFrom} - onInputChange={(value) => { - /* istanbul ignore next */ - onInputChange(value, fromTokenBalance); - }} - inputValue={fromTokenInputValue} - leftValue={fromTokenInputValue && swapFromFiatValue} - selectedItem={selectedFromToken} - maxListItems={30} - loading={ - loading && - (!tokensToSearchSwapFrom?.length || - !topAssets || - !Object.keys(topAssets).length) - } - selectPlaceHolderText={t('swapSelect')} - hideItemIf={(item) => - isEqualCaseInsensitive(item.address, selectedToToken?.address) - } - listContainerClassName="build-quote__open-dropdown" - autoFocus - /> - <div - className={classnames('build-quote__balance-message', { - 'build-quote__balance-message--error': - balanceError || fromTokenError, - })} - > - {!fromTokenError && - !balanceError && - fromTokenSymbol && - swapYourTokenBalance} - {!fromTokenError && balanceError && fromTokenSymbol && ( - <div className="build-quite__insufficient-funds"> - <div className="build-quite__insufficient-funds-first"> - {t('swapsNotEnoughForTx', [fromTokenSymbol])} - </div> - <div className="build-quite__insufficient-funds-second"> - {swapYourTokenBalance} - </div> - </div> - )} - {fromTokenError && ( - <> - <div className="build-quote__form-error"> - {t('swapTooManyDecimalsError', [ - fromTokenSymbol, - fromTokenDecimals, - ])} - </div> - <div>{swapYourTokenBalance}</div> - </> - )} - </div> - <div className="build-quote__swap-arrows-row"> - <button - className="build-quote__swap-arrows" - data-testid="build-quote__swap-arrows" - onClick={() => { - onToSelect(selectedFromToken); - onFromSelect(selectedToToken); - }} - > - <i className="fa fa-arrow-up" title={t('swapSwapSwitch')} /> - <i className="fa fa-arrow-down" title={t('swapSwapSwitch')} /> - </button> - </div> - <div className="build-quote__dropdown-swap-to-header"> - <div className="build-quote__input-label">{t('swapSwapTo')}</div> - </div> - <div className="dropdown-input-pair dropdown-input-pair__to"> - <DropdownSearchList - startingItem={selectedToToken} - itemsToSearch={tokensToSearchSwapTo} - fuseSearchKeys={fuseSearchKeys} - selectPlaceHolderText={t('swapSelectAToken')} - maxListItems={30} - onSelect={onToSelect} - loading={ - loading && - (!tokensToSearchSwapTo?.length || - !topAssets || - !Object.keys(topAssets).length) - } - externallySelectedItem={selectedToToken} - hideItemIf={hideDropdownItemIf} - listContainerClassName="build-quote__open-to-dropdown" - hideRightLabels - defaultToAll - shouldSearchForImports - /> - </div> - {toTokenIsNotDefault && - (occurrences < 2 ? ( - <ActionableMessage - type={occurrences === 1 ? 'warning' : 'danger'} - message={ - <div className="build-quote__token-verification-warning-message"> - <div className="build-quote__bold"> - {occurrences === 1 - ? t('swapTokenVerificationOnlyOneSource') - : t('swapTokenVerificationAddedManually')} - </div> - <div>{tokenVerificationDescription}</div> - </div> - } - primaryAction={ - /* istanbul ignore next */ - verificationClicked - ? null - : { - label: t('continue'), - onClick: () => setVerificationClicked(true), - } - } - withRightButton - infoTooltipText={ - blockExplorerTokenLink && - t('swapVerifyTokenExplanation', [blockExplorerLabel]) - } - /> - ) : ( - <div className="build-quote__token-message"> - <span - className="build-quote__bold" - key="token-verification-bold-text" - > - {t('swapTokenVerificationSources', [occurrences])} - </span> - {blockExplorerTokenLink && ( - <> - {t('swapTokenVerificationMessage', [ - <a - className="build-quote__token-etherscan-link" - key="build-quote-etherscan-link" - onClick={() => { - /* istanbul ignore next */ - trackEvent({ - event: 'Clicked Block Explorer Link', - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: 'Token Tracker', - action: 'Swaps Confirmation', - block_explorer_domain: getURLHostName( - blockExplorerTokenLink, - ), - }, - }); - global.platform.openTab({ - url: blockExplorerTokenLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerLabel} - </a>, - ])} - <InfoTooltip - position="top" - contentText={t('swapVerifyTokenExplanation', [ - blockExplorerLabel, - ])} - containerClassName="build-quote__token-tooltip-container" - key="token-verification-info-tooltip" - /> - </> - )} - </div> - ))} - {!isDirectWrappingEnabled && ( - <div className="build-quote__slippage-buttons-container"> - <SlippageButtons - onSelect={(newSlippage) => { - dispatch(setMaxSlippage(newSlippage)); - }} - maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE} - currentSlippage={maxSlippage} - isDirectWrappingEnabled={isDirectWrappingEnabled} - /> - </div> - )} - </div> - <SwapsFooter - onSubmit={ - /* istanbul ignore next */ - async () => { - // We need this to know how long it took to go from clicking on the Review swap button to rendered View Quote page. - dispatch(setReviewSwapClickedTimestamp(Date.now())); - // In case that quotes prefetching is waiting to be executed, but hasn't started yet, - // we want to cancel it and fetch quotes from here. - if (timeoutIdForQuotesPrefetching) { - clearTimeout(timeoutIdForQuotesPrefetching); - dispatch( - fetchQuotesAndSetQuoteState( - history, - fromTokenInputValue, - maxSlippage, - trackEvent, - ), - ); - } else if (areQuotesPresent) { - // If there are prefetched quotes already, go directly to the View Quote page. - history.push(VIEW_QUOTE_ROUTE); - } else { - // If the "Review swap" button was clicked while quotes are being fetched, go to the Loading Quotes page. - await dispatch(setBackgroundSwapRouteState('loading')); - history.push(LOADING_QUOTES_ROUTE); - } - } - } - submitText={t('swapReviewSwap')} - disabled={isReviewSwapButtonDisabled} - hideCancel - showTermsOfService - /> - </div> - ); -} - -BuildQuote.propTypes = { - ethBalance: PropTypes.string, - selectedAccountAddress: PropTypes.string, - shuffledTokensList: PropTypes.array, -}; diff --git a/ui/pages/swaps/build-quote/build-quote.stories.js b/ui/pages/swaps/build-quote/build-quote.stories.js deleted file mode 100644 index 008b4b4a3ed4..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.stories.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { shuffle } from 'lodash'; -import testData from '../../../../.storybook/test-data'; -import BuildQuote from './build-quote'; - -const tokenValuesArr = shuffle(testData.metamask.tokenList); - -export default { - title: 'Pages/Swaps/BuildQuote', - - argTypes: { - ethBalance: { - control: { type: 'text' }, - }, - selectedAccountAddress: { - control: { type: 'text' }, - }, - shuffledTokensList: { control: 'object' }, - }, - args: { - ethBalance: '0x8', - selectedAccountAddress: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - shuffledTokensList: tokenValuesArr, - }, -}; - -export const DefaultStory = (args) => { - return ( - <> - <BuildQuote {...args} /> - </> - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/build-quote/build-quote.test.js b/ui/pages/swaps/build-quote/build-quote.test.js deleted file mode 100644 index aa8738e43ecb..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.test.js +++ /dev/null @@ -1,223 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import { createTestProviderTools } from '../../../../test/stub/provider'; -import { - setSwapsFromToken, - setSwapToToken, - setFromTokenInputValue, -} from '../../../ducks/swaps/swaps'; -import { mockNetworkState } from '../../../../test/stub/networks'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import BuildQuote from '.'; - -const middleware = [thunk]; -const createProps = (customProps = {}) => { - return { - ethBalance: '0x8', - selectedAccountAddress: 'selectedAccountAddress', - isFeatureFlagLoaded: false, - shuffledTokensList: [], - ...customProps, - }; -}; - -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - ignoreTokens: jest.fn(), - setBackgroundSwapRouteState: jest.fn(), - clearSwapsQuotes: jest.fn(), - stopPollingForQuotes: jest.fn(), - clearSmartTransactionFees: jest.fn(), - setSwapsFromToken: jest.fn(), - setSwapToToken: jest.fn(), - setFromTokenInputValue: jest.fn(), -}); - -jest.mock('../../../ducks/swaps/swaps', () => { - const actual = jest.requireActual('../../../ducks/swaps/swaps'); - return { - ...actual, - setSwapsFromToken: jest.fn(), - setSwapToToken: jest.fn(), - setFromTokenInputValue: jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }), - }; -}); - -jest.mock('../swaps.util', () => { - const actual = jest.requireActual('../swaps.util'); - return { - ...actual, - fetchTokenBalance: jest.fn(() => Promise.resolve()), - fetchTokenPrice: jest.fn(() => Promise.resolve()), - }; -}); - -const providerResultStub = { - eth_getCode: '0x123', - eth_call: - '0x00000000000000000000000000000000000000000000000029a2241af62c0000', -}; -const { provider } = createTestProviderTools({ - scaffold: providerResultStub, - networkId: '5', - chainId: '5', -}); - -describe('BuildQuote', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - - beforeEach(() => { - global.ethereumProvider = provider; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect(getByText('Swap to')).toBeInTheDocument(); - expect(getByText('Select')).toBeInTheDocument(); - expect(getByText('Slippage tolerance')).toBeInTheDocument(); - expect(getByText('2%')).toBeInTheDocument(); - expect(getByText('3%')).toBeInTheDocument(); - expect(getByText('Review swap')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); - }); - - it('switches swap from and to tokens', () => { - const setSwapFromTokenMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setSwapsFromToken.mockImplementation(setSwapFromTokenMock); - const setSwapToTokenMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setSwapToToken.mockImplementation(setSwapToTokenMock); - const mockStore = createSwapsMockStore(); - const store = configureMockStore(middleware)(mockStore); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - <BuildQuote {...props} />, - store, - ); - expect(getByText('Swap from')).toBeInTheDocument(); - fireEvent.click(getByTestId('build-quote__swap-arrows')); - expect(setSwapsFromToken).toHaveBeenCalledWith(mockStore.swaps.toToken); - expect(setSwapToToken).toHaveBeenCalled(); - }); - - it('renders the block explorer link, only 1 verified source', () => { - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 1; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect(getByText('Only verified on 1 source.')).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); - }); - - it('renders the block explorer link, 0 verified sources', () => { - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 0; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect( - getByText('This token has been added manually.'), - ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); - }); - - it('clicks on a block explorer link', () => { - global.platform = { openTab: jest.fn() }; - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 1; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - const blockExplorer = getByText('etherscan.io'); - expect(blockExplorer).toBeInTheDocument(); - fireEvent.click(blockExplorer); - expect(global.platform.openTab).toHaveBeenCalledWith({ - url: 'https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - }); - }); - - it('clicks on the "max" link', () => { - const setFromTokenInputValueMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setFromTokenInputValue.mockImplementation(setFromTokenInputValueMock); - const mockStore = createSwapsMockStore(); - mockStore.swaps.fromToken = 'DAI'; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - const maxLink = getByText('Max'); - fireEvent.click(maxLink); - expect(setFromTokenInputValue).toHaveBeenCalled(); - }); -}); diff --git a/ui/pages/swaps/build-quote/index.js b/ui/pages/swaps/build-quote/index.js deleted file mode 100644 index 772229f2a187..000000000000 --- a/ui/pages/swaps/build-quote/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './build-quote'; diff --git a/ui/pages/swaps/build-quote/index.scss b/ui/pages/swaps/build-quote/index.scss deleted file mode 100644 index 5d454002a0b8..000000000000 --- a/ui/pages/swaps/build-quote/index.scss +++ /dev/null @@ -1,223 +0,0 @@ -@use "design-system"; - -.build-quote { - display: flex; - flex-flow: column; - align-items: center; - flex: 1; - width: 100%; - padding-top: 4px; - - &__content { - display: flex; - height: 100%; - flex-direction: column; - padding-left: 24px; - padding-right: 24px; - } - - &__content { - display: flex; - } - - &__dropdown-swap-to-header { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-top: 0; - margin-bottom: 12px; - } - - &__dropdown-input-pair-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - width: 100%; - margin-bottom: 12px; - flex: 0.5 1 auto; - max-height: 56px; - } - - &__title, - &__input-label { - @include design-system.H5; - - font-weight: bold; - color: var(--color-text-default); - margin-top: 3px; - } - - &__swap-arrows-row { - width: 100%; - display: flex; - justify-content: flex-end; - padding-right: 16px; - padding-top: 12px; - height: 24px; - position: relative; - } - - &__swap-arrows { - display: flex; - flex: 0 0 auto; - height: 24px; - cursor: pointer; - background: unset; - color: var(--color-icon-muted); - } - - &__max-button { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - } - - &__balance-message { - @include design-system.H7; - - width: 100%; - color: var(--color-text-muted); - margin-top: 4px; - display: flex; - flex-flow: column; - height: 18px; - - &--error { - div:first-of-type { - font-weight: bold; - color: var(--color-text-default); - } - - .build-quote__form-error:first-of-type { - font-weight: bold; - color: var(--color-error-default); - } - - div:last-of-type { - font-weight: normal; - color: var(--color-text-alternative); - } - } - } - - &__slippage-buttons-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 32px; - } - - &__open-dropdown, - &__open-to-dropdown { - max-height: 330px; - box-shadow: var(--shadow-size-sm) var(--color-shadow-default); - position: absolute; - width: 100%; - } - - .dropdown-input-pair { - .searchable-item-list { - &__item--add-token { - display: none; - } - } - - &__to { - .searchable-item-list { - &__item--add-token { - display: flex; - } - } - } - - &__input { - div { - border: 1px solid var(--color-border-default); - border-left: 0; - } - } - } - - &__open-to-dropdown { - max-height: 194px; - - @include design-system.screen-sm-min { - max-height: 276px; - } - } - - &__token-message { - @include design-system.H7; - - width: 100%; - color: var(--color-text-alternative); - margin-top: 4px; - - .info-tooltip { - display: inline-block; - } - } - - &__token-etherscan-link { - color: var(--color-primary-default); - cursor: pointer; - } - - &__token-tooltip-container { - // Needed to override the style property added by the react-tippy library - display: flex !important; - } - - &__bold { - font-weight: bold; - } - - &__underline { - text-decoration: underline; - } - - /* Prevents the swaps "Swap to" field from overflowing */ - .dropdown-input-pair__to .dropdown-search-list { - width: 100%; - } -} - -@keyframes slide-in { - 100% { transform: translateY(0%); } -} - -.smart-transactions-popover { - transform: translateY(-100%); - animation: slide-in 0.5s forwards; - - &__content { - flex-direction: column; - - ul { - list-style: inside; - } - - a { - color: var(--color-primary-default); - cursor: pointer; - } - } - - &__footer { - flex-direction: column; - flex: 1; - align-items: center; - border-top: 0; - - button { - border-radius: 50px; - } - - a { - font-size: inherit; - padding-bottom: 0; - } - } -} diff --git a/ui/pages/swaps/create-new-swap/create-new-swap.js b/ui/pages/swaps/create-new-swap/create-new-swap.js index 3f19b68631b2..6d7963e36ca8 100644 --- a/ui/pages/swaps/create-new-swap/create-new-swap.js +++ b/ui/pages/swaps/create-new-swap/create-new-swap.js @@ -9,7 +9,7 @@ import { I18nContext } from '../../../contexts/i18n'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { - navigateBackToBuildQuote, + navigateBackToPrepareSwap, setSwapsFromToken, } from '../../../ducks/swaps/swaps'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; @@ -32,7 +32,7 @@ export default function CreateNewSwap({ sensitiveTrackingProperties }) { sensitiveProperties: sensitiveTrackingProperties, }); history.push(DEFAULT_ROUTE); // It cleans up Swaps state. - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); dispatch(setSwapsFromToken(defaultSwapsToken)); }} > diff --git a/ui/pages/swaps/create-new-swap/create-new-swap.test.js b/ui/pages/swaps/create-new-swap/create-new-swap.test.js index 0ce6fa400150..86accf04da23 100644 --- a/ui/pages/swaps/create-new-swap/create-new-swap.test.js +++ b/ui/pages/swaps/create-new-swap/create-new-swap.test.js @@ -10,7 +10,7 @@ import { } from '../../../../test/jest'; import { setSwapsFromToken, - navigateBackToBuildQuote, + navigateBackToPrepareSwap, } from '../../../ducks/swaps/swaps'; import CreateNewSwap from '.'; @@ -23,7 +23,7 @@ const createProps = (customProps = {}) => { }; const backgroundConnection = { - navigateBackToBuildQuote: jest.fn(), + navigateBackToPrepareSwap: jest.fn(), setBackgroundSwapRouteState: jest.fn(), navigatedBackToBuildQuote: jest.fn(), }; @@ -35,7 +35,7 @@ jest.mock('../../../ducks/swaps/swaps', () => { return { ...actual, setSwapsFromToken: jest.fn(), - navigateBackToBuildQuote: jest.fn(), + navigateBackToPrepareSwap: jest.fn(), }; }); @@ -63,12 +63,12 @@ describe('CreateNewSwap', () => { }; }); setSwapsFromToken.mockImplementation(setSwapFromTokenMock); - const navigateBackToBuildQuoteMock = jest.fn(() => { + const navigateBackToPrepareSwapMock = jest.fn(() => { return { type: 'MOCK_ACTION', }; }); - navigateBackToBuildQuote.mockImplementation(navigateBackToBuildQuoteMock); + navigateBackToPrepareSwap.mockImplementation(navigateBackToPrepareSwapMock); const store = configureMockStore(middleware)(createSwapsMockStore()); const { getByText } = renderWithProvider( @@ -77,6 +77,6 @@ describe('CreateNewSwap', () => { ); await fireEvent.click(getByText('Create a new swap')); expect(setSwapFromTokenMock).toHaveBeenCalledTimes(1); - expect(navigateBackToBuildQuoteMock).toHaveBeenCalledTimes(1); + expect(navigateBackToPrepareSwapMock).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/swaps/dropdown-input-pair/README.mdx b/ui/pages/swaps/dropdown-input-pair/README.mdx deleted file mode 100644 index cac84e714daa..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/README.mdx +++ /dev/null @@ -1,15 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; - -import DropdownInputPair from '.'; - -# Dropdown Input Pair - -Dropdown to choose cryptocurrency with amount input field. - -<Canvas> - <Story id="pages-swaps-dropdowninputpair--default-story" /> -</Canvas> - -## Props - -<ArgsTable of={DropdownInputPair} /> diff --git a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap deleted file mode 100644 index d58907ed2684..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DropdownInputPair renders the component with initial props 1`] = ` -<div - class="MuiFormControl-root MuiTextField-root dropdown-input-pair__input MuiFormControl-marginDense MuiFormControl-fullWidth" -> - <div - class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-marginDense MuiInput-marginDense" - > - <input - aria-invalid="false" - class="MuiInputBase-input MuiInput-input MuiInputBase-inputMarginDense MuiInput-inputMarginDense" - dir="auto" - placeholder="0" - type="text" - value="" - /> - </div> -</div> -`; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js deleted file mode 100644 index 9da47ddf80ea..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js +++ /dev/null @@ -1,177 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import DropdownSearchList from '../dropdown-search-list'; -import TextField from '../../../components/ui/text-field'; - -const characterWidthMap = { - 1: 5.86, - 2: 10.05, - 3: 10.45, - 4: 11.1, - 5: 10, - 6: 10.06, - 7: 9.17, - 8: 10.28, - 9: 10.06, - 0: 11.22, - '.': 4.55, -}; - -const getInputWidth = (value) => { - const valueString = String(value); - const charArray = valueString.split(''); - return charArray.reduce( - (inputWidth, _char) => inputWidth + characterWidthMap[_char], - 12, - ); -}; -export default function DropdownInputPair({ - itemsToSearch = [], - onInputChange, - inputValue = '', - onSelect, - leftValue, - selectedItem, - SearchListPlaceholder, - maxListItems, - selectPlaceHolderText, - loading, - hideItemIf, - listContainerClassName, - autoFocus, -}) { - const [isOpen, setIsOpen] = useState(false); - const open = () => setIsOpen(true); - const close = () => setIsOpen(false); - const inputRef = useRef(); - const onTextFieldChange = (event) => { - event.stopPropagation(); - // Automatically prefix value with 0. if user begins typing . - const valueToUse = event.target.value === '.' ? '0.' : event.target.value; - - // Regex that validates strings with only numbers, 'x.', '.x', and 'x.x' - const regexp = /^(\.\d+|\d+(\.\d+)?|\d+\.)$/u; - // If the value is either empty or contains only numbers and '.' and only has one '.', update input to match - if (valueToUse === '' || regexp.test(valueToUse)) { - onInputChange(valueToUse); - } else { - // otherwise, use the previously set inputValue (effectively denying the user from inputting the last char) - // or an empty string if we do not yet have an inputValue - onInputChange(inputValue || ''); - } - }; - const [applyTwoLineStyle, setApplyTwoLineStyle] = useState(null); - useEffect(() => { - setApplyTwoLineStyle( - (inputRef?.current?.getBoundingClientRect()?.width || 0) + - getInputWidth(inputValue || '') > - 137, - ); - }, [inputValue, inputRef]); - - return ( - <div className="dropdown-input-pair"> - <DropdownSearchList - itemsToSearch={itemsToSearch} - SearchListPlaceholder={SearchListPlaceholder} - fuseSearchKeys={[ - { name: 'name', weight: 0.499 }, - { name: 'symbol', weight: 0.499 }, - { name: 'address', weight: 0.002 }, - ]} - maxListItems={maxListItems} - onOpen={open} - onClose={close} - onSelect={onSelect} - className={isOpen ? 'dropdown-input-pair__list--full-width' : ''} - externallySelectedItem={selectedItem} - selectPlaceHolderText={selectPlaceHolderText} - selectorClosedClassName="dropdown-input-pair__selector--closed" - listContainerClassName={listContainerClassName} - loading={loading} - hideItemIf={hideItemIf} - defaultToAll - /> - {!isOpen && ( - <TextField - className={classnames('dropdown-input-pair__input', { - 'dropdown-input-pair__two-line-input': applyTwoLineStyle, - })} - type="text" - placeholder="0" - onChange={onTextFieldChange} - fullWidth - margin="dense" - value={inputValue} - autoFocus={autoFocus} - /> - )} - {!isOpen && leftValue && ( - <div - className={classnames('dropdown-input-pair__left-value', { - 'dropdown-input-pair__left-value--two-lines': applyTwoLineStyle, - })} - ref={inputRef} - > - ≈ {leftValue} - </div> - )} - </div> - ); -} - -DropdownInputPair.propTypes = { - /** - * Give items data for the component - */ - itemsToSearch: PropTypes.array, - /** - * Handler for input change - */ - onInputChange: PropTypes.func, - /** - * Show input value content - */ - inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * Handler for onSelect - */ - onSelect: PropTypes.func, - /** - * Set value to left - */ - leftValue: PropTypes.string, - /** - * Show selected item - */ - selectedItem: PropTypes.object, - /** - * Doesn't look like this is used - */ - SearchListPlaceholder: PropTypes.func, - /** - * Define maximum item per list - */ - maxListItems: PropTypes.number, - /** - * Show select placeholder text - */ - selectPlaceHolderText: PropTypes.string, - /** - * Check if the component is loading - */ - loading: PropTypes.bool, - /** - * Handler for hide item - */ - hideItemIf: PropTypes.func, - /** - * Add custom CSS class for list container - */ - listContainerClassName: PropTypes.string, - /** - * Check if the component is auto focus - */ - autoFocus: PropTypes.bool, -}; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js deleted file mode 100644 index ff5a6c756c1b..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react'; -import { useArgs } from '@storybook/client-api'; - -import README from './README.mdx'; -import DropdownInputPair from '.'; - -const tokens = [ - { - primaryLabel: 'MetaMark (META)', - name: 'MetaMark', - iconUrl: '.storybook/images/metamark.svg', - erc20: true, - decimals: 18, - symbol: 'META', - address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', - }, - { - primaryLabel: '0x (ZRX)', - name: '0x', - iconUrl: '.storybook/images/0x.svg', - erc20: true, - symbol: 'ZRX', - decimals: 18, - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - }, - { - primaryLabel: 'AirSwap Token (AST)', - name: 'AirSwap Token', - iconUrl: '.storybook/images/AST.png', - erc20: true, - symbol: 'AST', - decimals: 4, - address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', - }, - { - primaryLabel: 'Basic Attention Token (BAT)', - name: 'Basic Attention Token', - iconUrl: '.storybook/images/BAT_icon.svg', - erc20: true, - symbol: 'BAT', - decimals: 18, - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - }, - { - primaryLabel: 'Civil Token (CVL)', - name: 'Civil Token', - iconUrl: '.storybook/images/CVL_token.svg', - erc20: true, - symbol: 'CVL', - decimals: 18, - address: '0x01FA555c97D7958Fa6f771f3BbD5CCD508f81e22', - }, - { - primaryLabel: 'Gladius (GLA)', - name: 'Gladius', - iconUrl: '.storybook/images/gladius.svg', - erc20: true, - symbol: 'GLA', - decimals: 8, - address: '0x71D01dB8d6a2fBEa7f8d434599C237980C234e4C', - }, - { - primaryLabel: 'Gnosis Token (GNO)', - name: 'Gnosis Token', - iconUrl: '.storybook/images/gnosis.svg', - erc20: true, - symbol: 'GNO', - decimals: 18, - address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', - }, - { - primaryLabel: 'OmiseGO (OMG)', - name: 'OmiseGO', - iconUrl: '.storybook/images/omg.jpg', - erc20: true, - symbol: 'OMG', - decimals: 18, - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - }, - { - primaryLabel: 'Sai Stablecoin v1.0 (SAI)', - name: 'Sai Stablecoin v1.0', - iconUrl: '.storybook/images/sai.svg', - erc20: true, - symbol: 'SAI', - decimals: 18, - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - }, - { - primaryLabel: 'Tether USD (USDT)', - name: 'Tether USD', - iconUrl: '.storybook/images/tether_usd.png', - erc20: true, - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - }, - { - primaryLabel: 'WednesdayCoin (WED)', - name: 'WednesdayCoin', - iconUrl: '.storybook/images/wed.png', - erc20: true, - symbol: 'WED', - decimals: 18, - address: '0x7848ae8F19671Dc05966dafBeFbBbb0308BDfAbD', - }, - { - primaryLabel: 'Wrapped BTC (WBTC)', - name: 'Wrapped BTC', - iconUrl: '.storybook/images/wbtc.png', - erc20: true, - symbol: 'WBTC', - decimals: 8, - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - }, -]; - -export default { - title: 'Pages/Swaps/DropdownInputPair', - - component: DropdownInputPair, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - itemsToSearch: { control: 'array' }, - onInputChange: { action: 'onInputChange' }, - inputValue: { control: 'text' }, - onSelect: { action: 'onSelect' }, - leftValue: { control: 'text' }, - selectedItem: { control: 'object' }, - maxListItems: { control: 'number' }, - selectPlaceHolderText: { control: 'text' }, - loading: { control: 'boolean' }, - listContainerClassName: { control: 'text' }, - autoFocus: { control: 'boolean' }, - }, -}; - -const tokensToSearch = tokens.map((token) => ({ - ...token, - primaryLabel: token.symbol, - secondaryLabel: token.name, - rightPrimaryLabel: `${(Math.random() * 100).toFixed( - Math.floor(Math.random() * 6), - )} ${token.symbol}`, - rightSecondaryLabel: `$${(Math.random() * 1000).toFixed(2)}`, -})); - -export const DefaultStory = (args) => { - const [{ inputValue, selectedItem = tokensToSearch[0] }, updateArgs] = - useArgs(); - return ( - <DropdownInputPair - {...args} - inputValue={inputValue} - onInputChange={(value) => { - updateArgs({ ...args, inputValue: value }); - }} - selectedItem={selectedItem} - /> - ); -}; - -DefaultStory.storyName = 'Default'; - -DefaultStory.args = { - itemsToSearch: tokensToSearch, - maxListItems: tokensToSearch.length, - loading: false, -}; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js deleted file mode 100644 index e9f319d25fcc..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import DropdownInputPair from '.'; - -const createProps = (customProps = {}) => { - return { - onInputChange: jest.fn(), - ...customProps, - }; -}; - -describe('DropdownInputPair', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByPlaceholderText } = renderWithProvider( - <DropdownInputPair {...props} />, - store, - ); - expect(getByPlaceholderText('0')).toBeInTheDocument(); - expect( - document.querySelector('.dropdown-input-pair__input'), - ).toMatchSnapshot(); - }); - - it('changes the input field', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByPlaceholderText } = renderWithProvider( - <DropdownInputPair {...props} />, - store, - ); - fireEvent.change(getByPlaceholderText('0'), { - target: { value: 1.1 }, - }); - expect(props.onInputChange).toHaveBeenCalledWith('1.1'); - }); -}); diff --git a/ui/pages/swaps/dropdown-input-pair/index.js b/ui/pages/swaps/dropdown-input-pair/index.js deleted file mode 100644 index d89fc83b8de2..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './dropdown-input-pair'; diff --git a/ui/pages/swaps/dropdown-input-pair/index.scss b/ui/pages/swaps/dropdown-input-pair/index.scss deleted file mode 100644 index 30d5440e0de6..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/index.scss +++ /dev/null @@ -1,78 +0,0 @@ -@use "design-system"; - -.dropdown-input-pair { - display: flex; - width: 312px; - height: 60px; - position: relative; - - &__input { - margin: 0 !important; - - input { - @include design-system.H4; - - padding-top: 6px; - } - - div { - border: 1px solid var(--color-border-default); - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; - height: 60px; - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ - } - - input[type=number] { - -moz-appearance: textfield; /* Firefox */ - } - } - - &__list { - &--full-width { - width: 100%; - } - } - - &__left-value { - @include design-system.H7; - - position: absolute; - right: 16px; - height: 100%; - display: flex; - align-items: center; - color: var(--color-text-alternative); - - &--two-lines { - right: inherit; - left: 157px; - align-items: unset; - top: 34px; - } - } - - .dropdown-input-pair__selector--closed { - height: 60px; - width: 142px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &__two-line-input { - div { - align-items: flex-start; - } - - input { - padding-top: 14px; - } - } -} diff --git a/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap b/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap deleted file mode 100644 index 6057b37ee370..000000000000 --- a/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DropdownSearchList renders the component with initial props 1`] = ` -<div> - <div - class="dropdown-search-list" - data-testid="dropdown-search-list" - tabindex="0" - > - <div - class="dropdown-search-list__selector-closed-container" - > - <div - class="dropdown-search-list__selector-closed" - > - <div - class="" - > - <img - alt="symbol" - class="url-icon dropdown-search-list__selector-closed-icon" - src="iconUrl" - /> - </div> - <div - class="dropdown-search-list__labels" - > - <div - class="dropdown-search-list__item-labels" - > - <span - class="dropdown-search-list__closed-primary-label" - > - symbol - </span> - </div> - </div> - </div> - <span - class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/arrow-down.svg');" - /> - </div> - </div> -</div> -`; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js deleted file mode 100644 index 1182ad12d72a..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js +++ /dev/null @@ -1,334 +0,0 @@ -import React, { - useState, - useCallback, - useEffect, - useContext, - useRef, -} from 'react'; -import { useSelector } from 'react-redux'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { isEqual } from 'lodash'; -import { I18nContext } from '../../../contexts/i18n'; -import SearchableItemList from '../searchable-item-list'; -import PulseLoader from '../../../components/ui/pulse-loader'; -import UrlIcon from '../../../components/ui/url-icon'; -import { - Icon, - IconName, - IconSize, -} from '../../../components/component-library'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import ImportToken from '../import-token'; -import { - isHardwareWallet, - getHardwareWalletType, - getCurrentChainId, - getRpcPrefsForCurrentProvider, -} from '../../../selectors/selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; -import { getURLHostName } from '../../../helpers/utils/util'; -import { getCurrentSmartTransactionsEnabled } from '../../../ducks/swaps/swaps'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; - -export default function DropdownSearchList({ - searchListClassName, - itemsToSearch, - selectPlaceHolderText, - fuseSearchKeys, - defaultToAll, - maxListItems, - onSelect, - startingItem, - onOpen, - onClose, - className = '', - externallySelectedItem, - selectorClosedClassName, - loading, - hideRightLabels, - hideItemIf, - listContainerClassName, - shouldSearchForImports, -}) { - const t = useContext(I18nContext); - const [isOpen, setIsOpen] = useState(false); - const [isImportTokenModalOpen, setIsImportTokenModalOpen] = useState(false); - const [selectedItem, setSelectedItem] = useState(startingItem); - const [tokenForImport, setTokenForImport] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - - const trackEvent = useContext(MetaMetricsContext); - - const close = useCallback(() => { - setIsOpen(false); - onClose?.(); - }, [onClose]); - - const onClickItem = useCallback( - (item) => { - onSelect?.(item); - setSelectedItem(item); - close(); - }, - [onSelect, close], - ); - - const onOpenImportTokenModalClick = (item) => { - setTokenForImport(item); - setIsImportTokenModalOpen(true); - }; - - /* istanbul ignore next */ - const onImportTokenClick = () => { - trackEvent({ - event: 'Token Imported', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - symbol: tokenForImport?.symbol, - address: tokenForImport?.address, - chain_id: chainId, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }, - }); - // Only when a user confirms import of a token, we add it and show it in a dropdown. - onSelect?.(tokenForImport); - setSelectedItem(tokenForImport); - setTokenForImport(null); - close(); - }; - - const onImportTokenCloseClick = () => { - setIsImportTokenModalOpen(false); - close(); - }; - - const onClickSelector = useCallback(() => { - if (!isOpen) { - setIsOpen(true); - onOpen?.(); - } - }, [isOpen, onOpen]); - - const prevExternallySelectedItemRef = useRef(); - useEffect(() => { - prevExternallySelectedItemRef.current = externallySelectedItem; - }); - const prevExternallySelectedItem = prevExternallySelectedItemRef.current; - - useEffect(() => { - if ( - externallySelectedItem && - !isEqual(externallySelectedItem, selectedItem) - ) { - setSelectedItem(externallySelectedItem); - } else if (prevExternallySelectedItem && !externallySelectedItem) { - setSelectedItem(null); - } - }, [externallySelectedItem, selectedItem, prevExternallySelectedItem]); - - const onKeyUp = (e) => { - if (e.key === 'Escape') { - close(); - } else if (e.key === 'Enter') { - onClickSelector(e); - } - }; - - const blockExplorerLink = - rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? - null; - - const blockExplorerHostName = getURLHostName(blockExplorerLink); - - const importTokenProps = { - onImportTokenCloseClick, - onImportTokenClick, - setIsImportTokenModalOpen, - tokenForImport, - }; - - return ( - <div - className={classnames('dropdown-search-list', className)} - data-testid="dropdown-search-list" - onClick={onClickSelector} - onKeyUp={onKeyUp} - tabIndex="0" - > - {tokenForImport && isImportTokenModalOpen && ( - <ImportToken isOpen {...importTokenProps} /> - )} - {!isOpen && ( - <div - className={classnames( - 'dropdown-search-list__selector-closed-container', - selectorClosedClassName, - )} - > - <div className="dropdown-search-list__selector-closed"> - {selectedItem?.iconUrl && ( - <UrlIcon - url={selectedItem.iconUrl} - className="dropdown-search-list__selector-closed-icon" - name={selectedItem?.symbol} - /> - )} - {!selectedItem?.iconUrl && ( - <div className="dropdown-search-list__default-dropdown-icon" /> - )} - <div className="dropdown-search-list__labels"> - <div className="dropdown-search-list__item-labels"> - <span - className={classnames( - 'dropdown-search-list__closed-primary-label', - { - 'dropdown-search-list__select-default': - !selectedItem?.symbol, - }, - )} - > - {selectedItem?.symbol || selectPlaceHolderText} - </span> - </div> - </div> - </div> - <Icon name={IconName.ArrowDown} size={IconSize.Xs} marginRight={3} /> - </div> - )} - {isOpen && ( - <> - <SearchableItemList - itemsToSearch={loading ? [] : itemsToSearch} - Placeholder={() => - /* istanbul ignore next */ - loading ? ( - <div className="dropdown-search-list__loading-item"> - <PulseLoader /> - <div className="dropdown-search-list__loading-item-text-container"> - <span className="dropdown-search-list__loading-item-text"> - {t('swapFetchingTokens')} - </span> - </div> - </div> - ) : ( - <div className="dropdown-search-list__placeholder"> - {t('swapBuildQuotePlaceHolderText', [searchQuery])} - {blockExplorerLink && ( - <div - tabIndex="0" - className="searchable-item-list__item searchable-item-list__item--add-token" - key="searchable-item-list-item-last" - > - <ActionableMessage - message={t('addTokenByContractAddress', [ - <a - key="dropdown-search-list__etherscan-link" - onClick={() => { - trackEvent({ - event: 'Clicked Block Explorer Link', - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: 'Token Tracker', - action: 'Verify Contract Address', - block_explorer_domain: blockExplorerHostName, - }, - }); - global.platform.openTab({ - url: blockExplorerLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerHostName} - </a>, - ])} - /> - </div> - )} - </div> - ) - } - searchPlaceholderText={t('swapSearchNameOrAddress')} - fuseSearchKeys={fuseSearchKeys} - defaultToAll={defaultToAll} - onClickItem={onClickItem} - onOpenImportTokenModalClick={onOpenImportTokenModalClick} - maxListItems={maxListItems} - className={classnames( - 'dropdown-search-list__token-container', - searchListClassName, - { - 'dropdown-search-list--open': isOpen, - }, - )} - hideRightLabels={hideRightLabels} - hideItemIf={hideItemIf} - listContainerClassName={listContainerClassName} - shouldSearchForImports={shouldSearchForImports} - searchQuery={searchQuery} - setSearchQuery={setSearchQuery} - /> - <div - className="dropdown-search-list__close-area" - data-testid="dropdown-search-list__close-area" - onClick={(event) => { - event.stopPropagation(); - setIsOpen(false); - onClose?.(); - }} - /> - </> - )} - </div> - ); -} - -DropdownSearchList.propTypes = { - itemsToSearch: PropTypes.array, - onSelect: PropTypes.func, - searchListClassName: PropTypes.string, - fuseSearchKeys: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - weight: PropTypes.number, - }), - ), - defaultToAll: PropTypes.bool, - maxListItems: PropTypes.number, - startingItem: PropTypes.object, - onOpen: PropTypes.func, - onClose: PropTypes.func, - className: PropTypes.string, - externallySelectedItem: PropTypes.object, - loading: PropTypes.bool, - selectPlaceHolderText: PropTypes.string, - selectorClosedClassName: PropTypes.string, - hideRightLabels: PropTypes.bool, - hideItemIf: PropTypes.func, - listContainerClassName: PropTypes.string, - shouldSearchForImports: PropTypes.bool, -}; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js deleted file mode 100644 index 73ec3ea7aec8..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import DropdownSearchList from '.'; - -const tokens = [ - { - primaryLabel: 'MetaMark (META)', - name: 'MetaMark', - iconUrl: 'metamark.svg', - erc20: true, - decimals: 18, - symbol: 'META', - address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', - }, - { - primaryLabel: '0x (ZRX)', - name: '0x', - iconUrl: '0x.svg', - erc20: true, - symbol: 'ZRX', - decimals: 18, - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - }, - { - primaryLabel: 'AirSwap Token (AST)', - name: 'AirSwap Token', - iconUrl: 'AST.png', - erc20: true, - symbol: 'AST', - decimals: 4, - address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', - }, - { - primaryLabel: 'Basic Attention Token (BAT)', - name: 'Basic Attention Token', - iconUrl: 'BAT_icon.svg', - erc20: true, - symbol: 'BAT', - decimals: 18, - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - }, - { - primaryLabel: 'Civil Token (CVL)', - name: 'Civil Token', - iconUrl: 'CVL_token.svg', - erc20: true, - symbol: 'CVL', - decimals: 18, - address: '0x01FA555c97D7958Fa6f771f3BbD5CCD508f81e22', - }, - { - primaryLabel: 'Gladius (GLA)', - name: 'Gladius', - iconUrl: 'gladius.svg', - erc20: true, - symbol: 'GLA', - decimals: 8, - address: '0x71D01dB8d6a2fBEa7f8d434599C237980C234e4C', - }, - { - primaryLabel: 'Gnosis Token (GNO)', - name: 'Gnosis Token', - iconUrl: 'gnosis.svg', - erc20: true, - symbol: 'GNO', - decimals: 18, - address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', - }, - { - primaryLabel: 'OmiseGO (OMG)', - name: 'OmiseGO', - iconUrl: 'omg.jpg', - erc20: true, - symbol: 'OMG', - decimals: 18, - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - }, - { - primaryLabel: 'Sai Stablecoin v1.0 (SAI)', - name: 'Sai Stablecoin v1.0', - iconUrl: 'sai.svg', - erc20: true, - symbol: 'SAI', - decimals: 18, - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - }, - { - primaryLabel: 'Tether USD (USDT)', - name: 'Tether USD', - iconUrl: 'tether_usd.png', - erc20: true, - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - }, - { - primaryLabel: 'WednesdayCoin (WED)', - name: 'WednesdayCoin', - iconUrl: 'wed.png', - erc20: true, - symbol: 'WED', - decimals: 18, - address: '0x7848ae8F19671Dc05966dafBeFbBbb0308BDfAbD', - }, - { - primaryLabel: 'Wrapped BTC (WBTC)', - name: 'Wrapped BTC', - iconUrl: 'wbtc.png', - erc20: true, - symbol: 'WBTC', - decimals: 8, - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - }, -]; - -export default { - title: 'Pages/Swaps/DropdownSearchList', -}; - -const tokensToSearch = tokens.map((token) => ({ - ...token, - primaryLabel: token.symbol, - secondaryLabel: token.name, - rightPrimaryLabel: `${(Math.random() * 100).toFixed( - Math.floor(Math.random() * 6), - )} ${token.symbol}`, - rightSecondaryLabel: `$${(Math.random() * 1000).toFixed(2)}`, -})); - -export const DefaultStory = () => { - return ( - <div style={{ height: '82vh', width: '357px' }}> - <DropdownSearchList - startingItem={tokensToSearch[0]} - itemsToSearch={tokensToSearch} - searchPlaceholderText="Search for a token" - fuseSearchKeys={[ - { name: 'name', weight: 0.5 }, - { name: 'symbol', weight: 0.5 }, - ]} - maxListItems={tokensToSearch.length} - defaultToAll - /> - </div> - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js deleted file mode 100644 index f0ae4a889169..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import DropdownSearchList from '.'; - -const createProps = (customProps = {}) => { - return { - startingItem: { - iconUrl: 'iconUrl', - symbol: 'symbol', - }, - ...customProps, - }; -}; - -jest.mock('../searchable-item-list', () => jest.fn(() => null)); - -describe('DropdownSearchList', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { container, getByText } = renderWithProvider( - <DropdownSearchList {...props} />, - store, - ); - expect(container).toMatchSnapshot(); - expect(getByText('symbol')).toBeInTheDocument(); - }); - - it('renders the component, opens the list and closes it', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByTestId } = renderWithProvider( - <DropdownSearchList {...props} />, - store, - ); - const dropdownSearchList = getByTestId('dropdown-search-list'); - expect(dropdownSearchList).toBeInTheDocument(); - fireEvent.click(dropdownSearchList); - const closeButton = getByTestId('dropdown-search-list__close-area'); - expect(closeButton).toBeInTheDocument(); - fireEvent.click(closeButton); - expect(closeButton).not.toBeInTheDocument(); - }); -}); diff --git a/ui/pages/swaps/dropdown-search-list/index.js b/ui/pages/swaps/dropdown-search-list/index.js deleted file mode 100644 index 3dd2e4ecf63e..000000000000 --- a/ui/pages/swaps/dropdown-search-list/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './dropdown-search-list'; diff --git a/ui/pages/swaps/dropdown-search-list/index.scss b/ui/pages/swaps/dropdown-search-list/index.scss deleted file mode 100644 index 904f9530d341..000000000000 --- a/ui/pages/swaps/dropdown-search-list/index.scss +++ /dev/null @@ -1,167 +0,0 @@ -@use "design-system"; - -.dropdown-search-list { - &__search-list-open { - margin: 24px; - box-shadow: none; - border-radius: 6px; - min-height: 297px; - width: 100%; - } - - &__token-container { - margin: 0; - min-height: auto; - border: 1px solid var(--color-border-default); - box-sizing: border-box; - box-shadow: none; - border-radius: 6px; - width: 100%; - } - - &--open { - box-shadow: var(--shadow-size-sm) var(--color-shadow-default); - border: 1px solid var(--color-border-default); - } - - &__close-area { - position: fixed; - top: 0; - left: 0; - z-index: 1000; - width: 100%; - height: 100%; - } - - &__selector-closed-container { - display: flex; - width: 100%; - position: relative; - align-items: center; - max-height: 60px; - transition: 200ms ease-in-out; - border-radius: 6px; - box-shadow: none; - border: 1px solid var(--color-border-default); - height: 60px; - - &:hover { - background: var(--color-background-default-hover); - } - } - - &__caret { - position: absolute; - right: 16px; - color: var(--color-icon-default); - } - - &__selector-closed { - display: flex; - flex-flow: row nowrap; - align-items: center; - padding: 16px 12px; - box-sizing: border-box; - cursor: pointer; - position: relative; - align-items: center; - flex: 1; - height: 60px; - - i { - font-size: 1.2em; - } - - .dropdown-search-list__item-labels { - width: 100%; - } - } - - &__selector-closed-icon { - width: 34px; - height: 34px; - } - - &__closed-primary-label { - @include design-system.H4; - - color: var(--color-text-default); - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__search-list--open { - box-shadow: var(--shadow-size-md) var(--color-shadow-default); - border: 1px solid var(--color-border-muted); - } - - &__default-dropdown-icon { - width: 34px; - height: 34px; - border-radius: 50%; - background: var(--color-background-alternative); - flex: 0 1 auto; - } - - &__labels { - display: flex; - justify-content: space-between; - width: 100%; - flex: 1; - } - - &__item-labels { - display: flex; - flex-direction: column; - justify-content: center; - margin-left: 8px; - } - - &__select-default { - color: var(--color-text-muted); - } - - &__placeholder { - @include design-system.H6; - - padding: 16px; - color: var(--color-text-alternative); - min-height: 300px; - position: relative; - z-index: 1002; - background: var(--color-background-default); - border-radius: 6px; - min-height: 194px; - overflow: hidden; - text-overflow: ellipsis; - - .searchable-item-list__item--add-token { - padding: 8px 0; - } - } - - &__loading-item { - transition: 200ms ease-in-out; - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: center; - padding: 16px 12px; - box-sizing: border-box; - cursor: pointer; - border-top: 1px solid var(--color-border-muted); - position: relative; - z-index: 1; - background: var(--color-background-default); - } - - &__loading-item-text-container { - margin-left: 4px; - } - - &__loading-item-text { - font-weight: bold; - } -} diff --git a/ui/pages/swaps/index.js b/ui/pages/swaps/index.js index 877e38aa7c84..cf079acc9623 100644 --- a/ui/pages/swaps/index.js +++ b/ui/pages/swaps/index.js @@ -14,7 +14,6 @@ import { Redirect, } from 'react-router-dom'; import { shuffle, isEqual } from 'lodash'; -import classnames from 'classnames'; import { TransactionStatus } from '@metamask/transaction-controller'; import { I18nContext } from '../../contexts/i18n'; @@ -40,11 +39,8 @@ import { prepareToLeaveSwaps, fetchSwapsLivenessAndFeatureFlags, getReviewSwapClickedTimestamp, - getPendingSmartTransactions, getCurrentSmartTransactionsEnabled, getCurrentSmartTransactionsError, - navigateBackToBuildQuote, - getSwapRedesignEnabled, setTransactionSettingsOpened, getLatestAddedTokenTo, } from '../../ducks/swaps/swaps'; @@ -57,8 +53,6 @@ import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, SMART_TRANSACTION_STATUS_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, DEFAULT_ROUTE, @@ -99,10 +93,8 @@ import AwaitingSignatures from './awaiting-signatures'; import SmartTransactionStatus from './smart-transaction-status'; import AwaitingSwap from './awaiting-swap'; import LoadingQuote from './loading-swaps-quotes'; -import BuildQuote from './build-quote'; import PrepareSwapPage from './prepare-swap-page/prepare-swap-page'; import NotificationPage from './notification-page/notification-page'; -import ViewQuote from './view-quote'; export default function Swap() { const t = useContext(I18nContext); @@ -117,7 +109,6 @@ export default function Swap() { const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE; const isSmartTransactionStatusRoute = pathname === SMART_TRANSACTION_STATUS_ROUTE; - const isViewQuoteRoute = pathname === VIEW_QUOTE_ROUTE; const isPrepareSwapRoute = pathname === PREPARE_SWAP_ROUTE; const [currentStxErrorTracked, setCurrentStxErrorTracked] = useState(false); @@ -140,7 +131,6 @@ export default function Swap() { const tokenList = useSelector(getTokenList, isEqual); const shuffledTokensList = shuffle(Object.values(tokenList)); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); - const pendingSmartTransactions = useSelector(getPendingSmartTransactions); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( getSmartTransactionsOptInStatus, @@ -149,7 +139,6 @@ export default function Swap() { const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); - const swapRedesignEnabled = useSelector(getSwapRedesignEnabled); const currentSmartTransactionsError = useSelector( getCurrentSmartTransactionsError, ); @@ -358,167 +347,72 @@ export default function Swap() { <div className="swaps"> <div className="swaps__container"> <div className="swaps__header"> - {!swapRedesignEnabled && ( - <div - className="swaps__header-edit" - onClick={async () => { - await dispatch(navigateBackToBuildQuote(history)); - }} - > - {isViewQuoteRoute && t('edit')} - </div> - )} - {swapRedesignEnabled && ( - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.center} - marginLeft={4} - width={FRACTIONS.ONE_TWELFTH} - tabIndex="0" - onKeyUp={(e) => { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - {!isAwaitingSwapRoute && - !isAwaitingSignaturesRoute && - !isSmartTransactionStatusRoute && ( - <Icon - name={IconName.Arrow2Left} - size={IconSize.Lg} - color={IconColor.iconAlternative} - onClick={redirectToDefaultRoute} - style={{ cursor: 'pointer' }} - title={t('cancel')} - /> - )} - </Box> - )} - <div className="swaps__title">{t('swap')}</div> - {!swapRedesignEnabled && ( - <div - className="swaps__header-cancel" - onClick={async () => { - clearTemporaryTokenRef.current(); - dispatch(clearSwapsState()); - await dispatch(resetBackgroundSwapsState()); - history.push(DEFAULT_ROUTE); - }} - > - {!isAwaitingSwapRoute && - !isAwaitingSignaturesRoute && - !isSmartTransactionStatusRoute && - t('cancel')} - </div> - )} - {swapRedesignEnabled && ( - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.center} - marginRight={4} - width={FRACTIONS.ONE_TWELFTH} - tabIndex="0" - onKeyUp={(e) => { - if (e.key === 'Enter') { - dispatch(setTransactionSettingsOpened(true)); - } - }} - > - {isPrepareSwapRoute && ( + <Box + display={DISPLAY.FLEX} + justifyContent={JustifyContent.center} + marginLeft={4} + width={FRACTIONS.ONE_TWELFTH} + tabIndex="0" + onKeyUp={(e) => { + if (e.key === 'Enter') { + redirectToDefaultRoute(); + } + }} + > + {!isAwaitingSwapRoute && + !isAwaitingSignaturesRoute && + !isSmartTransactionStatusRoute && ( <Icon - name={IconName.Setting} + name={IconName.Arrow2Left} size={IconSize.Lg} color={IconColor.iconAlternative} - onClick={() => { - dispatch(setTransactionSettingsOpened(true)); - }} + onClick={redirectToDefaultRoute} style={{ cursor: 'pointer' }} - title={t('transactionSettings')} + title={t('cancel')} /> )} - </Box> - )} + </Box> + <div className="swaps__title">{t('swap')}</div> + <Box + display={DISPLAY.FLEX} + justifyContent={JustifyContent.center} + marginRight={4} + width={FRACTIONS.ONE_TWELFTH} + tabIndex="0" + onKeyUp={(e) => { + if (e.key === 'Enter') { + dispatch(setTransactionSettingsOpened(true)); + } + }} + > + {isPrepareSwapRoute && ( + <Icon + name={IconName.Setting} + size={IconSize.Lg} + color={IconColor.iconAlternative} + onClick={() => { + dispatch(setTransactionSettingsOpened(true)); + }} + style={{ cursor: 'pointer' }} + title={t('transactionSettings')} + /> + )} + </Box> </div> - <div - className={classnames('swaps__content', { - 'swaps__content--redesign-enabled': swapRedesignEnabled, - })} - > + <div className="swaps__content"> <Switch> - <FeatureToggledRoute - redirectRoute={SWAPS_MAINTENANCE_ROUTE} - flag={swapsEnabled} - path={BUILD_QUOTE_ROUTE} - exact - render={() => { - if (swapRedesignEnabled) { - return <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} />; - } - if (tradeTxData && !conversionError) { - return <Redirect to={{ pathname: AWAITING_SWAP_ROUTE }} />; - } else if (tradeTxData && routeState) { - return <Redirect to={{ pathname: SWAPS_ERROR_ROUTE }} />; - } else if (routeState === 'loading' && aggregatorMetadata) { - return <Redirect to={{ pathname: LOADING_QUOTES_ROUTE }} />; - } - - return ( - <BuildQuote - ethBalance={ethBalance} - selectedAccountAddress={selectedAccountAddress} - shuffledTokensList={shuffledTokensList} - /> - ); - }} - /> <FeatureToggledRoute redirectRoute={SWAPS_MAINTENANCE_ROUTE} flag={swapsEnabled} path={PREPARE_SWAP_ROUTE} exact - render={() => { - if (!swapRedesignEnabled) { - return <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} />; - } - - return ( - <PrepareSwapPage - ethBalance={ethBalance} - selectedAccountAddress={selectedAccountAddress} - shuffledTokensList={shuffledTokensList} - /> - ); - }} - /> - <FeatureToggledRoute - redirectRoute={SWAPS_MAINTENANCE_ROUTE} - flag={swapsEnabled} - path={VIEW_QUOTE_ROUTE} - exact - render={() => { - if ( - pendingSmartTransactions.length > 0 && - routeState === 'smartTransactionStatus' - ) { - return ( - <Redirect - to={{ pathname: SMART_TRANSACTION_STATUS_ROUTE }} - /> - ); - } - if (swapRedesignEnabled) { - return <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} />; - } - if (Object.values(quotes).length) { - return ( - <ViewQuote numberOfQuotes={Object.values(quotes).length} /> - ); - } else if (fetchParams) { - return <Redirect to={{ pathname: SWAPS_ERROR_ROUTE }} />; - } - return <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} />; - }} + render={() => ( + <PrepareSwapPage + ethBalance={ethBalance} + selectedAccountAddress={selectedAccountAddress} + shuffledTokensList={shuffledTokensList} + /> + )} /> <Route path={SWAPS_ERROR_ROUTE} @@ -535,7 +429,7 @@ export default function Swap() { /> ); } - return <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} />; + return <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} />; }} /> <Route @@ -568,13 +462,13 @@ export default function Swap() { dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)); history.push(SWAPS_ERROR_ROUTE); } else { - history.push(VIEW_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } }} aggregatorMetadata={aggregatorMetadata} /> ) : ( - <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} /> + <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} /> ); }} /> @@ -585,7 +479,7 @@ export default function Swap() { return swapsEnabled === false ? ( <AwaitingSwap errorKey={OFFLINE_FOR_MAINTENANCE} /> ) : ( - <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} /> + <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} /> ); }} /> diff --git a/ui/pages/swaps/index.scss b/ui/pages/swaps/index.scss index 33fc1e9a17cb..4de2ac35707f 100644 --- a/ui/pages/swaps/index.scss +++ b/ui/pages/swaps/index.scss @@ -3,27 +3,21 @@ @import 'awaiting-swap/index'; @import 'awaiting-signatures/index'; @import 'smart-transaction-status/index'; -@import 'build-quote/index'; @import 'prepare-swap-page/index'; @import 'notification-page/index'; @import 'countdown-timer/index'; -@import 'dropdown-input-pair/index'; -@import 'dropdown-search-list/index'; @import 'exchange-rate-display/index'; @import 'fee-card/index'; @import 'loading-swaps-quotes/index'; -@import 'main-quote-summary/index'; @import 'searchable-item-list/index'; @import 'select-quote-popover/index'; -@import 'slippage-buttons/index'; @import 'swaps-footer/index'; -@import 'view-quote/index'; @import 'create-new-swap/index'; @import 'view-on-block-explorer/index'; @import 'transaction-settings/index'; @import 'list-with-search/index'; -@import 'popover-custom-background/index'; @import 'mascot-background-animation/index'; +@import 'selected-token/index'; .swaps { display: flex; @@ -78,13 +72,7 @@ } @include design-system.screen-sm-min { - width: 348px; - } - - &--redesign-enabled { - @include design-system.screen-sm-min { - width: 100%; - } + width: 100%; } } @@ -119,25 +107,6 @@ } } - &__header-cancel { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - padding-right: 24px; - flex: 1; - text-align: right; - } - - &__header-edit { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - padding-left: 24px; - flex: 1; - } - .actionable-message__message &__notification-close-button { background-color: transparent; position: absolute; diff --git a/ui/pages/swaps/index.test.js b/ui/pages/swaps/index.test.js index aa4109567f99..4157b614562f 100644 --- a/ui/pages/swaps/index.test.js +++ b/ui/pages/swaps/index.test.js @@ -22,7 +22,7 @@ jest.mock('react-router-dom', () => ({ }), useLocation: jest.fn(() => { return { - pathname: '/swaps/build-quote', + pathname: '/swaps/prepare-swap-page', }; }), })); @@ -81,12 +81,10 @@ describe('Swap', () => { it('renders the component with initial props', async () => { const swapsMockStore = createSwapsMockStore(); - swapsMockStore.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = false; const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider(<Swap />, store); await waitFor(() => expect(featureFlagsNock.isDone()).toBe(true)); expect(getByText('Swap')).toBeInTheDocument(); - expect(getByText('Cancel')).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); }); 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 a8b7c9bf2a51..e98d275f8aa8 100644 --- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js +++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js @@ -6,7 +6,7 @@ import { shuffle } from 'lodash'; import { useHistory } from 'react-router-dom'; import isEqual from 'lodash/isEqual'; import { - navigateBackToBuildQuote, + navigateBackToPrepareSwap, getFetchParams, getQuotesFetchStartTime, getCurrentSmartTransactionsEnabled, @@ -183,7 +183,7 @@ export default function LoadingSwapsQuotes({ submitText={t('back')} onSubmit={async () => { trackEvent(quotesRequestCancelledEventConfig); - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); }} hideCancel /> diff --git a/ui/pages/swaps/main-quote-summary/README.mdx b/ui/pages/swaps/main-quote-summary/README.mdx deleted file mode 100644 index c32397d1e762..000000000000 --- a/ui/pages/swaps/main-quote-summary/README.mdx +++ /dev/null @@ -1,14 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; -import MainQuoteSummary from '.'; - -# MainQuoteSummary - -MainQuoteSummary displays the quote of a swap. - -<Canvas> - <Story id="pages-swaps-mainquotesummary--default-story" /> -</Canvas> - -## Props - -<ArgsTable of={MainQuoteSummary} /> \ No newline at end of file diff --git a/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap b/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap deleted file mode 100644 index 3b202988fc42..000000000000 --- a/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MainQuoteSummary renders the component with initial props 1`] = ` -<div - class="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" -> - <span - class="main-quote-summary__source-row-value" - title="2" - > - 2 - </span> - <div - class="" - > - <span - class="icon-with-fallback__fallback url-icon__fallback main-quote-summary__icon-fallback" - > - E - </span> - </div> - <span - class="main-quote-summary__source-row-symbol" - title="ETH" - > - ETH - </span> -</div> -`; - -exports[`MainQuoteSummary renders the component with initial props 2`] = ` -<div - class="main-quote-summary__destination-row" -> - <div - class="" - > - <span - class="icon-with-fallback__fallback url-icon__fallback main-quote-summary__icon-fallback" - > - B - </span> - </div> - <span - class="main-quote-summary__destination-row-symbol" - > - BAT - </span> -</div> -`; - -exports[`MainQuoteSummary renders the component with initial props 3`] = ` -<div - class="main-quote-summary__quote-large" -> - <div> - <div - class="" - style="display: inline;" - tabindex="0" - title="" - > - <span - class="main-quote-summary__quote-large-number" - style="font-size: 50px; line-height: 48px;" - > - 0.2 - </span> - </div> - </div> -</div> -`; - -exports[`MainQuoteSummary renders the component with initial props 4`] = ` -<div - class="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" -> - <div - class="exchange-rate-display main-quote-summary__exchange-rate-display" - > - <div - class="box exchange-rate-display__quote-rate--no-link box--display-flex box--gap-1 box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default" - data-testid="exchange-rate-display-quote-rate" - > - <span> - 1 - </span> - <span - class="" - data-testid="exchange-rate-display-base-symbol" - > - ETH - </span> - <span> - = - </span> - <span> - 0.1 - </span> - <span - class="" - > - BAT - </span> - </div> - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative" - data-testid="exchange-rate-display-switch" - style="mask-image: url('./images/icons/swap-horizontal.svg'); cursor: pointer;" - title="Switch" - /> - </div> -</div> -`; diff --git a/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap b/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap deleted file mode 100644 index 2a1fef0ff0ac..000000000000 --- a/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap +++ /dev/null @@ -1,74 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`QuotesBackdrop renders the component with initial props 1`] = ` -<g - filter="url(#filter0_d)" -> - <path - d="M25.4749 54C25.4749 49.5817 29.0566 46 33.4749 46H328.475C332.893 46 336.475 49.5817 336.475 54V185.5C336.475 189.918 332.893 193.5 328.475 193.5H33.4749C29.0566 193.5 25.4749 189.918 25.4749 185.5V54Z" - fill="url(#paint0_linear)" - /> -</g> -`; - -exports[`QuotesBackdrop renders the component with initial props 2`] = ` -<filter - color-interpolation-filters="sRGB" - filterUnits="userSpaceOnUse" - height="242.164" - id="filter0_d" - width="389" - x="-13.5251" - y="0.335938" -> - <feflood - flood-opacity="0" - result="BackgroundImageFix" - /> - <fecolormatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - /> - <feoffset - dy="10" - /> - <fegaussianblur - stdDeviation="19.5" - /> - <fecolormatrix - type="matrix" - values="0 0 0 0 0.0117647 0 0 0 0 0.491686 0 0 0 0 0.839216 0 0 0 0.15 0" - /> - <feblend - in2="BackgroundImageFix" - mode="normal" - result="effect1_dropShadow" - /> - <feblend - in="SourceGraphic" - in2="effect1_dropShadow" - mode="normal" - result="shape" - /> -</filter> -`; - -exports[`QuotesBackdrop renders the component with initial props 3`] = ` -<lineargradient - gradientUnits="userSpaceOnUse" - id="paint0_linear" - x1="25.4749" - x2="342.234" - y1="90.693" - y2="90.693" -> - <stop - stop-color="#037DD6" - /> - <stop - offset="0.994792" - stop-color="#1098FC" - /> -</lineargradient> -`; diff --git a/ui/pages/swaps/main-quote-summary/index.js b/ui/pages/swaps/main-quote-summary/index.js deleted file mode 100644 index 235070e29323..000000000000 --- a/ui/pages/swaps/main-quote-summary/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './main-quote-summary'; diff --git a/ui/pages/swaps/main-quote-summary/index.scss b/ui/pages/swaps/main-quote-summary/index.scss deleted file mode 100644 index 3f7693705db8..000000000000 --- a/ui/pages/swaps/main-quote-summary/index.scss +++ /dev/null @@ -1,125 +0,0 @@ -@use "design-system"; - -.main-quote-summary { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - position: relative; - width: 100%; - color: var(--color-text-default); - margin-top: 28px; - margin-bottom: 56px; - - &__source-row, - &__destination-row { - width: 100%; - display: flex; - align-items: flex-start; - justify-content: center; - - @include design-system.H6; - - color: var(--color-text-alternative); - } - - &__source-row { - align-items: center; - } - - &__source-row-value, - &__source-row-symbol { - // Each of these spans can be half their container width minus the space - // needed for the token icon and the span margins - max-width: calc(50% - 13px); - } - - - &__source-row-value { - margin-right: 5px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__source-row-symbol { - margin-left: 5px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__destination-row { - margin-top: 6px; - } - - &__destination-row-symbol { - margin-left: 5px; - color: var(--color-text-default); - } - - &__icon, - &__icon-fallback { - height: 16px; - width: 16px; - } - - &__icon-fallback { - padding-top: 0; - font-size: 12px; - line-height: 16px; - } - - &__down-arrow { - margin-top: 5px; - color: var(--color-icon-muted); - } - - &__details { - display: flex; - flex-flow: column; - align-items: center; - width: 310px; - position: relative; - } - - &__quote-details-top { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - width: 100%; - } - - &__quote-large { - display: flex; - align-items: flex-start; - margin-top: 8px; - height: 50px; - } - - &__quote-large-number { - font-size: 50px; - line-height: 48px; - } - - &__quote-large-white { - font-size: 40px; - text-overflow: ellipsis; - width: 295px; - overflow: hidden; - white-space: nowrap; - } - - &__exchange-rate-container { - display: flex; - justify-content: center; - align-items: center; - width: 287px; - margin-top: 14px; - } - - &__exchange-rate-display { - color: var(--color-text-alternative); - } -} diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.js deleted file mode 100644 index d7ff9646a3a6..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.js +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BigNumber from 'bignumber.js'; -import Tooltip from '../../../components/ui/tooltip'; -import UrlIcon from '../../../components/ui/url-icon'; -import ExchangeRateDisplay from '../exchange-rate-display'; -import { formatSwapsValueForDisplay } from '../swaps.util'; -import { - calcTokenAmount, - toPrecisionWithoutTrailingZeros, -} from '../../../../shared/lib/transactions-controller-utils'; - -function getFontSizesAndLineHeights(fontSizeScore) { - if (fontSizeScore <= 9) { - return [50, 48]; - } - if (fontSizeScore <= 13) { - return [40, 32]; - } - return [26, 15]; -} - -export default function MainQuoteSummary({ - sourceValue, - sourceSymbol, - sourceDecimals, - sourceIconUrl, - destinationValue, - destinationSymbol, - destinationDecimals, - destinationIconUrl, -}) { - const sourceAmount = toPrecisionWithoutTrailingZeros( - calcTokenAmount(sourceValue, sourceDecimals).toString(10), - 12, - ); - const destinationAmount = calcTokenAmount( - destinationValue, - destinationDecimals, - ); - - const amountToDisplay = formatSwapsValueForDisplay(destinationAmount); - const amountDigitLength = amountToDisplay.match(/\d+/gu).join('').length; - const [numberFontSize, lineHeight] = - getFontSizesAndLineHeights(amountDigitLength); - let ellipsedAmountToDisplay = amountToDisplay; - - if (amountDigitLength > 20) { - ellipsedAmountToDisplay = `${amountToDisplay.slice(0, 20)}...`; - } - - return ( - <div className="main-quote-summary"> - <div className="main-quote-summary__details"> - <div className="main-quote-summary__quote-details-top"> - <div - className="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" - > - <span - className="main-quote-summary__source-row-value" - title={formatSwapsValueForDisplay(sourceAmount)} - > - {formatSwapsValueForDisplay(sourceAmount)} - </span> - <UrlIcon - url={sourceIconUrl} - className="main-quote-summary__icon" - name={sourceSymbol} - fallbackClassName="main-quote-summary__icon-fallback" - /> - <span - className="main-quote-summary__source-row-symbol" - title={sourceSymbol} - > - {sourceSymbol} - </span> - </div> - <i className="fa fa-arrow-down main-quote-summary__down-arrow" /> - <div className="main-quote-summary__destination-row"> - <UrlIcon - url={destinationIconUrl} - className="main-quote-summary__icon" - name={destinationSymbol} - fallbackClassName="main-quote-summary__icon-fallback" - /> - <span className="main-quote-summary__destination-row-symbol"> - {destinationSymbol} - </span> - </div> - <div className="main-quote-summary__quote-large"> - <Tooltip - interactive - position="bottom" - html={amountToDisplay} - disabled={ellipsedAmountToDisplay === amountToDisplay} - > - <span - className="main-quote-summary__quote-large-number" - style={{ - fontSize: numberFontSize, - lineHeight: `${lineHeight}px`, - }} - > - {`${ellipsedAmountToDisplay}`} - </span> - </Tooltip> - </div> - </div> - <div - className="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" - > - <ExchangeRateDisplay - primaryTokenValue={sourceValue} - primaryTokenDecimals={sourceDecimals} - primaryTokenSymbol={sourceSymbol} - secondaryTokenValue={destinationValue} - secondaryTokenDecimals={destinationDecimals} - secondaryTokenSymbol={destinationSymbol} - arrowColor="var(--color-primary-default)" - boldSymbols={false} - className="main-quote-summary__exchange-rate-display" - /> - </div> - </div> - </div> - ); -} - -MainQuoteSummary.propTypes = { - /** - * The amount that will be sent in the smallest denomination. - * For example, wei is the smallest denomination for ether. - */ - sourceValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(BigNumber), - ]).isRequired, - - /** - * Maximum number of decimal places for the source token. - */ - sourceDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** - * The ticker symbol for the source token. - */ - sourceSymbol: PropTypes.string.isRequired, - - /** - * The amount that will be received in the smallest denomination. - * For example, wei is the smallest denomination for ether. - */ - destinationValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(BigNumber), - ]).isRequired, - - /** - * Maximum number of decimal places for the destination token. - */ - destinationDecimals: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - - /** - * The ticker symbol for the destination token. - */ - destinationSymbol: PropTypes.string.isRequired, - - /** - * The location of the source token icon file. - */ - sourceIconUrl: PropTypes.string, - - /** - * The location of the destination token icon file. - */ - destinationIconUrl: PropTypes.string, -}; diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js deleted file mode 100644 index 56ed74624193..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import README from './README.mdx'; -import MainQuoteSummary from './main-quote-summary'; - -export default { - title: 'Pages/Swaps/MainQuoteSummary', - - component: MainQuoteSummary, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - sourceValue: { - control: 'text', - }, - sourceDecimals: { - control: 'number', - }, - sourceSymbol: { - control: 'text', - }, - destinationValue: { - control: 'text', - }, - destinationDecimals: { - control: 'number', - }, - destinationSymbol: { - control: 'text', - }, - sourceIconUrl: { - control: 'text', - }, - destinationIconUrl: { - control: 'text', - }, - }, - args: { - sourceValue: '2000000000000000000', - sourceDecimals: 18, - sourceSymbol: 'ETH', - destinationValue: '200000000000000000', - destinationDecimals: 18, - destinationSymbol: 'ABC', - sourceIconUrl: '.storybook/images/metamark.svg', - destinationIconUrl: '.storybook/images/sai.svg', - }, -}; - -export const DefaultStory = (args) => { - return ( - <div - style={{ - width: '360px', - height: '224px', - border: '1px solid black', - padding: '24px', - }} - > - <MainQuoteSummary {...args} /> - </div> - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js deleted file mode 100644 index 85e17bd48de4..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../test/jest'; -import MainQuoteSummary from '.'; - -const createProps = (customProps = {}) => { - return { - sourceValue: '2000000000000000000', - sourceDecimals: 18, - sourceSymbol: 'ETH', - destinationValue: '200000000000000000', - destinationDecimals: 18, - destinationSymbol: 'BAT', - ...customProps, - }; -}; - -describe('MainQuoteSummary', () => { - it('renders the component with initial props', () => { - const props = createProps(); - const { getAllByText } = renderWithProvider( - <MainQuoteSummary {...props} />, - ); - expect(getAllByText(props.sourceSymbol)).toHaveLength(2); - expect(getAllByText(props.destinationSymbol)).toHaveLength(2); - expect( - document.querySelector('.main-quote-summary__source-row'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__destination-row'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__quote-large'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.js deleted file mode 100644 index 44351c73bb5d..000000000000 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.js +++ /dev/null @@ -1,89 +0,0 @@ -/* eslint-disable @metamask/design-tokens/color-no-hex*/ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default function QuotesBackdrop({ withTopTab }) { - return ( - <svg - width="311" - height="164" - viewBox="25.5 29.335899353027344 311 164" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <g filter="url(#filter0_d)"> - <path - d="M25.4749 54C25.4749 49.5817 29.0566 46 33.4749 46H328.475C332.893 46 336.475 49.5817 336.475 54V185.5C336.475 189.918 332.893 193.5 328.475 193.5H33.4749C29.0566 193.5 25.4749 189.918 25.4749 185.5V54Z" - fill="url(#paint0_linear)" - /> - {withTopTab && ( - <path - d="M132.68 34.3305C133.903 31.3114 136.836 29.3359 140.094 29.3359H219.858C223.116 29.3359 226.048 31.3114 227.272 34.3305L237.443 59.4217C239.575 64.6815 235.705 70.4271 230.029 70.4271H129.922C124.247 70.4271 120.376 64.6814 122.508 59.4217L132.68 34.3305Z" - fill="url(#paint1_linear)" - /> - )} - </g> - <defs> - <filter - id="filter0_d" - x="-13.5251" - y="0.335938" - width="389" - height="242.164" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - /> - <feOffset dy="10" /> - <feGaussianBlur stdDeviation="19.5" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0.0117647 0 0 0 0 0.491686 0 0 0 0 0.839216 0 0 0 0.15 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow" - result="shape" - /> - </filter> - <linearGradient - id="paint0_linear" - x1="25.4749" - y1="90.693" - x2="342.234" - y2="90.693" - gradientUnits="userSpaceOnUse" - > - <stop stopColor="#037DD6" /> - <stop offset="0.994792" stopColor="#1098FC" /> - </linearGradient> - <linearGradient - id="paint1_linear" - x1="25.4749" - y1="90.693" - x2="342.234" - y2="90.693" - gradientUnits="userSpaceOnUse" - > - <stop stopColor="#037DD6" /> - <stop offset="0.994792" stopColor="#1098FC" /> - </linearGradient> - </defs> - </svg> - ); -} - -QuotesBackdrop.propTypes = { - withTopTab: PropTypes.bool, -}; diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js deleted file mode 100644 index 00d23c2656d6..000000000000 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../test/jest'; -import QuotesBackdrop from './quote-backdrop'; - -const createProps = (customProps = {}) => { - return { - withTopTab: false, - ...customProps, - }; -}; - -describe('QuotesBackdrop', () => { - it('renders the component with initial props', () => { - const { container } = renderWithProvider( - <QuotesBackdrop {...createProps()} />, - ); - expect(container.firstChild.nodeName).toBe('svg'); - expect(document.querySelector('g')).toMatchSnapshot(); - expect(document.querySelector('filter')).toMatchSnapshot(); - expect(document.querySelector('linearGradient')).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/popover-custom-background/index.scss b/ui/pages/swaps/popover-custom-background/index.scss deleted file mode 100644 index 07bc852edbfd..000000000000 --- a/ui/pages/swaps/popover-custom-background/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.popover-custom-background { - height: 100%; - width: 100%; - background: var(--color-background-alternative); - opacity: 0.6; -} diff --git a/ui/pages/swaps/popover-custom-background/popover-custom-background.js b/ui/pages/swaps/popover-custom-background/popover-custom-background.js deleted file mode 100644 index 8e8af648ad7f..000000000000 --- a/ui/pages/swaps/popover-custom-background/popover-custom-background.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Box from '../../../components/ui/box'; - -const PopoverCustomBackground = ({ onClose }) => { - return <Box className="popover-custom-background" onClick={onClose}></Box>; -}; - -export default PopoverCustomBackground; - -PopoverCustomBackground.propTypes = { - onClose: PropTypes.func, -}; diff --git a/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap b/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap deleted file mode 100644 index 8991347eecfc..000000000000 --- a/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PrepareSwapPage renders the component with initial props 1`] = `null`; diff --git a/ui/pages/swaps/prepare-swap-page/index.scss b/ui/pages/swaps/prepare-swap-page/index.scss index 1909f11024b1..60e24c6cdbce 100644 --- a/ui/pages/swaps/prepare-swap-page/index.scss +++ b/ui/pages/swaps/prepare-swap-page/index.scss @@ -27,12 +27,6 @@ margin-top: 16px; position: relative; - .dropdown-input-pair__input { - input { - text-align: right; - } - } - .MuiInputBase-root { padding-right: 0; } @@ -124,98 +118,6 @@ } } - .dropdown-search-list { - background-color: var(--color-background-alternative); - border-radius: 100px; - - &__select-default { - color: var(--color-text-default); - } - - &__labels { - flex: auto; - max-width: 110px; - - &--with-icon { - max-width: 95px; - } - } - - &__closed-primary-label { - font-weight: 500; - } - - &__selector-closed-container { - border: 0; - border-radius: 100px; - height: 32px; - max-height: 32px; - max-width: 165px; - width: auto; - } - - &__selector-closed-icon { - width: 24px; - height: 24px; - margin-right: 8px; - } - - &__selector-closed { - height: 32px; - max-width: 140px; - - div { - display: flex; - } - - &__item-labels { - width: 100%; - margin-left: 0; - } - } - } - - .dropdown-input-pair { - height: 32px; - width: auto; - - &__selector--closed { - height: 32px; - border-top-right-radius: 100px; - border-bottom-right-radius: 100px; - } - - .searchable-item-list { - &__item--add-token { - display: none; - } - } - - &__to { - display: flex; - justify-content: space-between; - align-items: center; - - .searchable-item-list { - &__item--add-token { - display: flex; - } - } - } - - &__input { - div { - border: 0; - } - } - - &__two-line-input { - input { - padding-bottom: 0; - } - } - } - &__token-etherscan-link { color: var(--color-primary-default); cursor: pointer; @@ -306,12 +208,6 @@ width: 100%; } - .main-quote-summary { - &__exchange-rate-display { - width: auto; - } - } - &::after { // Hide preloaded images. position: absolute; width: 0; @@ -365,6 +261,10 @@ &__edit-limit { white-space: nowrap; } + + &__exchange-rate-display { + color: var(--color-text-alternative); + } } @keyframes slide-in { diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js index d576dad0933d..a76160ef77bb 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js @@ -82,9 +82,6 @@ describe('PrepareSwapPage', () => { store, ); expect(getByText('Select token')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); }); it('switches swap from and to tokens', () => { diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 9921161c4da4..4c47437b1bd8 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -1205,7 +1205,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { secondaryTokenDecimals={destinationTokenDecimals} secondaryTokenSymbol={destinationTokenSymbol} boldSymbols={false} - className="main-quote-summary__exchange-rate-display" + className="review-quote__exchange-rate-display" showIconForSwappingTokens={false} /> </Box> diff --git a/ui/pages/swaps/select-quote-popover/quote-details/index.scss b/ui/pages/swaps/select-quote-popover/quote-details/index.scss index 861759235aba..80e034ab1d4d 100644 --- a/ui/pages/swaps/select-quote-popover/quote-details/index.scss +++ b/ui/pages/swaps/select-quote-popover/quote-details/index.scss @@ -30,10 +30,6 @@ align-items: center; height: inherit; - .view-quote__conversion-rate-eth-label { - color: var(--color-text-default); - } - i { color: var(--color-primary-default); } @@ -68,14 +64,6 @@ } } - .view-quote__conversion-rate-token-label { - @include design-system.H6; - - color: var(--color-text-default); - font-weight: bold; - margin-left: 2px; - } - &__metafox-logo { width: 17px; margin-right: 4px; diff --git a/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap b/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap index 11cd9372ed7f..189eda3e38e3 100644 --- a/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap +++ b/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap @@ -3,40 +3,44 @@ exports[`SelectedToken renders the component with initial props 1`] = ` <div> <div - class="dropdown-search-list dropdown-search-list__selector-closed-container dropdown-input-pair__selector--closed" - data-testid="dropdown-search-list" - tabindex="0" + class="selected-token" > <div - class="dropdown-search-list__selector-closed" + class="selected-token-list selected-token-list__selector-closed-container selected-token-input-pair__selector--closed" + data-testid="selected-token-list" + tabindex="0" > <div - class="" - > - <img - alt="ETH" - class="url-icon dropdown-search-list__selector-closed-icon" - src="iconUrl" - /> - </div> - <div - class="dropdown-search-list__labels dropdown-search-list__labels--with-icon" + class="selected-token-list__selector-closed" > <div - class="dropdown-search-list__item-labels" + class="" + > + <img + alt="ETH" + class="url-icon selected-token-list__selector-closed-icon" + src="iconUrl" + /> + </div> + <div + class="selected-token-list__labels selected-token-list__labels--with-icon" > - <span - class="dropdown-search-list__closed-primary-label" + <div + class="selected-token-list__item-labels" > - ETH - </span> + <span + class="selected-token-list__closed-primary-label" + > + ETH + </span> + </div> </div> </div> + <span + class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> </div> - <span - class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" - style="mask-image: url('./images/icons/arrow-down.svg');" - /> </div> </div> `; @@ -44,31 +48,35 @@ exports[`SelectedToken renders the component with initial props 1`] = ` exports[`SelectedToken renders the component with no token selected 1`] = ` <div> <div - class="dropdown-search-list dropdown-search-list__selector-closed-container dropdown-input-pair__selector--closed" - data-testid="dropdown-search-list" - tabindex="0" + class="selected-token" > <div - class="dropdown-search-list__selector-closed" + class="selected-token-list selected-token-list__selector-closed-container selected-token-input-pair__selector--closed" + data-testid="selected-token-list" + tabindex="0" > <div - class="dropdown-search-list__labels" + class="selected-token-list__selector-closed" > <div - class="dropdown-search-list__item-labels" + class="selected-token-list__labels" > - <span - class="dropdown-search-list__closed-primary-label dropdown-search-list__select-default" + <div + class="selected-token-list__item-labels" > - Select token - </span> + <span + class="selected-token-list__closed-primary-label selected-token-list__select-default" + > + Select token + </span> + </div> </div> </div> + <span + class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> </div> - <span - class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" - style="mask-image: url('./images/icons/arrow-down.svg');" - /> </div> </div> `; diff --git a/ui/pages/swaps/selected-token/index.scss b/ui/pages/swaps/selected-token/index.scss new file mode 100644 index 000000000000..bc69a934ae02 --- /dev/null +++ b/ui/pages/swaps/selected-token/index.scss @@ -0,0 +1,142 @@ +@use "design-system"; + +.selected-token { + .selected-token-list { + background-color: var(--color-background-alternative); + border-radius: 100px; + + &__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + } + + &__select-default { + color: var(--color-text-default); + } + + &__labels { + display: flex; + justify-content: space-between; + width: 100%; + flex: auto; + max-width: 110px; + + &--with-icon { + max-width: 95px; + } + } + + &__closed-primary-label { + @include design-system.H4; + + color: var(--color-text-default); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + } + + &__selector-closed-container { + display: flex; + position: relative; + align-items: center; + transition: 200ms ease-in-out; + box-shadow: none; + border: 0; + border-radius: 100px; + height: 32px; + max-height: 32px; + max-width: 165px; + width: auto; + + &:hover { + background: var(--color-background-default-hover); + } + } + + &__selector-closed-icon { + width: 24px; + height: 24px; + margin-right: 8px; + } + + &__selector-closed { + display: flex; + flex-flow: row nowrap; + padding: 16px 12px; + box-sizing: border-box; + cursor: pointer; + position: relative; + align-items: center; + flex: 1; + height: 32px; + max-width: 140px; + + i { + font-size: 1.2em; + } + + div { + display: flex; + } + + &__item-labels { + width: 100%; + margin-left: 0; + } + } + + &__item-labels { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + } + } + + .selected-token-input-pair { + height: 32px; + width: auto; + + &__selector--closed { + height: 60px; + border-top-right-radius: 100px; + border-bottom-right-radius: 100px; + } + + .searchable-item-list { + &__item--add-token { + display: none; + } + } + + &__to { + display: flex; + justify-content: space-between; + align-items: center; + + .searchable-item-list { + &__item--add-token { + display: flex; + } + } + } + + &__input { + div { + border: 0; + } + } + + &__two-line-input { + input { + padding-bottom: 0; + } + } + } +} diff --git a/ui/pages/swaps/selected-token/selected-token.js b/ui/pages/swaps/selected-token/selected-token.js index 516dbdc246e3..174a5925282b 100644 --- a/ui/pages/swaps/selected-token/selected-token.js +++ b/ui/pages/swaps/selected-token/selected-token.js @@ -29,52 +29,54 @@ export default function SelectedToken({ }; return ( - <div - className={classnames( - 'dropdown-search-list', - 'dropdown-search-list__selector-closed-container', - 'dropdown-input-pair__selector--closed', - )} - data-testid="dropdown-search-list" - tabIndex="0" - onClick={onClick} - onKeyUp={onKeyUp} - > - <div className="dropdown-search-list__selector-closed"> - {hasIcon && ( - <UrlIcon - url={selectedToken.iconUrl} - className="dropdown-search-list__selector-closed-icon" - name={selectedToken?.symbol} - /> + <div className="selected-token"> + <div + className={classnames( + 'selected-token-list', + 'selected-token-list__selector-closed-container', + 'selected-token-input-pair__selector--closed', )} - <div - className={classnames('dropdown-search-list__labels', { - 'dropdown-search-list__labels--with-icon': hasIcon, - })} - > - <div className="dropdown-search-list__item-labels"> - <span - data-testid={testId} - className={classnames( - 'dropdown-search-list__closed-primary-label', - { - 'dropdown-search-list__select-default': - !selectedToken?.symbol, - }, - )} - > - {selectedToken?.symbol || t('swapSelectAToken')} - </span> + data-testid="selected-token-list" + tabIndex="0" + onClick={onClick} + onKeyUp={onKeyUp} + > + <div className="selected-token-list__selector-closed"> + {hasIcon && ( + <UrlIcon + url={selectedToken.iconUrl} + className="selected-token-list__selector-closed-icon" + name={selectedToken?.symbol} + /> + )} + <div + className={classnames('selected-token-list__labels', { + 'selected-token-list__labels--with-icon': hasIcon, + })} + > + <div className="selected-token-list__item-labels"> + <span + data-testid={testId} + className={classnames( + 'selected-token-list__closed-primary-label', + { + 'selected-token-list__select-default': + !selectedToken?.symbol, + }, + )} + > + {selectedToken?.symbol || t('swapSelectAToken')} + </span> + </div> </div> </div> + <Icon + name={IconName.ArrowDown} + size={IconSize.Xs} + marginRight={3} + color={IconColor.iconAlternative} + /> </div> - <Icon - name={IconName.ArrowDown} - size={IconSize.Xs} - marginRight={3} - color={IconColor.iconAlternative} - /> </div> ); } diff --git a/ui/pages/swaps/selected-token/selected-token.test.js b/ui/pages/swaps/selected-token/selected-token.test.js index 15cca222a8ae..af70ed0bd8f1 100644 --- a/ui/pages/swaps/selected-token/selected-token.test.js +++ b/ui/pages/swaps/selected-token/selected-token.test.js @@ -37,7 +37,7 @@ describe('SelectedToken', () => { it('renders the component and opens the list', () => { const props = createProps(); const { getByTestId } = renderWithProvider(<SelectedToken {...props} />); - const dropdownSearchList = getByTestId('dropdown-search-list'); + const dropdownSearchList = getByTestId('selected-token-list'); expect(dropdownSearchList).toBeInTheDocument(); fireEvent.click(dropdownSearchList); expect(props.onClick).toHaveBeenCalledTimes(1); diff --git a/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap b/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap deleted file mode 100644 index 4f1f1d15f355..000000000000 --- a/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SlippageButtons renders the component with initial props 1`] = ` -<button - class="slippage-buttons__header slippage-buttons__header--open" -> - <span - class="mm-box mm-text mm-text--body-sm-bold mm-box--margin-right-2 mm-box--color-primary-default" - > - Advanced options - </span> - <i - class="fa fa-angle-up" - /> -</button> -`; - -exports[`SlippageButtons renders the component with initial props 2`] = ` -<div - class="button-group slippage-buttons__button-group radio-button-group" - role="radiogroup" -> - <button - aria-checked="false" - class="button-group__button radio-button" - data-testid="button-group__button0" - role="radio" - > - 2% - </button> - <button - aria-checked="true" - class="button-group__button radio-button button-group__button--active radio-button--active" - data-testid="button-group__button1" - role="radio" - > - 3% - </button> - <button - aria-checked="false" - class="button-group__button slippage-buttons__button-group-custom-button radio-button" - data-testid="button-group__button2" - role="radio" - > - custom - </button> -</div> -`; diff --git a/ui/pages/swaps/slippage-buttons/index.js b/ui/pages/swaps/slippage-buttons/index.js deleted file mode 100644 index 6cdbe8843019..000000000000 --- a/ui/pages/swaps/slippage-buttons/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './slippage-buttons'; diff --git a/ui/pages/swaps/slippage-buttons/index.scss b/ui/pages/swaps/slippage-buttons/index.scss deleted file mode 100644 index de6e68ba3556..000000000000 --- a/ui/pages/swaps/slippage-buttons/index.scss +++ /dev/null @@ -1,111 +0,0 @@ -@use "design-system"; - -.slippage-buttons { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - - &__header { - display: flex; - align-items: center; - color: var(--color-primary-default); - margin-bottom: 0; - margin-left: auto; - margin-right: auto; - background: unset; - - &--open { - margin-bottom: 8px; - } - } - - &__content { - padding-left: 10px; - } - - &__dropdown-content { - display: flex; - align-items: center; - } - - &__buttons-prefix { - display: flex; - align-items: center; - margin-right: 8px; - } - - &__button-group { - & &-custom-button { - cursor: text; - display: flex; - align-items: center; - justify-content: center; - position: relative; - min-width: 72px; - margin-right: 0; - } - } - - &__custom-input { - display: flex; - justify-content: center; - - input { - border: none; - width: 64px; - text-align: center; - background: var(--color-primary-default); - color: var(--color-primary-inverse); - font-weight: inherit; - - &::-webkit-input-placeholder { /* WebKit, Blink, Edge */ - color: var(--color-primary-inverse); - } - - &:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ - color: var(--color-primary-inverse); - opacity: 1; - } - - &::-moz-placeholder { /* Mozilla Firefox 19+ */ - color: var(--color-primary-inverse); - opacity: 1; - } - - &:-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: var(--color-primary-inverse); - } - - &::-ms-input-placeholder { /* Microsoft Edge */ - color: var(--color-primary-inverse); - } - - &::placeholder { /* Most modern browsers support this now. */ - color: var(--color-primary-inverse); - } - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ - } - - input[type=number] { - -moz-appearance: textfield; - } - - &--danger { - input { - background: var(--color-error-default); - } - } - } - - &__percentage-suffix { - position: absolute; - right: 5px; - } -} diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.js deleted file mode 100644 index 387958753e3c..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.js +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { I18nContext } from '../../../contexts/i18n'; -import ButtonGroup from '../../../components/ui/button-group'; -import Button from '../../../components/ui/button'; -import InfoTooltip from '../../../components/ui/info-tooltip'; -import { Slippage } from '../../../../shared/constants/swaps'; -import { Text } from '../../../components/component-library'; -import { - TextVariant, - TextColor, -} from '../../../helpers/constants/design-system'; - -export default function SlippageButtons({ - onSelect, - maxAllowedSlippage, - currentSlippage, - isDirectWrappingEnabled, -}) { - const t = useContext(I18nContext); - const [customValue, setCustomValue] = useState(() => { - if ( - typeof currentSlippage === 'number' && - !Object.values(Slippage).includes(currentSlippage) - ) { - return currentSlippage.toString(); - } - return ''; - }); - const [enteringCustomValue, setEnteringCustomValue] = useState(false); - const [activeButtonIndex, setActiveButtonIndex] = useState(() => { - if (currentSlippage === Slippage.high) { - return 1; // 3% slippage. - } else if (currentSlippage === Slippage.default) { - return 0; // 2% slippage. - } else if (typeof currentSlippage === 'number') { - return 2; // Custom slippage. - } - return 0; - }); - const [open, setOpen] = useState(() => { - return currentSlippage !== Slippage.default; // Only open Advanced options by default if it's not default slippage. - }); - const [inputRef, setInputRef] = useState(null); - - let errorText = ''; - if (customValue) { - // customValue is a string, e.g. '0' - if (Number(customValue) < 0) { - errorText = t('swapSlippageNegative'); - } else if (Number(customValue) > 0 && Number(customValue) <= 1) { - // We will not show this warning for 0% slippage, because we will only - // return non-slippage quotes from off-chain makers. - errorText = t('swapLowSlippageError'); - } else if ( - Number(customValue) >= 5 && - Number(customValue) <= maxAllowedSlippage - ) { - errorText = t('swapHighSlippageWarning'); - } else if (Number(customValue) > maxAllowedSlippage) { - errorText = t('swapsExcessiveSlippageWarning'); - } - } - - const customValueText = customValue || t('swapCustom'); - - useEffect(() => { - if ( - inputRef && - enteringCustomValue && - window.document.activeElement !== inputRef - ) { - inputRef.focus(); - } - }, [inputRef, enteringCustomValue]); - - return ( - <div className="slippage-buttons"> - <button - onClick={() => setOpen(!open)} - className={classnames('slippage-buttons__header', { - 'slippage-buttons__header--open': open, - })} - > - <Text - variant={TextVariant.bodySmBold} - marginRight={2} - color={TextColor.primaryDefault} - as="span" - > - {t('swapsAdvancedOptions')} - </Text> - {open ? ( - <i className="fa fa-angle-up" /> - ) : ( - <i className="fa fa-angle-down" /> - )} - </button> - <div className="slippage-buttons__content"> - {open && ( - <> - {!isDirectWrappingEnabled && ( - <div className="slippage-buttons__dropdown-content"> - <div className="slippage-buttons__buttons-prefix"> - <Text - variant={TextVariant.bodySmBold} - marginRight={1} - color={TextColor.textDefault} - > - {t('swapsMaxSlippage')} - </Text> - <InfoTooltip - position="top" - contentText={t('swapSlippageTooltip')} - /> - </div> - <ButtonGroup - defaultActiveButtonIndex={ - activeButtonIndex === 2 && !customValue - ? 1 - : activeButtonIndex - } - variant="radiogroup" - newActiveButtonIndex={activeButtonIndex} - className={classnames( - 'button-group', - 'slippage-buttons__button-group', - )} - > - <Button - onClick={() => { - setCustomValue(''); - setEnteringCustomValue(false); - setActiveButtonIndex(0); - onSelect(Slippage.default); - }} - > - {t('swapSlippagePercent', [Slippage.default])} - </Button> - <Button - onClick={() => { - setCustomValue(''); - setEnteringCustomValue(false); - setActiveButtonIndex(1); - onSelect(Slippage.high); - }} - > - {t('swapSlippagePercent', [Slippage.high])} - </Button> - <Button - className={classnames( - 'slippage-buttons__button-group-custom-button', - { - 'radio-button--danger': errorText, - }, - )} - onClick={() => { - setActiveButtonIndex(2); - setEnteringCustomValue(true); - }} - > - {enteringCustomValue ? ( - <div - className={classnames( - 'slippage-buttons__custom-input', - { - 'slippage-buttons__custom-input--danger': errorText, - }, - )} - > - <input - data-testid="slippage-buttons__custom-slippage" - onChange={(event) => { - const { value } = event.target; - const isValueNumeric = !isNaN(Number(value)); - if (isValueNumeric) { - setCustomValue(value); - onSelect(Number(value)); - } - }} - type="text" - maxLength="4" - ref={setInputRef} - onBlur={() => { - setEnteringCustomValue(false); - }} - value={customValue || ''} - /> - </div> - ) : ( - customValueText - )} - {(customValue || enteringCustomValue) && ( - <div className="slippage-buttons__percentage-suffix"> - % - </div> - )} - </Button> - </ButtonGroup> - </div> - )} - </> - )} - {errorText && ( - <Text - variant={TextVariant.bodyXs} - color={TextColor.errorDefault} - marginTop={2} - > - {errorText} - </Text> - )} - </div> - </div> - ); -} - -SlippageButtons.propTypes = { - onSelect: PropTypes.func.isRequired, - maxAllowedSlippage: PropTypes.number.isRequired, - currentSlippage: PropTypes.number, - isDirectWrappingEnabled: PropTypes.bool, -}; diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js deleted file mode 100644 index 68cfd8762f44..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import SlippageButtons from './slippage-buttons'; - -export default { - title: 'Pages/Swaps/SlippageButtons', -}; - -export const DefaultStory = () => ( - <div style={{ height: '200px', marginTop: '160px' }}> - <SlippageButtons onSelect={action('slippage')} /> - </div> -); - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js deleted file mode 100644 index 7834eefd59ea..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; - -import { renderWithProvider, fireEvent } from '../../../../test/jest'; -import { Slippage } from '../../../../shared/constants/swaps'; -import SlippageButtons from './slippage-buttons'; - -const createProps = (customProps = {}) => { - return { - onSelect: jest.fn(), - maxAllowedSlippage: 15, - currentSlippage: Slippage.high, - smartTransactionsEnabled: false, - ...customProps, - }; -}; - -describe('SlippageButtons', () => { - it('renders the component with initial props', () => { - const { getByText, queryByText, getByTestId } = renderWithProvider( - <SlippageButtons {...createProps()} />, - ); - expect(getByText('2%')).toBeInTheDocument(); - expect(getByText('3%')).toBeInTheDocument(); - expect(getByText('custom')).toBeInTheDocument(); - expect(getByText('Advanced options')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__header'), - ).toMatchSnapshot(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); - expect(queryByText('Smart Swaps')).not.toBeInTheDocument(); - expect(getByTestId('button-group__button1')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('renders slippage with a custom value', () => { - const { getByText } = renderWithProvider( - <SlippageButtons {...createProps({ currentSlippage: 2.5 })} />, - ); - expect(getByText('2.5')).toBeInTheDocument(); - }); - - it('renders the default slippage with Advanced options hidden', () => { - const { getByText, queryByText } = renderWithProvider( - <SlippageButtons - {...createProps({ currentSlippage: Slippage.default })} - />, - ); - expect(getByText('Advanced options')).toBeInTheDocument(); - expect(document.querySelector('.fa-angle-down')).toBeInTheDocument(); - expect(queryByText('2%')).not.toBeInTheDocument(); - }); - - it('opens the Advanced options section and sets a default slippage', () => { - const { getByText, getByTestId } = renderWithProvider( - <SlippageButtons - {...createProps({ currentSlippage: Slippage.default })} - />, - ); - fireEvent.click(getByText('Advanced options')); - fireEvent.click(getByTestId('button-group__button0')); - expect(getByTestId('button-group__button0')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('opens the Advanced options section and sets a high slippage', () => { - const { getByText, getByTestId } = renderWithProvider( - <SlippageButtons - {...createProps({ currentSlippage: Slippage.default })} - />, - ); - fireEvent.click(getByText('Advanced options')); - fireEvent.click(getByTestId('button-group__button1')); - expect(getByTestId('button-group__button1')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('sets a custom slippage value', () => { - const { getByTestId } = renderWithProvider( - <SlippageButtons {...createProps()} />, - ); - fireEvent.click(getByTestId('button-group__button2')); - expect(getByTestId('button-group__button2')).toHaveAttribute( - 'aria-checked', - 'true', - ); - const input = getByTestId('slippage-buttons__custom-slippage'); - fireEvent.change(input, { target: { value: 5 } }); - fireEvent.click(document); - expect(input).toHaveAttribute('value', '5'); - }); -}); diff --git a/ui/pages/swaps/smart-transaction-status/index.scss b/ui/pages/swaps/smart-transaction-status/index.scss index d19add085c65..f21e3e7752bf 100644 --- a/ui/pages/swaps/smart-transaction-status/index.scss +++ b/ui/pages/swaps/smart-transaction-status/index.scss @@ -70,4 +70,16 @@ &__remaining-time { font-variant-numeric: tabular-nums; } + + &__icon, + &__icon-fallback { + height: 16px; + width: 16px; + } + + &__icon-fallback { + padding-top: 0; + font-size: 12px; + line-height: 16px; + } } 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 e6a77f9474fb..d6e7f4d653cf 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -26,7 +26,7 @@ import { import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; import { DEFAULT_ROUTE, - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import { Text } from '../../../components/component-library'; import Box from '../../../components/ui/box'; @@ -323,12 +323,12 @@ export default function SmartTransactionStatusPage() { {fetchParamsSourceTokenInfo.iconUrl ? ( <UrlIcon url={fetchParamsSourceTokenInfo.iconUrl} - className="main-quote-summary__icon" + className="smart-transactions-status-summary__icon" name={ fetchParamsSourceTokenInfo.symbol ?? latestSmartTransaction?.destinationTokenSymbol } - fallbackClassName="main-quote-summary__icon-fallback" + fallbackClassName="smart-transactions-status-summary__icon-fallback" /> ) : null} <Box display={DISPLAY.BLOCK} marginLeft={2} marginRight={2}> @@ -337,12 +337,12 @@ export default function SmartTransactionStatusPage() { {fetchParamsDestinationTokenInfo.iconUrl ? ( <UrlIcon url={fetchParamsDestinationTokenInfo.iconUrl} - className="main-quote-summary__icon" + className="smart-transactions-status-summary__icon" name={ fetchParamsDestinationTokenInfo.symbol ?? latestSmartTransaction?.destinationTokenSymbol } - fallbackClassName="main-quote-summary__icon-fallback" + fallbackClassName="smart-transactions-status-summary__icon-fallback" /> ) : null} <Text @@ -467,7 +467,7 @@ export default function SmartTransactionStatusPage() { await dispatch(prepareToLeaveSwaps()); history.push(DEFAULT_ROUTE); } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } }} onCancel={async () => { diff --git a/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap b/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap deleted file mode 100644 index 5116743379bc..000000000000 --- a/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap +++ /dev/null @@ -1,233 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`View Price Quote Difference displays a fiat error when calculationError is present 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper high" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Check your rate before proceeding - </div> - <div> - <div - aria-describedby="tippy-tooltip-4" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping. - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`View Price Quote Difference displays an error when in high bucket 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper high" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Price difference of ~% - </div> - <div> - <div - aria-describedby="tippy-tooltip-3" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - You are about to swap 1 ETH (~) for 42.947749 LINK (~). - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`View Price Quote Difference displays an error when in medium bucket 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper medium" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Price difference of ~% - </div> - <div> - <div - aria-describedby="tippy-tooltip-2" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - You are about to swap 1 ETH (~) for 42.947749 LINK (~). - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`View Price Quote Difference should match snapshot 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper low" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Price difference of ~% - </div> - <div> - <div - aria-describedby="tippy-tooltip-1" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - You are about to swap 1 ETH (~) for 42.947749 LINK (~). - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; diff --git a/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap b/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap deleted file mode 100644 index 3f6273f9f738..000000000000 --- a/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ViewQuote renders the component with EIP-1559 enabled 1`] = ` -<div - class="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" -> - <span - class="main-quote-summary__source-row-value" - title="10" - > - 10 - </span> - <div - class="" - > - <img - alt="DAI" - class="url-icon main-quote-summary__icon" - src="https://foo.bar/logo.png" - /> - </div> - <span - class="main-quote-summary__source-row-symbol" - title="DAI" - > - DAI - </span> -</div> -`; - -exports[`ViewQuote renders the component with EIP-1559 enabled 2`] = ` -<div - class="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" -> - <div - class="exchange-rate-display main-quote-summary__exchange-rate-display" - > - <div - class="box exchange-rate-display__quote-rate--no-link box--display-flex box--gap-1 box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default" - data-testid="exchange-rate-display-quote-rate" - > - <span> - 1 - </span> - <span - class="" - data-testid="exchange-rate-display-base-symbol" - > - DAI - </span> - <span> - = - </span> - <span> - 2.2 - </span> - <span - class="" - > - USDC - </span> - </div> - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative" - data-testid="exchange-rate-display-switch" - style="mask-image: url('./images/icons/swap-horizontal.svg'); cursor: pointer;" - title="Switch" - /> - </div> -</div> -`; - -exports[`ViewQuote renders the component with initial props 1`] = ` -<div - class="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" -> - <span - class="main-quote-summary__source-row-value" - title="10" - > - 10 - </span> - <div - class="" - > - <img - alt="DAI" - class="url-icon main-quote-summary__icon" - src="https://foo.bar/logo.png" - /> - </div> - <span - class="main-quote-summary__source-row-symbol" - title="DAI" - > - DAI - </span> -</div> -`; - -exports[`ViewQuote renders the component with initial props 2`] = ` -<div - class="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" -> - <div - class="exchange-rate-display main-quote-summary__exchange-rate-display" - > - <div - class="box exchange-rate-display__quote-rate--no-link box--display-flex box--gap-1 box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default" - data-testid="exchange-rate-display-quote-rate" - > - <span> - 1 - </span> - <span - class="" - data-testid="exchange-rate-display-base-symbol" - > - DAI - </span> - <span> - = - </span> - <span> - 2.2 - </span> - <span - class="" - > - USDC - </span> - </div> - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative" - data-testid="exchange-rate-display-switch" - style="mask-image: url('./images/icons/swap-horizontal.svg'); cursor: pointer;" - title="Switch" - /> - </div> -</div> -`; diff --git a/ui/pages/swaps/view-quote/index.js b/ui/pages/swaps/view-quote/index.js deleted file mode 100644 index 4a412aa905fe..000000000000 --- a/ui/pages/swaps/view-quote/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './view-quote'; diff --git a/ui/pages/swaps/view-quote/index.scss b/ui/pages/swaps/view-quote/index.scss deleted file mode 100644 index f96451cc6f4c..000000000000 --- a/ui/pages/swaps/view-quote/index.scss +++ /dev/null @@ -1,179 +0,0 @@ -@use "design-system"; - -.view-quote { - display: flex; - flex-flow: column; - align-items: center; - flex: 1; - width: 100%; - - &::after { // Hide preloaded images. - position: absolute; - width: 0; - height: 0; - overflow: hidden; - z-index: -1; - content: url('/images/transaction-background-top.svg') url('/images/transaction-background-bottom.svg'); // Preload images for the STX status page. - } - - &__content { - display: flex; - flex-flow: column; - align-items: center; - width: 100%; - height: 100%; - padding-left: 20px; - padding-right: 20px; - - &_modal > div:not(.view-quote__warning-wrapper) { - opacity: 0.6; - pointer-events: none; - } - - @include design-system.screen-sm-max { - overflow-y: auto; - max-height: 420px; - } - } - - @include design-system.screen-sm-min { - width: 348px; - } - - &__price-difference-warning { - &-wrapper { - width: 100%; - - &.low, - &.medium, - &.high { - .actionable-message { - &::before { - background: none; - } - - .actionable-message__message { - color: inherit; - } - - button { - font-size: design-system.$font-size-h8; - padding: 4px 12px; - border-radius: 42px; - } - } - } - - &.low { - .actionable-message { - button { - background: var(--color-primary-default); - color: var(--color-primary-inverse); - } - } - } - - &.medium { - .actionable-message { - border-color: var(--color-warning-default); - background: var(--color-warning-muted); - - button { - background: var(--color-warning-default); - } - } - } - - &.high { - .actionable-message { - border-color: var(--color-error-default); - background: var(--color-error-muted); - - button { - background: var(--color-error-default); - color: var(--color-error-inverse); - } - } - } - } - - &-contents { - display: flex; - text-align: left; - - &-title { - font-weight: bold; - } - - &-actions { - text-align: end; - padding-top: 10px; - } - - i { - margin-inline-start: 10px; - } - } - } - - &__warning-wrapper { - width: 100%; - align-items: center; - justify-content: center; - max-width: 340px; - margin-top: 8px; - margin-bottom: 8px; - - @include design-system.screen-sm-min { - &--thin { - min-height: 36px; - } - - display: flex; - } - } - - &__bold { - font-weight: bold; - } - - &__countdown-timer-container { - display: flex; - justify-content: center; - margin-top: 8px; - } - - &__fee-card-container { - display: flex; - align-items: center; - width: 100%; - max-width: 311px; - margin-bottom: 8px; - - @include design-system.screen-sm-min { - margin-bottom: 0; - } - } - - &__metamask-rate { - display: flex; - } - - &__metamask-rate-text { - @include design-system.H7; - - color: var(--color-text-alternative); - } - - &__metamask-rate-info-icon { - margin-left: 4px; - } - - &__thin-swaps-footer { - max-height: 82px; - - @include design-system.screen-sm-min { - height: 72px; - } - } -} diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.js b/ui/pages/swaps/view-quote/view-quote-price-difference.js deleted file mode 100644 index 2607243494d4..000000000000 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useContext } from 'react'; - -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { I18nContext } from '../../../contexts/i18n'; - -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import Tooltip from '../../../components/ui/tooltip'; -import Box from '../../../components/ui/box'; -import { - JustifyContent, - DISPLAY, -} from '../../../helpers/constants/design-system'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import { Icon, IconName } from '../../../components/component-library'; - -export default function ViewQuotePriceDifference(props) { - const { - usedQuote, - sourceTokenValue, - destinationTokenValue, - onAcknowledgementClick, - acknowledged, - priceSlippageFromSource, - priceSlippageFromDestination, - priceDifferencePercentage, - priceSlippageUnknownFiatValue, - } = props; - - const t = useContext(I18nContext); - - let priceDifferenceTitle = ''; - let priceDifferenceMessage = ''; - let priceDifferenceClass = ''; - let priceDifferenceAcknowledgementText = ''; - if (priceSlippageUnknownFiatValue) { - // A calculation error signals we cannot determine dollar value - priceDifferenceTitle = t('swapPriceUnavailableTitle'); - priceDifferenceMessage = t('swapPriceUnavailableDescription'); - priceDifferenceClass = GasRecommendations.high; - priceDifferenceAcknowledgementText = t('tooltipApproveButton'); - } else { - priceDifferenceTitle = t('swapPriceDifferenceTitle', [ - priceDifferencePercentage, - ]); - priceDifferenceMessage = t('swapPriceDifference', [ - sourceTokenValue, // Number of source token to swap - usedQuote.sourceTokenInfo.symbol, // Source token symbol - priceSlippageFromSource, // Source tokens total value - destinationTokenValue, // Number of destination tokens in return - usedQuote.destinationTokenInfo.symbol, // Destination token symbol, - priceSlippageFromDestination, // Destination tokens total value - ]); - priceDifferenceClass = usedQuote.priceSlippage.bucket; - priceDifferenceAcknowledgementText = t('tooltipApproveButton'); - } - - return ( - <div - className={classnames( - 'view-quote__price-difference-warning-wrapper', - priceDifferenceClass, - )} - > - <ActionableMessage - message={ - <div className="view-quote__price-difference-warning-contents"> - <div className="view-quote__price-difference-warning-contents-text"> - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.spaceBetween} - paddingBottom={2} - > - <div className="view-quote__price-difference-warning-contents-title"> - {priceDifferenceTitle} - </div> - <Tooltip position="bottom" title={t('swapPriceImpactTooltip')}> - <Icon name={IconName.Info} /> - </Tooltip> - </Box> - {priceDifferenceMessage} - {!acknowledged && ( - <div className="view-quote__price-difference-warning-contents-actions"> - <button - onClick={() => { - onAcknowledgementClick(); - }} - > - {priceDifferenceAcknowledgementText} - </button> - </div> - )} - </div> - </div> - } - /> - </div> - ); -} - -ViewQuotePriceDifference.propTypes = { - usedQuote: PropTypes.object, - sourceTokenValue: PropTypes.string, - destinationTokenValue: PropTypes.string, - onAcknowledgementClick: PropTypes.func, - acknowledged: PropTypes.bool, - priceSlippageFromSource: PropTypes.string, - priceSlippageFromDestination: PropTypes.string, - priceDifferencePercentage: PropTypes.number, - priceSlippageUnknownFiatValue: PropTypes.bool, -}; diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js deleted file mode 100644 index 5702f39f67fe..000000000000 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../test/lib/render-helpers'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import ViewQuotePriceDifference from './view-quote-price-difference'; - -describe('View Price Quote Difference', () => { - const mockState = { - metamask: { - tokens: [], - preferences: { showFiatInTestnets: true }, - currentCurrency: 'usd', - currencyRates: { - ETH: { - conversionRate: 600.0, - }, - }, - }, - }; - - const mockStore = configureMockStore()(mockState); - - // Sample transaction is 1 $ETH to ~42.880915 $LINK - const DEFAULT_PROPS = { - usedQuote: { - trade: { - data: '0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000007756e69737761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca0000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000024855454cb32d335f0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000005fc7b7100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f161421c8e0000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca', - from: '0xd7440fdcb70a9fba55dfe06942ddbc17679c90ac', - value: '0xde0b6b3a7640000', - gas: '0xbbfd0', - to: '0x881D40237659C251811CEC9c364ef91dC08D300C', - }, - sourceAmount: '1000000000000000000', - destinationAmount: '42947749216634160067', - error: null, - sourceToken: '0x0000000000000000000000000000000000000000', - destinationToken: '0x514910771af9ca656af840dff83e8264ecf986ca', - approvalNeeded: null, - maxGas: 770000, - averageGas: 210546, - estimatedRefund: 80000, - fetchTime: 647, - aggregator: 'uniswap', - aggType: 'DEX', - fee: 0.875, - gasMultiplier: 1.5, - priceSlippage: { - ratio: 1.007876641534847, - calculationError: '', - bucket: GasRecommendations.low, - sourceAmountInETH: 1, - destinationAmountInETH: 0.9921849150875727, - }, - slippage: 2, - sourceTokenInfo: { - symbol: 'ETH', - name: 'Ether', - address: '0x0000000000000000000000000000000000000000', - decimals: 18, - iconUrl: 'images/black-eth-logo.png', - }, - destinationTokenInfo: { - address: '0x514910771af9ca656af840dff83e8264ecf986ca', - symbol: 'LINK', - decimals: 18, - occurances: 12, - iconUrl: - 'https://cloudflare-ipfs.com/ipfs/QmQhZAdcZvW9T2tPm516yHqbGkfhyZwTZmLixW9MXJudTA', - }, - ethFee: '0.011791', - ethValueOfTokens: '0.99220724791716534441', - overallValueOfQuote: '0.98041624791716534441', - metaMaskFeeInEth: '0.00875844985551091729', - isBestQuote: true, - savings: { - performance: '0.00207907025112527799', - fee: '0.005581', - metaMaskFee: '0.00875844985551091729', - total: '-0.0010983796043856393', - medianMetaMaskFee: '0.00874009740688812165', - }, - }, - sourceTokenValue: '1', - destinationTokenValue: '42.947749', - }; - - it('should match snapshot', () => { - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...DEFAULT_PROPS} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays an error when in medium bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GasRecommendations.medium; - - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...props} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays an error when in high bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GasRecommendations.high; - - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...props} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays a fiat error when calculationError is present', () => { - const props = { ...DEFAULT_PROPS, priceSlippageUnknownFiatValue: true }; - props.usedQuote.priceSlippage.calculationError = - 'Could not determine price.'; - - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...props} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js deleted file mode 100644 index be02ba840eb5..000000000000 --- a/ui/pages/swaps/view-quote/view-quote.js +++ /dev/null @@ -1,1089 +0,0 @@ -import React, { - useState, - useContext, - useMemo, - useEffect, - useRef, - useCallback, -} from 'react'; -import { shallowEqual, useSelector, useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import BigNumber from 'bignumber.js'; -import { isEqual } from 'lodash'; -import classnames from 'classnames'; -import { captureException } from '@sentry/browser'; - -import { I18nContext } from '../../../contexts/i18n'; -import SelectQuotePopover from '../select-quote-popover'; -import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; -import { usePrevious } from '../../../hooks/usePrevious'; -import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import FeeCard from '../fee-card'; -import { - getQuotes, - getApproveTxParams, - getFetchParams, - setBalanceError, - getQuotesLastFetched, - getBalanceError, - getCustomSwapsGas, // Gas limit. - getCustomMaxFeePerGas, - getCustomMaxPriorityFeePerGas, - getSwapsUserFeeLevel, - getDestinationTokenInfo, - getUsedSwapsGasPrice, - getTopQuote, - getUsedQuote, - signAndSendTransactions, - getBackgroundSwapRouteState, - swapsQuoteSelected, - getSwapsQuoteRefreshTime, - getReviewSwapClickedTimestamp, - signAndSendSwapsSmartTransaction, - getSwapsNetworkConfig, - getSmartTransactionsError, - getCurrentSmartTransactionsError, - getSwapsSTXLoading, - fetchSwapsSmartTransactionFees, - getSmartTransactionFees, - getCurrentSmartTransactionsEnabled, -} from '../../../ducks/swaps/swaps'; -import { - conversionRateSelector, - getSelectedAccount, - getCurrentCurrency, - getTokenExchangeRates, - getSwapsDefaultToken, - getCurrentChainId, - isHardwareWallet, - getHardwareWalletType, - checkNetworkAndAccountSupports1559, - getUSDConversionRate, -} from '../../../selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; -import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; -import { - getLayer1GasFee, - safeRefetchQuotes, - setCustomApproveTxData, - setSwapsErrorKey, - showModal, - setSwapsQuotesPollingLimitEnabled, -} from '../../../store/actions'; -import { SET_SMART_TRANSACTIONS_ERROR } from '../../../store/actionConstants'; -import { - ASSET_ROUTE, - BUILD_QUOTE_ROUTE, - DEFAULT_ROUTE, - SWAPS_ERROR_ROUTE, - AWAITING_SWAP_ROUTE, -} from '../../../helpers/constants/routes'; -import MainQuoteSummary from '../main-quote-summary'; -import { getCustomTxParamsData } from '../../confirmations/confirm-approve/confirm-approve.util'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import { - quotesToRenderableData, - getRenderableNetworkFeesForQuote, - getFeeForSmartTransaction, -} from '../swaps.util'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { QUOTES_EXPIRED_ERROR } from '../../../../shared/constants/swaps'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import CountdownTimer from '../countdown-timer'; -import SwapsFooter from '../swaps-footer'; -import PulseLoader from '../../../components/ui/pulse-loader'; // TODO: Replace this with a different loading component. -import Box from '../../../components/ui/box'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { parseStandardTokenTransactionData } from '../../../../shared/modules/transaction.utils'; -import { getTokenValueParam } from '../../../../shared/lib/metamask-controller-utils'; -import { - calcGasTotal, - calcTokenAmount, - toPrecisionWithoutTrailingZeros, -} from '../../../../shared/lib/transactions-controller-utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { addHexPrefix } from '../../../../app/scripts/lib/util'; -import { - calcTokenValue, - calculateMaxGasLimit, -} from '../../../../shared/lib/swaps-utils'; -import { - addHexes, - decGWEIToHexWEI, - decimalToHex, - decWEIToDecETH, - hexWEIToDecGWEI, - sumHexes, -} from '../../../../shared/modules/conversion.utils'; -import ViewQuotePriceDifference from './view-quote-price-difference'; - -let intervalId; - -export default function ViewQuote() { - const history = useHistory(); - const dispatch = useDispatch(); - const t = useContext(I18nContext); - const trackEvent = useContext(MetaMetricsContext); - - const [dispatchedSafeRefetch, setDispatchedSafeRefetch] = useState(false); - const [submitClicked, setSubmitClicked] = useState(false); - const [selectQuotePopoverShown, setSelectQuotePopoverShown] = useState(false); - const [warningHidden, setWarningHidden] = useState(false); - const [originalApproveAmount, setOriginalApproveAmount] = useState(null); - const [multiLayerL1FeeTotal, setMultiLayerL1FeeTotal] = useState(null); - const [multiLayerL1ApprovalFeeTotal, setMultiLayerL1ApprovalFeeTotal] = - useState(null); - // We need to have currentTimestamp in state, otherwise it would change with each rerender. - const [currentTimestamp] = useState(Date.now()); - - const [acknowledgedPriceDifference, setAcknowledgedPriceDifference] = - useState(false); - const priceDifferenceRiskyBuckets = [ - GasRecommendations.high, - GasRecommendations.medium, - ]; - - const routeState = useSelector(getBackgroundSwapRouteState); - const quotes = useSelector(getQuotes, isEqual); - useEffect(() => { - if (!Object.values(quotes).length) { - history.push(BUILD_QUOTE_ROUTE); - } else if (routeState === 'awaiting') { - history.push(AWAITING_SWAP_ROUTE); - } - }, [history, quotes, routeState]); - - const quotesLastFetched = useSelector(getQuotesLastFetched); - - // Select necessary data - const gasPrice = useSelector(getUsedSwapsGasPrice); - const customMaxGas = useSelector(getCustomSwapsGas); - const customMaxFeePerGas = useSelector(getCustomMaxFeePerGas); - const customMaxPriorityFeePerGas = useSelector(getCustomMaxPriorityFeePerGas); - const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); - const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); - const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); - const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); - const conversionRate = useSelector(conversionRateSelector); - const USDConversionRate = useSelector(getUSDConversionRate); - const currentCurrency = useSelector(getCurrentCurrency); - const swapsTokens = useSelector(getTokens, isEqual); - const networkAndAccountSupports1559 = useSelector( - checkNetworkAndAccountSupports1559, - ); - const balanceError = useSelector(getBalanceError); - const fetchParams = useSelector(getFetchParams, isEqual); - const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = useSelector(getUsedQuote, isEqual); - const tradeValue = usedQuote?.trade?.value ?? '0x0'; - const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); - const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); - const chainId = useSelector(getCurrentChainId); - const nativeCurrencySymbol = useSelector(getNativeCurrency); - const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const swapsSTXLoading = useSelector(getSwapsSTXLoading); - const currentSmartTransactionsError = useSelector( - getCurrentSmartTransactionsError, - ); - const smartTransactionsError = useSelector(getSmartTransactionsError); - const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); - const unsignedTransaction = usedQuote.trade; - const isSmartTransaction = - currentSmartTransactionsEnabled && smartTransactionsOptInStatus; - - let gasFeeInputs; - if (networkAndAccountSupports1559) { - // For Swaps we want to get 'high' estimations by default. - // eslint-disable-next-line react-hooks/rules-of-hooks - gasFeeInputs = useGasFeeInputs(GasRecommendations.high, { - userFeeLevel: swapsUserFeeLevel || GasRecommendations.high, - }); - } - - const fetchParamsSourceToken = fetchParams?.sourceToken; - - const additionalTrackingParams = { - reg_tx_fee_in_usd: undefined, - reg_tx_fee_in_eth: undefined, - reg_tx_max_fee_in_usd: undefined, - reg_tx_max_fee_in_eth: undefined, - stx_fee_in_usd: undefined, - stx_fee_in_eth: undefined, - stx_max_fee_in_usd: undefined, - stx_max_fee_in_eth: undefined, - }; - - const usedGasLimit = - usedQuote?.gasEstimateWithRefund || - `0x${decimalToHex(usedQuote?.averageGas || 0)}`; - - const maxGasLimit = calculateMaxGasLimit( - usedQuote?.gasEstimate, - usedQuote?.gasMultiplier, - usedQuote?.maxGas, - customMaxGas, - ); - - let maxFeePerGas; - let maxPriorityFeePerGas; - let baseAndPriorityFeePerGas; - - // EIP-1559 gas fees. - if (networkAndAccountSupports1559) { - const { - maxFeePerGas: suggestedMaxFeePerGas, - maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas, - gasFeeEstimates, - } = gasFeeInputs; - const estimatedBaseFee = gasFeeEstimates?.estimatedBaseFee ?? '0'; - maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); - maxPriorityFeePerGas = - customMaxPriorityFeePerGas || - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas); - baseAndPriorityFeePerGas = addHexes( - decGWEIToHexWEI(estimatedBaseFee), - maxPriorityFeePerGas, - ); - } - let gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); - if (multiLayerL1FeeTotal !== null) { - gasTotalInWeiHex = sumHexes( - gasTotalInWeiHex || '0x0', - multiLayerL1FeeTotal || '0x0', - ); - } - - const { tokensWithBalances } = useTokenTracker({ - tokens: swapsTokens, - includeFailedTokens: true, - }); - const balanceToken = - fetchParamsSourceToken === defaultSwapsToken.address - ? defaultSwapsToken - : tokensWithBalances.find(({ address }) => - isEqualCaseInsensitive(address, fetchParamsSourceToken), - ); - - const selectedFromToken = balanceToken || usedQuote.sourceTokenInfo; - const tokenBalance = - tokensWithBalances?.length && - calcTokenAmount( - selectedFromToken.balance || '0x0', - selectedFromToken.decimals, - ).toFixed(9); - const tokenBalanceUnavailable = - tokensWithBalances && balanceToken === undefined; - - const approveData = parseStandardTokenTransactionData(approveTxParams?.data); - const approveValue = approveData && getTokenValueParam(approveData); - const approveAmount = - approveValue && - selectedFromToken?.decimals !== undefined && - calcTokenAmount(approveValue, selectedFromToken.decimals).toFixed(9); - const approveGas = approveTxParams?.gas; - - const renderablePopoverData = useMemo(() => { - return quotesToRenderableData({ - quotes, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, - conversionRate, - currentCurrency, - approveGas, - tokenConversionRates: memoizedTokenConversionRates, - chainId, - smartTransactionEstimatedGas: - smartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees, - nativeCurrencySymbol, - multiLayerL1ApprovalFeeTotal, - }); - }, [ - quotes, - gasPrice, - baseAndPriorityFeePerGas, - networkAndAccountSupports1559, - conversionRate, - currentCurrency, - approveGas, - memoizedTokenConversionRates, - chainId, - smartTransactionFees?.tradeTxFees, - nativeCurrencySymbol, - smartTransactionsEnabled, - smartTransactionsOptInStatus, - multiLayerL1ApprovalFeeTotal, - ]); - - const renderableDataForUsedQuote = renderablePopoverData.find( - (renderablePopoverDatum) => - renderablePopoverDatum.aggId === usedQuote.aggregator, - ); - - const { - destinationTokenDecimals, - destinationTokenSymbol, - destinationTokenValue, - destinationIconUrl, - sourceTokenDecimals, - sourceTokenSymbol, - sourceTokenValue, - sourceTokenIconUrl, - } = renderableDataForUsedQuote; - - let { feeInFiat, feeInEth, rawEthFee, feeInUsd } = - getRenderableNetworkFeesForQuote({ - tradeGas: usedGasLimit, - approveGas, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, - currentCurrency, - conversionRate, - USDConversionRate, - tradeValue, - sourceSymbol: sourceTokenSymbol, - sourceAmount: usedQuote.sourceAmount, - chainId, - nativeCurrencySymbol, - multiLayerL1FeeTotal, - }); - additionalTrackingParams.reg_tx_fee_in_usd = Number(feeInUsd); - additionalTrackingParams.reg_tx_fee_in_eth = Number(rawEthFee); - - const renderableMaxFees = getRenderableNetworkFeesForQuote({ - tradeGas: maxGasLimit, - approveGas, - gasPrice: maxFeePerGas || gasPrice, - currentCurrency, - conversionRate, - USDConversionRate, - tradeValue, - sourceSymbol: sourceTokenSymbol, - sourceAmount: usedQuote.sourceAmount, - chainId, - nativeCurrencySymbol, - multiLayerL1FeeTotal, - }); - let { - feeInFiat: maxFeeInFiat, - feeInEth: maxFeeInEth, - rawEthFee: maxRawEthFee, - feeInUsd: maxFeeInUsd, - } = renderableMaxFees; - additionalTrackingParams.reg_tx_max_fee_in_usd = Number(maxFeeInUsd); - additionalTrackingParams.reg_tx_max_fee_in_eth = Number(maxRawEthFee); - - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees - ) { - const stxEstimatedFeeInWeiDec = - smartTransactionFees?.tradeTxFees.feeEstimate + - (smartTransactionFees?.approvalTxFees?.feeEstimate || 0); - const stxMaxFeeInWeiDec = - stxEstimatedFeeInWeiDec * swapsNetworkConfig.stxMaxFeeMultiplier; - ({ feeInFiat, feeInEth, rawEthFee, feeInUsd } = getFeeForSmartTransaction({ - chainId, - currentCurrency, - conversionRate, - USDConversionRate, - nativeCurrencySymbol, - feeInWeiDec: stxEstimatedFeeInWeiDec, - })); - additionalTrackingParams.stx_fee_in_usd = Number(feeInUsd); - additionalTrackingParams.stx_fee_in_eth = Number(rawEthFee); - additionalTrackingParams.estimated_gas = - smartTransactionFees?.tradeTxFees.gasLimit; - ({ - feeInFiat: maxFeeInFiat, - feeInEth: maxFeeInEth, - rawEthFee: maxRawEthFee, - feeInUsd: maxFeeInUsd, - } = getFeeForSmartTransaction({ - chainId, - currentCurrency, - conversionRate, - USDConversionRate, - nativeCurrencySymbol, - feeInWeiDec: stxMaxFeeInWeiDec, - })); - additionalTrackingParams.stx_max_fee_in_usd = Number(maxFeeInUsd); - additionalTrackingParams.stx_max_fee_in_eth = Number(maxRawEthFee); - } - - const tokenCost = new BigNumber(usedQuote.sourceAmount); - const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus( - new BigNumber(gasTotalInWeiHex, 16), - ); - - const insufficientTokens = - (tokensWithBalances?.length || balanceError) && - tokenCost.gt(new BigNumber(selectedFromToken.balance || '0x0')); - - const insufficientEth = ethCost.gt(new BigNumber(ethBalance || '0x0')); - - const tokenBalanceNeeded = insufficientTokens - ? toPrecisionWithoutTrailingZeros( - calcTokenAmount(tokenCost, selectedFromToken.decimals) - .minus(tokenBalance) - .toString(10), - 6, - ) - : null; - - const ethBalanceNeeded = insufficientEth - ? toPrecisionWithoutTrailingZeros( - ethCost - .minus(ethBalance, 16) - .div('1000000000000000000', 10) - .toString(10), - 6, - ) - : null; - - let ethBalanceNeededStx; - if (isSmartTransaction && smartTransactionsError?.balanceNeededWei) { - ethBalanceNeededStx = decWEIToDecETH( - smartTransactionsError.balanceNeededWei - - smartTransactionsError.currentBalanceWei, - ); - } - - const destinationToken = useSelector(getDestinationTokenInfo, isEqual); - useEffect(() => { - if (isSmartTransaction) { - if (insufficientTokens) { - dispatch(setBalanceError(true)); - } else if (balanceError && !insufficientTokens) { - dispatch(setBalanceError(false)); - } - } else if (insufficientTokens || insufficientEth) { - dispatch(setBalanceError(true)); - } else if (balanceError && !insufficientTokens && !insufficientEth) { - dispatch(setBalanceError(false)); - } - }, [ - insufficientTokens, - insufficientEth, - balanceError, - dispatch, - isSmartTransaction, - ]); - - useEffect(() => { - const currentTime = Date.now(); - const timeSinceLastFetched = currentTime - quotesLastFetched; - if ( - timeSinceLastFetched > swapsQuoteRefreshTime && - !dispatchedSafeRefetch - ) { - setDispatchedSafeRefetch(true); - dispatch(safeRefetchQuotes()); - } else if (timeSinceLastFetched > swapsQuoteRefreshTime) { - dispatch(setSwapsErrorKey(QUOTES_EXPIRED_ERROR)); - history.push(SWAPS_ERROR_ROUTE); - } - }, [ - quotesLastFetched, - dispatchedSafeRefetch, - dispatch, - history, - swapsQuoteRefreshTime, - ]); - - useEffect(() => { - if (!originalApproveAmount && approveAmount) { - setOriginalApproveAmount(approveAmount); - } - }, [originalApproveAmount, approveAmount]); - - // If it's not a Smart Transaction and ETH balance is needed, we want to show a warning. - const isNotStxAndEthBalanceIsNeeded = - (!currentSmartTransactionsEnabled || !smartTransactionsOptInStatus) && - ethBalanceNeeded; - - // If it's a Smart Transaction and ETH balance is needed, we want to show a warning. - const isStxAndEthBalanceIsNeeded = isSmartTransaction && ethBalanceNeededStx; - - // Indicates if we should show to a user a warning about insufficient funds for swapping. - const showInsufficientWarning = - (balanceError || - tokenBalanceNeeded || - isNotStxAndEthBalanceIsNeeded || - isStxAndEthBalanceIsNeeded) && - !warningHidden; - - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - - const numberOfQuotes = Object.values(quotes).length; - const bestQuoteReviewedEventSent = useRef(); - const eventObjectBase = useMemo(() => { - return { - token_from: sourceTokenSymbol, - token_from_amount: sourceTokenValue, - token_to: destinationTokenSymbol, - token_to_amount: destinationTokenValue, - request_type: fetchParams?.balanceError, - slippage: fetchParams?.slippage, - custom_slippage: fetchParams?.slippage !== 2, - response_time: fetchParams?.responseTime, - best_quote_source: topQuote?.aggregator, - available_quotes: numberOfQuotes, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }; - }, [ - sourceTokenSymbol, - sourceTokenValue, - destinationTokenSymbol, - destinationTokenValue, - fetchParams?.balanceError, - fetchParams?.slippage, - fetchParams?.responseTime, - topQuote?.aggregator, - numberOfQuotes, - hardwareWalletUsed, - hardwareWalletType, - smartTransactionsEnabled, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - ]); - - const trackAllAvailableQuotesOpened = () => { - trackEvent({ - event: 'All Available Quotes Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - other_quote_selected: usedQuote?.aggregator !== topQuote?.aggregator, - other_quote_selected_source: - usedQuote?.aggregator === topQuote?.aggregator - ? null - : usedQuote?.aggregator, - }, - }); - }; - const trackQuoteDetailsOpened = () => { - trackEvent({ - event: 'Quote Details Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - other_quote_selected: usedQuote?.aggregator !== topQuote?.aggregator, - other_quote_selected_source: - usedQuote?.aggregator === topQuote?.aggregator - ? null - : usedQuote?.aggregator, - }, - }); - }; - const trackEditSpendLimitOpened = () => { - trackEvent({ - event: 'Edit Spend Limit Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - custom_spend_limit_set: originalApproveAmount === approveAmount, - custom_spend_limit_amount: - originalApproveAmount === approveAmount ? null : approveAmount, - }, - }); - }; - const trackBestQuoteReviewedEvent = useCallback(() => { - trackEvent({ - event: 'Best Quote Reviewed', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - network_fees: feeInFiat, - }, - }); - }, [trackEvent, eventObjectBase, feeInFiat]); - const trackViewQuotePageLoadedEvent = useCallback(() => { - trackEvent({ - event: 'View Quote Page Loaded', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - response_time: currentTimestamp - reviewSwapClickedTimestamp, - }, - }); - }, [ - trackEvent, - eventObjectBase, - currentTimestamp, - reviewSwapClickedTimestamp, - ]); - - useEffect(() => { - if ( - !bestQuoteReviewedEventSent.current && - [ - sourceTokenSymbol, - sourceTokenValue, - destinationTokenSymbol, - destinationTokenValue, - fetchParams, - topQuote, - numberOfQuotes, - feeInFiat, - ].every((dep) => dep !== null && dep !== undefined) - ) { - bestQuoteReviewedEventSent.current = true; - trackBestQuoteReviewedEvent(); - } - }, [ - fetchParams, - topQuote, - numberOfQuotes, - feeInFiat, - destinationTokenSymbol, - destinationTokenValue, - sourceTokenSymbol, - sourceTokenValue, - trackBestQuoteReviewedEvent, - ]); - - const metaMaskFee = usedQuote.fee; - - /* istanbul ignore next */ - const onFeeCardTokenApprovalClick = () => { - trackEditSpendLimitOpened(); - dispatch( - showModal({ - name: 'EDIT_APPROVAL_PERMISSION', - decimals: selectedFromToken.decimals, - origin: 'MetaMask', - setCustomAmount: (newCustomPermissionAmount) => { - const customPermissionAmount = - newCustomPermissionAmount === '' - ? originalApproveAmount - : newCustomPermissionAmount; - const newData = getCustomTxParamsData(approveTxParams.data, { - customPermissionAmount, - decimals: selectedFromToken.decimals, - }); - - if ( - customPermissionAmount?.length && - approveTxParams.data !== newData - ) { - dispatch(setCustomApproveTxData(newData)); - } - }, - tokenAmount: originalApproveAmount, - customTokenAmount: - originalApproveAmount === approveAmount ? null : approveAmount, - tokenBalance, - tokenSymbol: selectedFromToken.symbol, - requiredMinimum: calcTokenAmount( - usedQuote.sourceAmount, - selectedFromToken.decimals, - ), - }), - ); - }; - const actionableBalanceErrorMessage = tokenBalanceUnavailable - ? t('swapTokenBalanceUnavailable', [sourceTokenSymbol]) - : t('swapApproveNeedMoreTokens', [ - <span key="swapApproveNeedMoreTokens-1" className="view-quote__bold"> - {tokenBalanceNeeded || ethBalanceNeededStx || ethBalanceNeeded} - </span>, - tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) - ? sourceTokenSymbol - : defaultSwapsToken.symbol, - ]); - - // Price difference warning - const priceSlippageBucket = usedQuote?.priceSlippage?.bucket; - const lastPriceDifferenceBucket = usePrevious(priceSlippageBucket); - - // If the user agreed to a different bucket of risk, make them agree again - useEffect(() => { - if ( - acknowledgedPriceDifference && - lastPriceDifferenceBucket === GasRecommendations.medium && - priceSlippageBucket === GasRecommendations.high - ) { - setAcknowledgedPriceDifference(false); - } - }, [ - priceSlippageBucket, - acknowledgedPriceDifference, - lastPriceDifferenceBucket, - ]); - - let viewQuotePriceDifferenceComponent = null; - const priceSlippageFromSource = useEthFiatAmount( - usedQuote?.priceSlippage?.sourceAmountInETH || 0, - { showFiat: true }, - ); - const priceSlippageFromDestination = useEthFiatAmount( - usedQuote?.priceSlippage?.destinationAmountInETH || 0, - { showFiat: true }, - ); - - // We cannot present fiat value if there is a calculation error or no slippage - // from source or destination - const priceSlippageUnknownFiatValue = - !priceSlippageFromSource || - !priceSlippageFromDestination || - Boolean(usedQuote?.priceSlippage?.calculationError); - - let priceDifferencePercentage = 0; - if (usedQuote?.priceSlippage?.ratio) { - priceDifferencePercentage = parseFloat( - new BigNumber(usedQuote.priceSlippage.ratio, 10) - .minus(1, 10) - .times(100, 10) - .toFixed(2), - 10, - ); - } - - const shouldShowPriceDifferenceWarning = - !tokenBalanceUnavailable && - !showInsufficientWarning && - usedQuote && - (priceDifferenceRiskyBuckets.includes(priceSlippageBucket) || - priceSlippageUnknownFiatValue); - - if (shouldShowPriceDifferenceWarning) { - viewQuotePriceDifferenceComponent = ( - <ViewQuotePriceDifference - usedQuote={usedQuote} - sourceTokenValue={sourceTokenValue} - destinationTokenValue={destinationTokenValue} - priceSlippageFromSource={priceSlippageFromSource} - priceSlippageFromDestination={priceSlippageFromDestination} - priceDifferencePercentage={priceDifferencePercentage} - priceSlippageUnknownFiatValue={priceSlippageUnknownFiatValue} - onAcknowledgementClick={() => { - setAcknowledgedPriceDifference(true); - }} - acknowledged={acknowledgedPriceDifference} - /> - ); - } - - const disableSubmissionDueToPriceWarning = - shouldShowPriceDifferenceWarning && !acknowledgedPriceDifference; - - const isShowingWarning = - showInsufficientWarning || shouldShowPriceDifferenceWarning; - - const isSwapButtonDisabled = Boolean( - submitClicked || - balanceError || - tokenBalanceUnavailable || - disableSubmissionDueToPriceWarning || - (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || - (!networkAndAccountSupports1559 && - (gasPrice === null || gasPrice === undefined)) || - (currentSmartTransactionsEnabled && - (currentSmartTransactionsError || smartTransactionsError)) || - (currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees), - ); - - useEffect(() => { - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !insufficientTokens - ) { - const unsignedTx = { - from: unsignedTransaction.from, - to: unsignedTransaction.to, - value: unsignedTransaction.value, - data: unsignedTransaction.data, - gas: unsignedTransaction.gas, - chainId, - }; - intervalId = setInterval(() => { - if (!swapsSTXLoading) { - dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction: unsignedTx, - approveTxParams, - fallbackOnNotEnoughFunds: false, - }), - ); - } - }, swapsNetworkConfig.stxGetTransactionsRefreshTime); - dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction: unsignedTx, - approveTxParams, - fallbackOnNotEnoughFunds: false, - }), - ); - } else if (intervalId) { - clearInterval(intervalId); - } - return () => clearInterval(intervalId); - // eslint-disable-next-line - }, [ - dispatch, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - unsignedTransaction.data, - unsignedTransaction.from, - unsignedTransaction.value, - unsignedTransaction.gas, - unsignedTransaction.to, - chainId, - swapsNetworkConfig.stxGetTransactionsRefreshTime, - insufficientTokens, - ]); - - useEffect(() => { - // Thanks to the next line we will only do quotes polling 3 times before showing a Quote Timeout modal. - dispatch(setSwapsQuotesPollingLimitEnabled(true)); - if (reviewSwapClickedTimestamp) { - trackViewQuotePageLoadedEvent(); - } - }, [dispatch, trackViewQuotePageLoadedEvent, reviewSwapClickedTimestamp]); - - useEffect(() => { - // if smart transaction error is turned off, reset submit clicked boolean - if ( - !currentSmartTransactionsEnabled && - currentSmartTransactionsError && - submitClicked - ) { - setSubmitClicked(false); - } - }, [ - currentSmartTransactionsEnabled, - currentSmartTransactionsError, - submitClicked, - ]); - - useEffect(() => { - if (!usedQuote?.multiLayerL1TradeFeeTotal) { - return; - } - const getEstimatedL1Fees = async () => { - try { - let l1ApprovalFeeTotal = '0x0'; - if (approveTxParams) { - l1ApprovalFeeTotal = await dispatch( - getLayer1GasFee({ - transactionParams: { - ...approveTxParams, - gasPrice: addHexPrefix(approveTxParams.gasPrice), - value: '0x0', // For approval txs we need to use "0x0" here. - }, - chainId, - }), - ); - setMultiLayerL1ApprovalFeeTotal(l1ApprovalFeeTotal); - } - const l1FeeTotal = sumHexes( - usedQuote.multiLayerL1TradeFeeTotal, - l1ApprovalFeeTotal, - ); - setMultiLayerL1FeeTotal(l1FeeTotal); - } catch (e) { - captureException(e); - setMultiLayerL1FeeTotal(null); - setMultiLayerL1ApprovalFeeTotal(null); - } - }; - getEstimatedL1Fees(); - }, [unsignedTransaction, approveTxParams, chainId, usedQuote]); - - useEffect(() => { - if (isSmartTransaction) { - // Removes a smart transactions error when the component loads. - dispatch({ - type: SET_SMART_TRANSACTIONS_ERROR, - payload: null, - }); - } - }, [isSmartTransaction, dispatch]); - - return ( - <div className="view-quote"> - <div - className={classnames('view-quote__content', { - 'view-quote__content_modal': disableSubmissionDueToPriceWarning, - })} - > - { - /* istanbul ignore next */ - selectQuotePopoverShown && ( - <SelectQuotePopover - quoteDataRows={renderablePopoverData} - onClose={() => setSelectQuotePopoverShown(false)} - onSubmit={(aggId) => dispatch(swapsQuoteSelected(aggId))} - swapToSymbol={destinationTokenSymbol} - initialAggId={usedQuote.aggregator} - onQuoteDetailsIsOpened={trackQuoteDetailsOpened} - hideEstimatedGasFee={ - smartTransactionsEnabled && smartTransactionsOptInStatus - } - /> - ) - } - - <div - className={classnames('view-quote__warning-wrapper', { - 'view-quote__warning-wrapper--thin': !isShowingWarning, - })} - > - {viewQuotePriceDifferenceComponent} - {(showInsufficientWarning || tokenBalanceUnavailable) && ( - <ActionableMessage - message={actionableBalanceErrorMessage} - onClose={ - /* istanbul ignore next */ - () => setWarningHidden(true) - } - /> - )} - </div> - <div className="view-quote__countdown-timer-container"> - <CountdownTimer - timeStarted={quotesLastFetched} - warningTime="0:10" - labelKey="swapNewQuoteIn" - /> - </div> - <MainQuoteSummary - sourceValue={calcTokenValue(sourceTokenValue, sourceTokenDecimals)} - sourceDecimals={sourceTokenDecimals} - sourceSymbol={sourceTokenSymbol} - destinationValue={calcTokenValue( - destinationTokenValue, - destinationTokenDecimals, - )} - destinationDecimals={destinationTokenDecimals} - destinationSymbol={destinationTokenSymbol} - sourceIconUrl={sourceTokenIconUrl} - destinationIconUrl={destinationIconUrl} - /> - {currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees && - !showInsufficientWarning && ( - <Box marginTop={0} marginBottom={10}> - <PulseLoader /> - </Box> - )} - {(!currentSmartTransactionsEnabled || - !smartTransactionsOptInStatus || - smartTransactionFees?.tradeTxFees) && ( - <div - className={classnames('view-quote__fee-card-container', { - 'view-quote__fee-card-container--three-rows': - approveTxParams && (!balanceError || warningHidden), - })} - > - <FeeCard - primaryFee={{ - fee: feeInEth, - maxFee: maxFeeInEth, - }} - secondaryFee={{ - fee: feeInFiat, - maxFee: maxFeeInFiat, - }} - hideTokenApprovalRow={ - !approveTxParams || (balanceError && !warningHidden) - } - tokenApprovalSourceTokenSymbol={sourceTokenSymbol} - onTokenApprovalClick={onFeeCardTokenApprovalClick} - metaMaskFee={String(metaMaskFee)} - numberOfQuotes={Object.values(quotes).length} - onQuotesClick={ - /* istanbul ignore next */ - () => { - trackAllAvailableQuotesOpened(); - setSelectQuotePopoverShown(true); - } - } - maxPriorityFeePerGasDecGWEI={hexWEIToDecGWEI( - maxPriorityFeePerGas, - )} - maxFeePerGasDecGWEI={hexWEIToDecGWEI(maxFeePerGas)} - /> - </div> - )} - </div> - <SwapsFooter - onSubmit={ - /* istanbul ignore next */ () => { - setSubmitClicked(true); - if (!balanceError) { - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees - ) { - dispatch( - signAndSendSwapsSmartTransaction({ - unsignedTransaction, - trackEvent, - history, - additionalTrackingParams, - }), - ); - } else { - dispatch( - signAndSendTransactions( - history, - trackEvent, - additionalTrackingParams, - ), - ); - } - } else if (destinationToken.symbol === defaultSwapsToken.symbol) { - history.push(DEFAULT_ROUTE); - } else { - history.push(`${ASSET_ROUTE}/${destinationToken.address}`); - } - } - } - submitText={ - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - swapsSTXLoading - ? t('preparingSwap') - : t('swap') - } - hideCancel - disabled={isSwapButtonDisabled} - className={isShowingWarning ? 'view-quote__thin-swaps-footer' : ''} - showTopBorder - /> - </div> - ); -} diff --git a/ui/pages/swaps/view-quote/view-quote.test.js b/ui/pages/swaps/view-quote/view-quote.test.js deleted file mode 100644 index 3193a54dfb35..000000000000 --- a/ui/pages/swaps/view-quote/view-quote.test.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { NetworkType } from '@metamask/controller-utils'; -import { NetworkStatus } from '@metamask/network-controller'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - renderWithProvider, - createSwapsMockStore, - MOCKS, -} from '../../../../test/jest'; - -import ViewQuote from '.'; - -jest.mock( - '../../../components/ui/info-tooltip/info-tooltip-icon', - () => () => '<InfoTooltipIcon />', -); - -jest.mock('../../confirmations/hooks/useGasFeeInputs', () => { - return { - useGasFeeInputs: () => { - return { - maxFeePerGas: 16, - maxPriorityFeePerGas: 3, - gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(), - }; - }, - }; -}); - -const middleware = [thunk]; -const createProps = (customProps = {}) => { - return { - inputValue: '5', - onInputChange: jest.fn(), - ethBalance: '6 ETH', - setMaxSlippage: jest.fn(), - maxSlippage: 15, - selectedAccountAddress: 'selectedAccountAddress', - isFeatureFlagLoaded: false, - ...customProps, - }; -}; - -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - safeRefetchQuotes: jest.fn(), - setSwapsErrorKey: jest.fn(), - updateTransaction: jest.fn(), - getGasFeeTimeEstimate: jest.fn(), - setSwapsQuotesPollingLimitEnabled: jest.fn(), -}); - -describe('ViewQuote', () => { - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - <ViewQuote {...props} />, - store, - ); - expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot(); - expect( - getByTestId('main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - expect(getByText('Estimated gas fee')).toBeInTheDocument(); - expect(getByText('0.00008 ETH')).toBeInTheDocument(); - expect(getByText('Max fee')).toBeInTheDocument(); - expect(getByText('Swap')).toBeInTheDocument(); - }); - - it('renders the component with EIP-1559 enabled', () => { - const state = createSwapsMockStore(); - state.metamask.selectedNetworkClientId = NetworkType.mainnet; - state.metamask.networksMetadata = { - [NetworkType.mainnet]: { - EIPS: { 1559: true }, - status: NetworkStatus.Available, - }, - }; - const store = configureMockStore(middleware)(state); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - <ViewQuote {...props} />, - store, - ); - expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot(); - expect( - getByTestId('main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - expect(getByText('Estimated gas fee')).toBeInTheDocument(); - expect(getByText('0.00008 ETH')).toBeInTheDocument(); - expect(getByText('Max fee')).toBeInTheDocument(); - expect(getByText('Swap')).toBeInTheDocument(); - }); -}); From 42e5eab8c1cd1be84b0b1d3663a996c4f7b30ff3 Mon Sep 17 00:00:00 2001 From: Howard Braham <howrad@gmail.com> Date: Tue, 15 Oct 2024 15:13:19 -0700 Subject: [PATCH 154/226] fix: SENTRY_DSN_FAKE problem (#27881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixing this problem: https://github.com/MetaMask/metamask-extension/pull/27548/files#r1801845403 <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27881?quickstart=1) ## **Related issues** ## **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 - [ ] 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. --- app/scripts/lib/setupSentry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index d440578144cc..627f46fa79fa 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -238,8 +238,8 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - !getManifestFlags().sentry?.forceEnable || - (process.env.IN_TEST && !SENTRY_DSN_DEV) + process.env.IN_TEST && + (!SENTRY_DSN_DEV || !getManifestFlags().sentry?.forceEnable) ) { return SENTRY_DSN_FAKE; } From 3f69851404c12f654dffd92f84cae98e4ccef325 Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Wed, 16 Oct 2024 10:42:39 +0200 Subject: [PATCH 155/226] test(mock-e2e): add private domains logic for the privacy report (#27844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Introduce the concept of "private domains" for the `privacy-snapshot.json`. This allow to hide some part of a host [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27844?quickstart=1) ## **Related issues** Required by: - https://github.com/MetaMask/metamask-extension/pull/27730 ## **Manual testing steps** 1. Use this PR to test the feature: - https://github.com/MetaMask/metamask-extension/pull/27730 2. `yarn build:test:flask` 3. Remove the this line https://github.com/MetaMask/metamask-extension/blob/d5715503202bfaf451f60a6392e48366291942f7/privacy-snapshot.json#L2 4. `yarn test:e2e:single test/e2e/flask/btc/btc-account-overview.spec.ts --browser=chrome --update-privacy-snapshot` 5. The `privacy-snapshot.json` should have been updated again with the line you just removed ## **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. --- test/e2e/mock-e2e.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 209777f32bd7..1d0783b82624 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -66,6 +66,18 @@ const emptyHtmlPage = () => `<!DOCTYPE html> const browserAPIRequestDomains = /^.*\.(googleapis\.com|google\.com|mozilla\.net|mozilla\.com|mozilla\.org|gvt1\.com)$/iu; +/** + * Some third-party providers might use random URLs that we don't want to track + * in the privacy report "in clear". We identify those private hosts with a + * `pattern` regexp and replace the original host by a more generic one (`host`). + * For example, "my-secret-host.provider.com" could be denoted as "*.provider.com" in + * the privacy report. This would prevent disclosing the "my-secret-host" subdomain + * in this case. + */ +const privateHostMatchers = [ + // { pattern: RegExp, host: string } +]; + /** * @typedef {import('mockttp').Mockttp} Mockttp * @typedef {import('mockttp').MockedEndpoint} MockedEndpoint @@ -712,6 +724,25 @@ async function setupMocking( const portfolioRequestsMatcher = (request) => request.headers.referer === 'https://portfolio.metamask.io/'; + /** + * Tests a request against private domains and returns a set of generic hostnames that + * match. + * + * @param request + * @returns A set of matched results. + */ + const matchPrivateHosts = (request) => { + const privateHosts = new Set(); + + for (const { pattern, host: privateHost } of privateHostMatchers) { + if (request.headers.host.match(pattern)) { + privateHosts.add(privateHost); + } + } + + return privateHosts; + }; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -721,6 +752,16 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { + const privateHosts = matchPrivateHosts(request); + if (privateHosts.size) { + for (const privateHost of privateHosts) { + privacyReport.add(privateHost); + } + // At this point, we know the request at least one private doamin, so we just stops here to avoid + // using the request any further. + return; + } + if ( request.headers.host.match(browserAPIRequestDomains) === null && !portfolioRequestsMatcher(request) From bb2e2a997e6a4ef4c07a6680f424b8be4d42f887 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:45:04 +0200 Subject: [PATCH 156/226] fix: flaky test `Snap Account Signatures and Disconnects can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)` (#27887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** After connecting to the test dapp we are automatically switched to Mainnet (this is a recent change, didn't happen before). Then there is a race condition where the network the dapp sees is different from the wallet one, causing a miss-match and making the signature fail never opening the dialog. This seems an issue on the wallet side, as we should preserve the selected network after connecting, as we used to do before. I've opened an issue for that [here](https://github.com/MetaMask/metamask-extension/issues/27891). This just fixes the issue on the e2e side, but it needs to be fixed on the wallet side and remove the extra step again once the issue is fixed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27887?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27892 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** See how you are automatically switched to mainnet https://github.com/user-attachments/assets/d8fa53f5-b207-46bc-8e54-073245eff7b7 ## **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/accounts/common.ts | 45 +++++++++++++++++++++++-------------- test/e2e/helpers.js | 5 +---- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index eda4ef5fbf6f..62f3fc082b53 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -10,7 +10,6 @@ import { unlockWallet, validateContractDetails, multipleGanacheOptions, - regularDelayMs, } from '../helpers'; import { Driver } from '../webdriver/driver'; import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; @@ -67,23 +66,16 @@ export async function installSnapSimpleKeyring( await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); - await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); - // Wait until popup is closed before proceeding - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); await driver.waitForSelector({ @@ -159,7 +151,7 @@ export async function makeNewAccountAndSwitch(driver: Driver) { text: 'Add account', }); // Click the ok button on the success modal - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ css: '[data-testid="confirmation-submit-button"]', text: 'Ok', }); @@ -196,17 +188,40 @@ async function switchToAccount2(driver: Driver) { export async function connectAccountToTestDapp(driver: Driver) { await switchToOrOpenDapp(driver); - await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElementAndWaitForWindowToClose({ + + // 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) { @@ -296,12 +311,8 @@ export async function signData( }, async () => { await switchToOrOpenDapp(driver); - await driver.clickElement(locatorID); - // take extra time to load the popup - await driver.delay(500); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); }, ); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 65a405f5325d..564f99f2cde6 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -1208,10 +1208,7 @@ async function tempToggleSettingRedesignedConfirmations(driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); + await driver.clickElement('[data-testid="account-options-menu-button"]'); // fix race condition with mmi build if (process.env.MMI) { From 0edfb4881eda0abd83565ec6e61ab8ecea79ee28 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:54:11 +0200 Subject: [PATCH 157/226] fix: flaky test `Wallet Revoke Permissions should revoke eth_accounts permissions via test dapp` (#27894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** The problem is that this test is utilizing a wrong pattern where we are asserting the element by its text, instead of wait for the exact selector we want by text. When when it's trying to assert the text, sometimes the value is empty. ![Screenshot from 2024-10-16 11-28-45](https://github.com/user-attachments/assets/846dcaaa-e15a-4265-87b8-578c820f6569) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27894?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27722 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../revoke-permissions.spec.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js index 14d2af60bdbc..696095e3fd79 100644 --- a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, openDapp, @@ -25,12 +24,10 @@ describe('Wallet Revoke Permissions', function () { // Get initial accounts permissions await driver.clickElement('#getPermissions'); - const permissionsResult = await driver.findElement( - '#permissionsResult', - ); - - // Eth_accounts permission - assert.equal(await permissionsResult.getText(), 'eth_accounts'); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'eth_accounts', + }); // Revoke eth_accounts permissions await driver.clickElement('#revokeAccountsPermission'); @@ -39,10 +36,10 @@ describe('Wallet Revoke Permissions', function () { await driver.clickElement('#getPermissions'); // Eth_accounts permissions removed - assert.equal( - await permissionsResult.getText(), - 'No permissions found.', - ); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'No permissions found.', + }); }, ); }); From 95045ed8f6ba8c06777c717df264257c310f5a2e Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:54:20 +0200 Subject: [PATCH 158/226] fix: flaky test `ERC721 NFTs testdapp interaction should prompt users to add their NFTs to their wallet (all at once)` (#27889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** The problem is that we are looking for the Deposit transaction by its text in the activity tab, but this element updates its state, from `pending`to `confirm`, meaning that it can become stale when we do the assertion after (see video). ``` await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); assert.equal(await transactionItem.isDisplayed(), true); ``` To mitigate this problem, we wait until the transaction is confirmed, and then look for the Deposit tx. This not only fixes the race condition, but it also ensures that the tx is successful. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ![Screenshot from 2024-10-16 08-35-56](https://github.com/user-attachments/assets/db176ec1-b71c-4019-bc67-7a4714c2c17b) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27889?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/26759 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** https://github.com/user-attachments/assets/0e463ddf-f744-4428-8472-0ec2a585171e ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. --- .../tokens/nft/erc721-interaction.spec.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index c2d3b7a8ce82..9ebc247ea795 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -225,25 +225,30 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement({ text: 'Mint', tag: 'button' }); // Notification - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); 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.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({ + + // We need to wait until the transaction is confirmed before looking for the tx + // otherwise the element becomes stale, as it updates from 'pending' to 'confirmed' + await driver.waitForSelector('.transaction-status-label--confirmed'); + + 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({ @@ -255,7 +260,6 @@ describe('ERC721 NFTs testdapp interaction', function () { // watch all nfts await driver.clickElement({ text: 'Watch all NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirm watchNFT @@ -277,8 +281,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); await removeButtons[0].click(); - await driver.clickElement({ text: 'Add NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Add NFTs', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, From ec4fb5fcefd62980057dfee5da8bd3aa3627dd35 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:54:45 +0200 Subject: [PATCH 159/226] fix: flaky test `Permissions sets permissions and connect to Dapp` (#27888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** We proceed switching to the Extension full view without waiting until the dialog is closed. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27888?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27890 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../dapp-interactions/permissions.spec.js | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/test/e2e/tests/dapp-interactions/permissions.spec.js b/test/e2e/tests/dapp-interactions/permissions.spec.js index adf3b809a656..4b6c210f0a98 100644 --- a/test/e2e/tests/dapp-interactions/permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/permissions.spec.js @@ -28,19 +28,15 @@ describe('Permissions', function () { tag: 'button', }); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - await driver.clickElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', }); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // shows connected sites await driver.clickElement( @@ -64,21 +60,17 @@ describe('Permissions', function () { assert.equal(domains.length, 1); // can get accounts within the dapp - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement({ text: 'eth_accounts', tag: 'button', }); - const getAccountsResult = await driver.waitForSelector({ + await driver.waitForSelector({ css: '#getAccountsResult', text: publicAddress, }); - assert.equal( - (await getAccountsResult.getText()).toLowerCase(), - publicAddress.toLowerCase(), - ); }, ); }); 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 160/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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); ``` <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 161/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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 <chloe.gao@consensys.net> 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<void> => { + 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<void> => { + 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<void> => { + 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<void> => { + 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<void> => { + 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<void> { + 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<void> { 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<void> { @@ -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<void> { + ): Promise<string> { 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<void> { - 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<void> { 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<void> { + 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<void> { 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 <jiexiluan@gmail.com> Date: Wed, 16 Oct 2024 06:10:47 -0700 Subject: [PATCH 162/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **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<EditAccountsModalProps> = ({ isPinned={Boolean(account.pinned)} startAccessory={ <Checkbox - isChecked={selectedAccountAddresses.includes( - account.address, + isChecked={selectedAccountAddresses.some( + (selectedAccountAddress) => + 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<SiteCellProps> = ({ 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`] = ` </div> </div> `; + +exports[`ConnectPage should render with defaults from the requested permissions 1`] = ` +<div> + <div + class="mm-box multichain-page mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--width-full mm-box--height-full mm-box--background-color-background-alternative" + > + <div + class="mm-box multichain-page__inner-container main-container connect-page mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--height-full mm-box--background-color-background-alternative" + data-testid="connect-page" + > + <div + class="mm-box mm-header-base multichain-page-header mm-box--padding-4 mm-box--padding-bottom-0 mm-box--display-flex mm-box--justify-content-center mm-box--width-full" + > + <div + class="mm-box" + > + <p + class="mm-box mm-text mm-text--body-md-bold mm-text--ellipsis mm-text--text-align-center mm-box--padding-inline-start-8 mm-box--padding-inline-end-8 mm-box--display-block mm-box--color-text-default" + > + <h2 + class="mm-box mm-text mm-text--heading-lg mm-box--color-text-default" + > + Connect with MetaMask + </h2> + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + This site wants to + : + </p> + </p> + </div> + </div> + <div + class="mm-box multichain-page-content mm-box--padding-4 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--height-full" + > + <div + class="mm-box mm-box--padding-4 mm-box--gap-4 mm-box--background-color-background-default mm-box--rounded-lg" + > + <div + class="mm-box multichain-connection-list-item mm-box--padding-top-0 mm-box--padding-bottom-2 mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-row mm-box--align-items-baseline mm-box--width-full mm-box--background-color-background-default" + data-testid="site-cell-connection-list-item" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-icon mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-alternative mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" + > + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/wallet.svg');" + /> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column mm-box--width-5/12" + style="align-self: center; flex-grow: 1;" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--text-align-left mm-box--color-text-default" + > + See your accounts and suggest transactions + </p> + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" + > + <span + class="mm-box mm-text mm-text--body-sm mm-text--ellipsis mm-box--width-max mm-box--color-text-alternative" + > + Requesting for Test Account + </span> + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-account mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" + > + <div + class="mm-avatar-account__jazzicon" + > + <div + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 16px; height: 16px; display: inline-block; background: rgb(250, 58, 0);" + > + <svg + height="16" + width="16" + x="0" + y="0" + > + <rect + fill="#18CDF2" + height="16" + transform="translate(-0.52419675189697 -1.6521420347302493) rotate(328.9 8 8)" + width="16" + x="0" + y="0" + /> + <rect + fill="#035E56" + height="16" + transform="translate(-9.149230854416022 5.2962309358743) rotate(176.2 8 8)" + width="16" + x="0" + y="0" + /> + <rect + fill="#F26602" + height="16" + transform="translate(8.333921009111961 -7.102569861498541) rotate(468.9 8 8)" + width="16" + x="0" + y="0" + /> + </svg> + </div> + </div> + </div> + </div> + </div> + <button + class="mm-box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" + data-testid="edit" + > + Edit + </button> + </div> + <div + class="mm-box multichain-connection-list-item mm-box--padding-top-2 mm-box--padding-bottom-0 mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-row mm-box--align-items-baseline mm-box--width-full mm-box--background-color-background-default" + data-testid="site-cell-connection-list-item" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-icon mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-alternative mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" + > + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/data.svg');" + /> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column mm-box--width-5/12" + style="align-self: center; flex-grow: 1;" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--text-align-left mm-box--color-text-default" + > + Use your enabled networks + </p> + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" + > + <span + class="mm-box mm-text mm-text--body-sm mm-text--ellipsis mm-box--width-max mm-box--color-text-alternative" + > + Requesting for + </span> + <div + aria-describedby="tippy-tooltip-6" + class="" + data-original-title="This can be changed in "Settings > Alerts"" + data-tooltipped="" + style="display: inline;" + > + <div + class="mm-box multichain-avatar-group mm-box--display-flex mm-box--gap-1 mm-box--align-items-center" + data-testid="avatar-group" + > + <div + class="mm-box mm-box--display-flex" + > + <div + class="mm-box mm-box--rounded-full" + style="margin-left: 0px;" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-token mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full" + > + <img + alt="Custom Mainnet RPC logo" + class="mm-avatar-token__token-image" + src="./images/eth_logo.svg" + /> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <button + class="mm-box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" + data-testid="edit" + > + Edit + </button> + </div> + </div> + </div> + <div + class="mm-box multichain-page-footer mm-box--padding-4 mm-box--display-flex mm-box--gap-4 mm-box--width-full" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-column mm-box--width-full" + > + <div + class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + <span> + + Only connect with sites you trust. + <button + class="mm-box mm-text mm-button-base mm-button-link mm-button-link--size-inherit mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" + target="_blank" + > + Learn more + </button> + + + </span> + </p> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-4 mm-box--width-full" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-secondary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent mm-box--rounded-pill mm-box--border-color-primary-default box--border-style-solid box--border-width-1" + data-testid="cancel-btn" + > + Cancel + </button> + <button + class="mm-box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-primary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" + data-testid="confirm-btn" + data-theme="light" + > + Connect + </button> + </div> + </div> + </div> + </div> + </div> +</div> +`; 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<ConnectPageProps> = ({ }) => { 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<ConnectPageProps> = ({ ), [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<ConnectPageProps> = ({ }, [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 163/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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) <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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/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 <jyotipuri@gmail.com> Date: Wed, 16 Oct 2024 20:24:01 +0530 Subject: [PATCH 164/226] 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** <img width="361" alt="Screenshot 2024-10-11 at 7 43 59 PM" src="https://github.com/user-attachments/assets/8dd1cb10-ee5e-4a21-bb33-9222a0f1735f"> ## **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( + <ConfirmTitle />, + 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 <howrad@gmail.com> Date: Wed, 16 Oct 2024 08:28:24 -0700 Subject: [PATCH 165/226] 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='<insert SRP here>'` and `PASSWORD='<insert wallet password here>'` 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 166/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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 <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** ![Screenshot 2024-07-12 at 3 44 34 PM](https://github.com/user-attachments/assets/9bcb8552-c1e7-4a4e-b9f3-b70327341d68) ### **After** <!-- [screenshots/recordings] --> 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 = <T>( 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<any>(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<BigNumber>(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<string, SwapsTokenObject> = { + [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<string, SwapsTokenObject>, + 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<NativeAsset> + | AssetWithDisplayData<ERC20Asset> + | 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<NativeAsset> | AssetWithDisplayData<ERC20Asset> + > { + 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" > <div - class="bridge__header" + class="mm-box mm-header-base multichain-page-header bridge__header mm-box--padding-4 mm-box--display-flex mm-box--justify-content-center mm-box--width-full" > <div - class="mm-box mm-box--margin-left-4 mm-box--display-flex mm-box--justify-content-center mm-box--width-1/12" - tabindex="0" + class="mm-box" + style="min-width: 0px;" > - <span - class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-icon-alternative" - style="mask-image: url('./images/icons/arrow-2-left.svg'); cursor: pointer;" - title="Cancel" - /> + <button + aria-label="Back" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/arrow-left.svg');" + /> + </button> </div> <div - class="bridge__title" + class="mm-box" > - Bridge + <p + class="mm-box mm-text mm-text--body-md-bold mm-text--ellipsis mm-text--text-align-center mm-box--padding-inline-start-8 mm-box--padding-inline-end-8 mm-box--display-block mm-box--color-text-default" + > + Bridge + </p> + </div> + <div + class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" + style="min-width: 0px;" + > + <button + aria-label="Settings" + class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/setting.svg');" + /> + </button> </div> </div> <div - class="bridge__content bridge__content--redesign-enabled" + class="mm-box multichain-page-content bridge__content mm-box--padding-4 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--height-full" /> + <div + class="mm-box multichain-page-footer mm-box--padding-4 mm-box--display-flex mm-box--gap-4 mm-box--width-full" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--disabled mm-button-primary mm-button-primary--disabled mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" + data-testid="bridge-cta-button" + data-theme="light" + disabled="" + > + Select token + </button> + </div> </div> </div> </div> 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 ( <div className="bridge"> <div className="bridge__container"> - <div className="bridge__header"> - <Box - display={Display.Flex} - justifyContent={JustifyContent.center} - marginLeft={4} - width={BlockSize.OneTwelfth} - tabIndex={0} - onKeyUp={(e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - <Icon - name={IconName.Arrow2Left} - size={IconSize.Lg} - color={IconColor.iconAlternative} + <Header + className="bridge__header" + startAccessory={ + <ButtonIcon + iconName={IconName.ArrowLeft} + size={ButtonIconSize.Sm} + ariaLabel={t('back')} onClick={redirectToDefaultRoute} - style={{ cursor: 'pointer' }} - title={t('cancel')} /> - </Box> - - <div className="bridge__title">{t('bridge')}</div> - </div> - <div - className={classnames( - 'bridge__content', - 'bridge__content--redesign-enabled', - )} + } + endAccessory={ + <ButtonIcon + iconName={IconName.Setting} + size={ButtonIconSize.Sm} + ariaLabel={t('settings')} + /> + } > + {t('bridge')} + </Header> + <Content className="bridge__content"> <Switch> <FeatureToggledRoute redirectRoute={SWAPS_MAINTENANCE_ROUTE} @@ -94,7 +94,10 @@ const CrossChainSwap = () => { }} /> </Switch> - </div> + </Content> + <Footer> + <BridgeCTAButton /> + </Footer> </div> </div> ); 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`] = ` +<div> + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--disabled mm-button-primary mm-button-primary--disabled mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" + data-testid="bridge-cta-button" + data-theme="light" + disabled="" + > + Select token + </button> +</div> +`; 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`] = ` +<div> + <div + class="prepare-bridge-page" + > + <div + class="mm-box prepare-bridge-page__content" + > + <div + class="mm-box prepare-bridge-page__from" + > + <div + class="mm-box prepare-bridge-page__input-row" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md asset-picker mm-text--body-md-medium mm-box--padding-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--gap-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-transparent mm-box--rounded-pill" + data-testid="asset-picker-button" + > + <span + class="mm-box mm-text mm-text--inherit mm-box--color-text-default" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex" + > + <div + class="mm-box mm-badge-wrapper mm-box--display-inline-block" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-token mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full" + > + <img + alt="ETH logo" + class="mm-avatar-token__token-image" + src="./images/eth_logo.svg" + /> + </div> + <div + class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-muted box--border-style-solid box--border-width-1" + > + <img + alt="Ethereum Mainnet logo" + class="mm-avatar-network__network-image" + src="./images/eth_logo.svg" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box" + > + <div + class="" + style="display: inline;" + tabindex="0" + title="" + > + <p + class="mm-box mm-text asset-picker__symbol mm-text--body-md mm-box--color-text-default" + > + ETH + </p> + </div> + </div> + </div> + </span> + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-start-0 mm-box--display-inline-block mm-box--color-icon-default" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> + </button> + <div> + <div + class="mm-box mm-text-field mm-text-field--size-md mm-text-field--focused mm-text-field--truncate amount-input mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--align-items-center mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-width-1 box--border-style-solid" + > + <input + autocomplete="off" + class="mm-box mm-text mm-input mm-input--disable-state-styles mm-text-field__input mm-text--body-md mm-box--margin-0 mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--color-text-default mm-box--background-color-transparent mm-box--border-style-none" + data-testid="from-amount" + focused="true" + placeholder="0" + type="number" + value="" + /> + </div> + </div> + </div> + <div + class="mm-box prepare-bridge-page__amounts-row" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + + </p> + <div + class="mm-box currency-display-component mm-box--display-flex mm-box--flex-wrap-wrap mm-box--align-items-center" + title="$0.00" + > + <span + class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default" + > + $0.00 + </span> + </div> + </div> + </div> + <div + class="mm-box prepare-bridge-page__switch-tokens" + > + <button + aria-label="switch-tokens" + class="mm-box mm-button-icon mm-button-icon--size-lg mm-button-icon--disabled mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--width-full mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + data-testid="switch-tokens" + disabled="" + > + <span + class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/arrow-2-down.svg');" + /> + </button> + </div> + <div + class="mm-box prepare-bridge-page__to" + > + <div + class="mm-box prepare-bridge-page__input-row" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md asset-picker mm-text--body-md-medium mm-box--padding-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--gap-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-transparent mm-box--rounded-pill" + data-testid="asset-picker-button" + > + <span + class="mm-box mm-text mm-text--inherit mm-box--color-text-default" + > + <p + class="mm-box mm-text asset-picker__fallback mm-text--body-md mm-box--color-text-default" + > + Select token + </p> + </span> + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-start-0 mm-box--display-inline-block mm-box--color-icon-default" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> + </button> + <div> + <div + class="amount-tooltip" + style="display: inherit;" + tabindex="0" + title="" + > + <div + class="mm-box mm-text-field mm-text-field--size-md mm-text-field--disabled mm-text-field--truncate amount-input mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--align-items-center mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-width-1 box--border-style-solid" + > + <input + autocomplete="off" + class="mm-box mm-text mm-input mm-input--disable-state-styles mm-input--disabled mm-text-field__input mm-text--body-md mm-box--margin-0 mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--color-text-default mm-box--background-color-transparent mm-box--border-style-none" + data-testid="to-amount" + disabled="" + focused="false" + placeholder="0" + readonly="" + type="number" + value="0" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box prepare-bridge-page__amounts-row" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + + </p> + <div + class="mm-box currency-display-component mm-box--display-flex mm-box--flex-wrap-wrap mm-box--align-items-center" + title="$0.00" + > + <span + class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default" + > + $0.00 + </span> + </div> + </div> + </div> + </div> + </div> +</div> +`; + +exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` +<div> + <div + class="prepare-bridge-page" + > + <div + class="mm-box prepare-bridge-page__content" + > + <div + class="mm-box prepare-bridge-page__from" + > + <div + class="mm-box prepare-bridge-page__input-row" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md asset-picker mm-text--body-md-medium mm-box--padding-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--gap-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-transparent mm-box--rounded-pill" + data-testid="asset-picker-button" + > + <span + class="mm-box mm-text mm-text--inherit mm-box--color-text-default" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex" + > + <div + class="mm-box mm-badge-wrapper mm-box--display-inline-block" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-token mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full" + > + <img + alt="ETH logo" + class="mm-avatar-token__token-image" + src="./images/eth_logo.svg" + /> + </div> + <div + class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-muted box--border-style-solid box--border-width-1" + > + <img + alt="Ethereum Mainnet logo" + class="mm-avatar-network__network-image" + src="./images/eth_logo.svg" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box" + > + <div + class="" + style="display: inline;" + tabindex="0" + title="" + > + <p + class="mm-box mm-text asset-picker__symbol mm-text--body-md mm-box--color-text-default" + > + ETH + </p> + </div> + </div> + </div> + </span> + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-start-0 mm-box--display-inline-block mm-box--color-icon-default" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> + </button> + <div> + <div + class="amount-tooltip" + style="display: inherit;" + tabindex="0" + title="" + > + <div + class="mm-box mm-text-field mm-text-field--size-md mm-text-field--focused mm-text-field--truncate amount-input mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--align-items-center mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-width-1 box--border-style-solid" + > + <input + autocomplete="off" + class="mm-box mm-text mm-input mm-input--disable-state-styles mm-text-field__input mm-text--body-md mm-box--margin-0 mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--color-text-default mm-box--background-color-transparent mm-box--border-style-none" + data-testid="from-amount" + focused="true" + placeholder="0" + type="number" + value="1" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box prepare-bridge-page__amounts-row" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + + </p> + <div + class="mm-box currency-display-component mm-box--display-flex mm-box--flex-wrap-wrap mm-box--align-items-center" + title="$0.00" + > + <span + class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default" + > + $0.00 + </span> + </div> + </div> + </div> + <div + class="mm-box prepare-bridge-page__switch-tokens" + > + <button + aria-label="switch-tokens" + class="mm-box mm-button-icon mm-button-icon--size-lg mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--width-full mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + data-testid="switch-tokens" + > + <span + class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/arrow-2-down.svg');" + /> + </button> + </div> + <div + class="mm-box prepare-bridge-page__to" + > + <div + class="mm-box prepare-bridge-page__input-row" + > + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md asset-picker mm-text--body-md-medium mm-box--padding-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--gap-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-transparent mm-box--rounded-pill" + data-testid="asset-picker-button" + > + <span + class="mm-box mm-text mm-text--inherit mm-box--color-text-default" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex" + > + <div + class="mm-box mm-badge-wrapper mm-box--display-inline-block" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-token mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full" + > + <img + alt="UNI logo" + class="mm-avatar-token__token-image" + src="http://url" + /> + </div> + <div + class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-muted box--border-style-solid box--border-width-1" + > + <img + alt="Linea Mainnet logo" + class="mm-avatar-network__network-image" + src="./images/linea-logo-mainnet.svg" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box" + > + <div + class="" + style="display: inline;" + tabindex="0" + title="" + > + <p + class="mm-box mm-text asset-picker__symbol mm-text--body-md mm-box--color-text-default" + > + UNI + </p> + </div> + </div> + </div> + </span> + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-start-0 mm-box--display-inline-block mm-box--color-icon-default" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> + </button> + <div> + <div + class="amount-tooltip" + style="display: inherit;" + tabindex="0" + title="" + > + <div + class="mm-box mm-text-field mm-text-field--size-md mm-text-field--disabled mm-text-field--truncate amount-input mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--align-items-center mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-width-1 box--border-style-solid" + > + <input + autocomplete="off" + class="mm-box mm-text mm-input mm-input--disable-state-styles mm-input--disabled mm-text-field__input mm-text--body-md mm-box--margin-0 mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--color-text-default mm-box--background-color-transparent mm-box--border-style-none" + data-testid="to-amount" + disabled="" + focused="false" + placeholder="0" + readonly="" + type="number" + value="0" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box prepare-bridge-page__amounts-row" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + + </p> + <div + class="mm-box currency-display-component mm-box--display-flex mm-box--flex-wrap-wrap mm-box--align-items-center" + title="$0.00" + > + <span + class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default" + > + $0.00 + </span> + </div> + </div> + </div> + </div> + </div> +</div> +`; 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( + <BridgeCTAButton />, + 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( + <BridgeCTAButton />, + 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 ( + <Button + data-testid="bridge-cta-button" + onClick={() => { + if (isTxSubmittable) { + // dispatch tx submission + } + }} + disabled={!isTxSubmittable} + > + {label} + </Button> + ); +}; 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<typeof TextField>, + 'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled' + >; +} & Pick< + React.ComponentProps<typeof AssetPicker>, + '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 ( + <Box className={className}> + <Box className="prepare-bridge-page__input-row"> + <AssetPicker + header={header} + visibleTabs={[TabName.TOKENS]} + asset={ + networkProps?.network?.chainId && token + ? generateAssetFromToken(networkProps.network.chainId, token) + : undefined + } + onAssetChange={onAssetChange} + networkProps={networkProps} + customTokenListGenerator={customTokenListGenerator} + /> + <Tooltip + containerClassName="amount-tooltip" + position="top" + title={amountFieldProps.value} + disabled={(amountFieldProps.value?.toString()?.length ?? 0) < 12} + arrow + hideOnClick={false} + // explicitly inherit display since Tooltip will default to block + style={{ display: 'inherit' }} + > + <TextField + type={TextFieldType.Number} + className="amount-input" + placeholder="0" + onChange={(e) => { + onAmountChange?.(e.target.value); + }} + {...amountFieldProps} + /> + </Tooltip> + </Box> + <Box className="prepare-bridge-page__amounts-row"> + <Text> + {formattedBalance ? `${t('balance')}: ${formattedBalance}` : ' '} + </Text> + <CurrencyDisplay + currency="usd" + displayValue={ + token?.symbol && + networkProps?.network?.chainId && + isSwapsDefaultTokenSymbol( + token.symbol, + networkProps.network.chainId, + ) + ? ethFiatValue + : tokenFiatValue + } + hideLabel + /> + </Box> + </Box> + ); +}; 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( + <PrepareBridgePage />, + 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( + <PrepareBridgePage />, + 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(<PrepareBridgePage />, 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 ( - <div> - <PrepareSwapPage - ethBalance={ethBalance} - selectedAccountAddress={selectedAccountAddress} - shuffledTokensList={shuffledTokensList} - /> + <div className="prepare-bridge-page"> + <Box className="prepare-bridge-page__content"> + <BridgeInputGroup + className="prepare-bridge-page__from" + header={t('bridgeFrom')} + token={fromToken} + onAmountChange={(e) => { + 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, + }} + /> + + <Box className="prepare-bridge-page__switch-tokens"> + <ButtonIcon + iconProps={{ + className: classnames({ + rotate: rotateSwitchTokens, + }), + }} + width={BlockSize.Full} + data-testid="switch-tokens" + ariaLabel="switch-tokens" + iconName={IconName.Arrow2Down} + disabled={!toChain} + onClick={() => { + setRotateSwitchTokens(!rotateSwitchTokens); + const toChainClientId = + toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints + ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex] + .networkClientId + : undefined; + toChainClientId && dispatch(setActiveNetwork(toChainClientId)); + dispatch(switchToAndFromTokens({ fromChain })); + }} + /> + </Box> + + <BridgeInputGroup + className="prepare-bridge-page__to" + header={t('bridgeTo')} + token={toToken} + onAssetChange={(token) => 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, + }} + /> + </Box> </div> ); }; + +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 <nidhi.kumari@consensys.net> Date: Wed, 16 Oct 2024 17:40:18 +0100 Subject: [PATCH 167/226] 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 = ( <EditAccountsModal accounts={accounts} defaultSelectedAccountAddresses={[accounts[0].address]} - activeTabOrigin={'https://test.dapp'} {...props} />, 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<EditAccountsModalProps> = ({ - activeTabOrigin, accounts, defaultSelectedAccountAddresses, onClose, @@ -56,7 +54,6 @@ export const EditAccountsModal: React.FC<EditAccountsModalProps> = ({ const trackEvent = useContext(MetaMetricsContext); const [showAddNewAccounts, setShowAddNewAccounts] = useState(false); - const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( defaultSelectedAccountAddresses, ); @@ -84,155 +81,151 @@ export const EditAccountsModal: React.FC<EditAccountsModalProps> = ({ } }; - 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 ( - <> - <Modal - isOpen - onClose={onClose} - data-testid="edit-accounts-modal" - className="edit-accounts-modal" - > - <ModalOverlay /> - <ModalContent> - <ModalHeader onClose={onClose}>{t('editAccounts')}</ModalHeader> - <ModalBody paddingLeft={0} paddingRight={0}> - {showAddNewAccounts ? ( - <Box paddingLeft={4} paddingRight={4} paddingBottom={4}> - <CreateEthAccount - onActionComplete={() => setShowAddNewAccounts(false)} + <Modal + isOpen + onClose={onClose} + data-testid="edit-accounts-modal" + className="edit-accounts-modal" + > + <ModalOverlay /> + <ModalContent> + <ModalHeader onClose={onClose}>{t('editAccounts')}</ModalHeader> + <ModalBody + paddingLeft={0} + paddingRight={0} + className="edit-accounts-modal__body" + > + {showAddNewAccounts ? ( + <Box paddingLeft={4} paddingRight={4} paddingBottom={4}> + <CreateEthAccount + onActionComplete={() => setShowAddNewAccounts(false)} + /> + </Box> + ) : ( + <> + <Box + padding={4} + display={Display.Flex} + justifyContent={JustifyContent.spaceBetween} + > + <Checkbox + label={t('selectAll')} + isChecked={checked} + gap={4} + onClick={() => + allAreSelected() ? deselectAll() : selectAll() + } + isIndeterminate={isIndeterminate} /> + <ButtonLink onClick={() => setShowAddNewAccounts(true)}> + {t('newAccount')} + </ButtonLink> </Box> - ) : ( - <> - <Box - padding={4} - display={Display.Flex} - justifyContent={JustifyContent.spaceBetween} - > - <Checkbox - label={t('selectAll')} - isChecked={checked} - gap={4} - onClick={() => - allAreSelected() ? deselectAll() : selectAll() - } - isIndeterminate={isIndeterminate} - /> - <ButtonLink onClick={() => setShowAddNewAccounts(true)}> - {t('newAccount')} - </ButtonLink> - </Box> - {accounts.map((account) => ( - <AccountListItem - onClick={() => handleAccountClick(account.address)} - account={account} - key={account.address} - isPinned={Boolean(account.pinned)} - startAccessory={ - <Checkbox - isChecked={selectedAccountAddresses.some( - (selectedAccountAddress) => - isEqualCaseInsensitive( - selectedAccountAddress, - account.address, - ), - )} - /> - } - selected={false} - /> - ))} - - <ModalFooter> - {selectedAccountAddresses.length === 0 ? ( - <Box - display={Display.Flex} - flexDirection={FlexDirection.Column} - gap={4} - > - <Box - display={Display.Flex} - gap={1} - alignItems={AlignItems.center} - > - <Icon - name={IconName.Danger} - size={IconSize.Xs} - color={IconColor.errorDefault} - /> - <Text - variant={TextVariant.bodySm} - color={TextColor.errorDefault} - > - {t('disconnectMessage', [hostName])} - </Text> - </Box> - <ButtonPrimary - data-testid="disconnect-accounts-button" - onClick={() => { - onSubmit([]); - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - danger - > - {t('disconnect')} - </ButtonPrimary> - </Box> - ) : ( - <ButtonPrimary - data-testid="connect-more-accounts-button" - onClick={() => { - // 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')} - </ButtonPrimary> - )} - </ModalFooter> - </> - )} - </ModalBody> - </ModalContent> - </Modal> - </> + {accounts.map((account) => ( + <AccountListItem + onClick={() => handleAccountClick(account.address)} + account={account} + key={account.address} + isPinned={Boolean(account.pinned)} + startAccessory={ + <Checkbox + isChecked={selectedAccountAddresses.some( + (selectedAccountAddress) => + isEqualCaseInsensitive( + selectedAccountAddress, + account.address, + ), + )} + /> + } + selected={false} + /> + ))} + </> + )} + </ModalBody> + + <ModalFooter> + {selectedAccountAddresses.length === 0 ? ( + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + gap={4} + width={BlockSize.Full} + alignItems={AlignItems.center} + > + <Box + display={Display.Flex} + gap={1} + alignItems={AlignItems.center} + > + <Icon + name={IconName.Danger} + size={IconSize.Xs} + color={IconColor.errorDefault} + /> + <Text + variant={TextVariant.bodySm} + color={TextColor.errorDefault} + > + {t('disconnectMessage')} + </Text> + </Box> + <ButtonPrimary + data-testid="disconnect-accounts-button" + onClick={() => { + onSubmit([]); + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + danger + > + {t('disconnect')} + </ButtonPrimary> + </Box> + ) : ( + <ButtonPrimary + data-testid="connect-more-accounts-button" + onClick={() => { + 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')} + </ButtonPrimary> + )} + </ModalFooter> + </ModalContent> + </Modal> ); }; 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')} </ModalHeader> - <ModalBody paddingLeft={0} paddingRight={0}> + <ModalBody + paddingLeft={0} + paddingRight={0} + className="edit-networks-modal__body" + > <Box padding={4}> <Checkbox label={t('selectAll')} @@ -146,46 +148,36 @@ export const EditNetworksModal = ({ showEndAccessory={false} /> ))} - <ModalFooter> - {selectedChainIds.length === 0 ? ( + </ModalBody> + <ModalFooter> + {selectedChainIds.length === 0 ? ( + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + gap={4} + alignItems={AlignItems.center} + width={BlockSize.Full} + > <Box display={Display.Flex} - flexDirection={FlexDirection.Column} - gap={4} + gap={1} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} > - <Box - display={Display.Flex} - gap={1} - alignItems={AlignItems.center} - > - <Icon - name={IconName.Danger} - size={IconSize.Xs} - color={IconColor.errorDefault} - /> - <Text - variant={TextVariant.bodySm} - color={TextColor.errorDefault} - > - {t('disconnectMessage', [hostName])} - </Text> - </Box> - <ButtonPrimary - data-testid="disconnect-chains-button" - onClick={() => { - onSubmit([]); - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - danger + <Icon + name={IconName.Danger} + size={IconSize.Sm} + color={IconColor.errorDefault} + /> + <Text + variant={TextVariant.bodySm} + color={TextColor.errorDefault} > - {t('disconnect')} - </ButtonPrimary> + {t('disconnectMessage')} + </Text> </Box> - ) : ( <ButtonPrimary - data-testid="connect-more-chains-button" + data-testid="disconnect-chains-button" onClick={() => { 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')} </ButtonPrimary> - )} - </ModalFooter> - </ModalBody> + </Box> + ) : ( + <ButtonPrimary + data-testid="connect-more-chains-button" + onClick={() => { + onSubmit(selectedChainIds); + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + > + {t('update')} + </ButtonPrimary> + )} + </ModalFooter> </ModalContent> </Modal> ); }; 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} /> ) : ( <NoConnectionContent /> 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<SiteCellProps> = ({ onSelectChainIds, selectedAccountAddresses, selectedChainIds, - activeTabOrigin, isConnectFlow, }) => { const t = useI18nContext(); @@ -148,7 +146,6 @@ export const SiteCell: React.FC<SiteCellProps> = ({ </Box> {showEditAccountsModal && ( <EditAccountsModal - activeTabOrigin={activeTabOrigin} accounts={accounts} defaultSelectedAccountAddresses={selectedAccountAddresses} onClose={() => setShowEditAccountsModal(false)} @@ -158,7 +155,6 @@ export const SiteCell: React.FC<SiteCellProps> = ({ {showEditNetworksModal && ( <EditNetworksModal - activeTabOrigin={activeTabOrigin} nonTestNetworks={nonTestNetworks} testNetworks={testNetworks} defaultSelectedChainIds={selectedChainIds} diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index 45e6c5b1f48f..0ae22b3d9e0f 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -62,7 +62,6 @@ export const ConnectPage: React.FC<ConnectPageProps> = ({ permissionsRequestId, rejectPermissionsRequest, approveConnection, - activeTabOrigin, }) => { const t = useI18nContext(); @@ -146,7 +145,6 @@ export const ConnectPage: React.FC<ConnectPageProps> = ({ onSelectChainIds={setSelectedChainIds} selectedAccountAddresses={selectedAccountAddresses} selectedChainIds={selectedChainIds} - activeTabOrigin={activeTabOrigin} isConnectFlow /> </Content> From 601b5fabea723404b6043a19b0176be681d5ca05 Mon Sep 17 00:00:00 2001 From: David Walsh <davidwalsh83@gmail.com> Date: Wed, 16 Oct 2024 11:45:57 -0500 Subject: [PATCH 168/226] fix: Onboarding: Code style nits (#27767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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() { <> <div className="privacy-settings" data-testid="privacy-settings"> <div - className={`container ${showDetail ? 'show-detail' : 'show-list'}`} + className={classnames('container', { + 'show-detail': showDetail, + 'show-list': !showDetail, + })} > <div className="list-view"> <Box @@ -357,9 +363,9 @@ export default function PrivacySettings() { </div> <div - className={`detail-view ${ - !showDetail && hiddenClass ? 'hidden' : '' - }`} + className={classnames('detail-view', { + hidden: !showDetail && hiddenClass, + })} > <Box className="privacy-settings__header" @@ -388,7 +394,7 @@ export default function PrivacySettings() { width={BlockSize.Full} > <Text variant={TextVariant.headingLg} as="h2"> - {selectedItem && selectedItem.title} + {selectedItem?.title} </Text> </Box> </Box> @@ -397,7 +403,7 @@ export default function PrivacySettings() { className="privacy-settings__settings" data-testid="privacy-settings-settings" > - {selectedItem && selectedItem.id === 1 ? ( + {selectedItem?.id === 1 ? ( <> <Setting dataTestId="basic-functionality-toggle" @@ -572,7 +578,7 @@ export default function PrivacySettings() { /> </> ) : null} - {selectedItem && selectedItem.id === 2 ? ( + {selectedItem?.id === 2 ? ( <> <Setting value={turnOnTokenDetection} @@ -709,7 +715,7 @@ export default function PrivacySettings() { /> </> ) : null} - {selectedItem && selectedItem.id === 3 ? ( + {selectedItem?.id === 3 ? ( <> <Setting value={turnOn4ByteResolution} From ccc5aad513bbe0f32f56fa7331613c7a33852d54 Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Wed, 16 Oct 2024 19:16:34 +0200 Subject: [PATCH 169/226] 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 <maarten@zuidhoorn.com> Date: Wed, 16 Oct 2024 21:11:49 +0200 Subject: [PATCH 170/226] 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 <guillaumeroux123@gmail.com> Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Co-authored-by: Hassan Malik <hbmalik88@gmail.com> Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com> --- ...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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";;;;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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -diff --git a/dist/ui.d.cts b/dist/ui.d.cts -index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644 ---- a/dist/ui.d.cts -+++ b/dist/ui.d.cts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map -index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644 ---- a/dist/ui.d.cts.map -+++ b/dist/ui.d.cts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.d.mts b/dist/ui.d.mts -index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644 ---- a/dist/ui.d.mts -+++ b/dist/ui.d.mts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map -index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644 ---- a/dist/ui.d.mts.map -+++ b/dist/ui.d.mts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.mjs b/dist/ui.mjs -index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644 ---- a/dist/ui.mjs -+++ b/dist/ui.mjs -@@ -195,13 +195,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ assert(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ assert(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map -index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644 ---- a/dist/ui.mjs.map -+++ b/dist/ui.mjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";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<Type>(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 <Link href={token.href} children={getLinkText(token)} />;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n <Bold>\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 </Bold>\n );\n\n case 'em':\n return (\n <Italic>\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 </Italic>\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 <Address address={component.value} />;\n\n case NodeType.Button:\n return (\n <Button\n name={component.name}\n variant={getButtonVariant(component.variant)}\n type={component.buttonType}\n >\n {component.value}\n </Button>\n );\n\n case NodeType.Copyable:\n return (\n <Copyable value={component.value} sensitive={component.sensitive} />\n );\n\n case NodeType.Divider:\n return <Divider />;\n\n case NodeType.Form:\n return (\n <Form name={component.name}>\n {getChildren(component.children.map(getElement))}\n </Form>\n );\n\n case NodeType.Heading:\n return <Heading children={component.value} />;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return <Image src={component.value} />;\n\n case NodeType.Input:\n return (\n <Field label={component.label} error={component.error}>\n <Input\n name={component.name}\n type={component.inputType}\n value={component.value}\n placeholder={component.placeholder}\n />\n </Field>\n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n <Box children={getChildren(component.children.map(getElement))} />\n );\n\n case NodeType.Row:\n return (\n <Row label={component.label} variant={component.variant}>\n {getElement(component.value) as RowChildren}\n </Row>\n );\n\n case NodeType.Spinner:\n return <Spinner />;\n\n case NodeType.Text:\n return <Text>{getChildren(getTextChildren(component.value))}</Text>;\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<number>(\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<Element extends JSXElement>(\n element: Element,\n): element is Element & {\n props: { children: Nestable<JSXElement | string> };\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<Value>(\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, unknown>): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}</${type}>${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 8a40082d4d80..fffc9ae44f49 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -413,6 +413,7 @@ export const unrestrictedMethods = Object.freeze([ 'snap_updateInterface', 'snap_getInterfaceState', 'snap_resolveInterface', + 'snap_getCurrencyRate', ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 'metamaskinstitutional_authenticate', 'metamaskinstitutional_reauthenticate', diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5b3693960113..0c692703f242 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1479,6 +1479,7 @@ export default class MetamaskController extends EventEmitter { `${this.phishingController.name}:testOrigin`, `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, + `${this.snapController.name}:get`, ], }); @@ -5975,6 +5976,19 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getAll', ), + getCurrencyRate: (currency) => { + const rate = this.multichainRatesController.state.rates[currency]; + const { fiatCurrency } = this.multichainRatesController.state; + + if (!rate) { + return undefined; + } + + return { + ...rate, + currency: fiatCurrency, + }; + }, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hasPermission: this.permissionController.hasPermission.bind( this.permissionController, diff --git a/builds.yml b/builds.yml index a69bf611a322..bcd035b56bc1 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -67,7 +67,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -90,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io diff --git a/package.json b/package.json index fe2f43f63c9a..652b8d4b3afb 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.5.1", + "@metamask/snaps-sdk": "^6.9.0", "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -264,8 +264,7 @@ "@metamask/network-controller@npm:^17.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^20.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", - "path-to-regexp": "1.9.0", - "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "path-to-regexp": "1.9.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -342,7 +341,7 @@ "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", - "@metamask/preinstalled-example-snap": "^0.1.0", + "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", @@ -353,11 +352,11 @@ "@metamask/selected-network-controller": "^18.0.1", "@metamask/signature-controller": "^19.1.0", "@metamask/smart-transactions-controller": "^13.0.0", - "@metamask/snaps-controllers": "^9.7.0", - "@metamask/snaps-execution-environments": "^6.7.2", - "@metamask/snaps-rpc-methods": "^11.1.1", - "@metamask/snaps-sdk": "^6.5.1", - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch", + "@metamask/snaps-controllers": "^9.11.1", + "@metamask/snaps-execution-environments": "^6.9.1", + "@metamask/snaps-rpc-methods": "^11.5.0", + "@metamask/snaps-sdk": "^6.9.0", + "@metamask/snaps-utils": "^8.4.1", "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 2b1a6bc6532d..7fdbf5e1fd24 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.1/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.2', }; diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index 14abe8eceea7..a41b92c9f463 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -43,6 +43,7 @@ import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; import { SnapUICard } from '../snaps/snap-ui-card'; import { SnapUIAddress } from '../snaps/snap-ui-address'; +import { SnapUIAvatar } from '../snaps/snap-ui-avatar'; import { SnapUISelector } from '../snaps/snap-ui-selector'; import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -106,6 +107,7 @@ export const safeComponentList = { SnapUICard, SnapUISelector, SnapUIAddress, + SnapUIAvatar, SnapUIFooterButton, FormTextField, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index 669f7dd30799..539548622135 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { CaipAccountId, isHexString, @@ -11,32 +10,35 @@ import { Display, TextColor, } from '../../../../helpers/constants/design-system'; -import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon'; -import Jazzicon from '../../../ui/jazzicon'; -import { getUseBlockie } from '../../../../selectors'; import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; +import { SnapUIAvatar } from '../snap-ui-avatar'; export type SnapUIAddressProps = { // The address must be a CAIP-10 string. address: string; - diameter?: number; + // This is not currently exposed to Snaps. + avatarSize?: 'xs' | 'sm' | 'md' | 'lg'; }; export const SnapUIAddress: React.FunctionComponent<SnapUIAddressProps> = ({ 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<SnapUIAddressProps> = ({ return ( <Box display={Display.Flex} alignItems={AlignItems.center} gap={2}> - {useBlockie ? ( - <BlockieIdenticon - address={parsed.address} - diameter={diameter} - borderRadius="50%" - /> - ) : ( - <Jazzicon - namespace={parsed.chain.namespace} - address={parsed.address} - diameter={diameter} - style={{ display: 'flex' }} - /> - )} + <SnapUIAvatar address={caipIdentifier} size={avatarSize} /> <Text color={TextColor.inherit}>{shortenedAddress}</Text> </Box> ); 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<string, number> = { + 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<SnapUIAvatarProps> = ({ + address, + size = 'md', +}) => { + const parsed = useMemo(() => { + return parseCaipAccountId(address as CaipAccountId); + }, [address]); + const useBlockie = useSelector(getUseBlockie); + + return useBlockie ? ( + <BlockieIdenticon + address={parsed.address} + diameter={DIAMETERS[size]} + borderRadius="50%" + /> + ) : ( + <Jazzicon + namespace={parsed.chain.namespace} + address={parsed.address} + diameter={DIAMETERS[size]} + style={{ display: 'flex' }} + /> + ); +}; 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 ( + <ButtonLink + as="a" + size={ButtonLinkSize.Inherit} + className="snap-ui-link" + onClick={handleLinkClick} + > + {children} + </ButtonLink> + ); + } + return ( <> <SnapLinkWarning isOpen={isOpen} onClose={handleModalClose} url={href} /> 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<AddressElement> = ({ 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<AvatarElement> = ({ 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<FieldElement> = ({ element, @@ -79,9 +80,7 @@ export const field: UIComponentFactory<FieldElement> = ({ 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<HeadingElement> = ({ 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<InputElement> = ({ 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<InputElement> = ({ 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 <ejwessel@gmail.com> Date: Wed, 16 Oct 2024 15:07:36 -0700 Subject: [PATCH 171/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 172/226] 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<string>).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 -[<img src="assets/success_approval_templates.png" width="300">](assets/confirmation.png) \ No newline at end of file +[<img src="assets/success_approval_templates.png" width="300">](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 ( <section className="error-page"> @@ -81,8 +83,8 @@ class ErrorPage extends PureComponent { <details> <summary>{t('errorDetails')}</summary> <ul> - {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<void>((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<void>((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<string> { 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<void, MetaMaskReduxState, unknown, AnyAction> { 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 173/226] fix: SonarCloud for forks (#27700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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 <markjstacey@gmail.com> --- .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 174/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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<string, any>; 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 <priya.narayanaswamy@consensys.net> Date: Thu, 17 Oct 2024 08:52:01 +0200 Subject: [PATCH 175/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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?= <albertolivecorbella@gmail.com> Date: Thu, 17 Oct 2024 11:55:27 +0200 Subject: [PATCH 176/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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 <metamaskbot@users.noreply.github.com> --- .../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 = () => { <ModalHeader onClose={handleClose}> {t('custodyRefreshTokenModalTitle')} </ModalHeader> - { - // @ts-expect-error: todo: Merge MetaMask Institutional PR 778 to fix this - custodian.iconUrl ? ( - <Box - display={Display.Flex} - flexDirection={FlexDirection.Column} - alignItems={AlignItems.center} - paddingTop={5} - > - <Box display={Display.Block} textAlign={TextAlign.Center}> - <img - // @ts-expect-error: todo: Merge MetaMask Institutional 778 PR to fix this - src={custodian.iconUrl} - width={45} - alt={custodian.displayName} - /> - </Box> - </Box> - ) : ( + {custodian.iconUrl ? ( + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + alignItems={AlignItems.center} + paddingTop={5} + > <Box display={Display.Block} textAlign={TextAlign.Center}> - <Text>{custodian.displayName}</Text> + <img + src={custodian.iconUrl} + width={45} + alt={custodian.displayName} + /> </Box> - ) - } + </Box> + ) : ( + <Box display={Display.Block} textAlign={TextAlign.Center}> + <Text>{custodian.displayName}</Text> + </Box> + )} <Box width={BlockSize.Full} backgroundColor={BackgroundColor.backgroundDefault} diff --git a/ui/store/institutional/institution-background.test.js b/ui/store/institutional/institution-background.test.js index 7a3ca82e53eb..722bf7a3135c 100644 --- a/ui/store/institutional/institution-background.test.js +++ b/ui/store/institutional/institution-background.test.js @@ -11,6 +11,7 @@ import { setNoteToTraderMessage, setTypedMessageInProgress, setPersonalMessageInProgress, + logAndStoreApiRequest, } from './institution-background'; jest.mock('../actions', () => ({ @@ -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<void> { + 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 177/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. --------- Co-authored-by: sahar-fehri <sahar.fehri@consensys.net> --- 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 <charly.chevalier@consensys.net> Date: Thu, 17 Oct 2024 13:45:55 +0200 Subject: [PATCH 178/226] 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 <metamaskbot@users.noreply.github.com> --- 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 179/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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<boolean>} 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<void>} 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 <pedro.figueiredo@consensys.net> Date: Thu, 17 Oct 2024 13:57:29 +0100 Subject: [PATCH 180/226] feat: Add transaction flow and details sections (#27654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Adds two new sections for the wallet initiated ERC20 token transfer redesigned confirmation. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="368" alt="Screenshot 2024-10-07 at 11 04 58" src="https://github.com/user-attachments/assets/3e160876-be5c-46d2-b03a-b841f93b08d1"> ## **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<void> { @@ -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<void> { @@ -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 = () => { /> )} <GasFeesSection /> - {showAdvancedDetails && <AdvancedDetails />} + <AdvancedDetails /> <EditSpendingCapModal isOpenEditSpendingCapModal={isOpenEditSpendingCapModal} setIsOpenEditSpendingCapModal={setIsOpenEditSpendingCapModal} diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx b/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx index a1ed4a1e43dc..fc58008d7784 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx @@ -1,9 +1,7 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import React from 'react'; -import { useSelector } from 'react-redux'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { useConfirmContext } from '../../../../context/confirm'; import { SimulationDetails } from '../../../simulation-details'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; @@ -14,10 +12,6 @@ const BaseTransactionInfo = () => { const { currentConfirmation: transactionMeta } = useConfirmContext<TransactionMeta>(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - if (!transactionMeta?.txParams) { return null; } @@ -33,7 +27,7 @@ const BaseTransactionInfo = () => { </ConfirmInfoSection> <TransactionDetails /> <GasFeesSection /> - {showAdvancedDetails && <AdvancedDetails />} + <AdvancedDetails /> </> ); }; 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<Numeric | undefined>(); 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<TransactionMeta>(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - const decodedResponse = useDecodedTransactionData(); const { value, pending } = decodedResponse; @@ -45,7 +39,7 @@ const SetApprovalForAllInfo = () => { )} <ApproveDetails isSetApprovalForAll /> <GasFeesSection /> - {showAdvancedDetails && <AdvancedDetails />} + <AdvancedDetails /> </> ); }; 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[`<AdvancedDetails /> does not render component for advanced transaction details 1`] = ` +exports[`<AdvancedDetails /> does not render component when the state property is false 1`] = `<div />`; + +exports[`<AdvancedDetails /> renders component when the prop override is passed 1`] = ` <div> <div class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" @@ -23,7 +25,7 @@ exports[`<AdvancedDetails /> does not render component for advanced transaction </p> <div> <div - aria-describedby="tippy-tooltip-1" + aria-describedby="tippy-tooltip-2" class="" data-original-title="This is the transaction number of an account. Nonce for the first transaction is 0 and it increases in sequential order." data-tooltipped="" @@ -120,7 +122,7 @@ exports[`<AdvancedDetails /> does not render component for advanced transaction </div> `; -exports[`<AdvancedDetails /> renders component for advanced transaction details 1`] = ` +exports[`<AdvancedDetails /> renders component when the state property is true 1`] = ` <div> <div class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" @@ -143,7 +145,7 @@ exports[`<AdvancedDetails /> renders component for advanced transaction details </p> <div> <div - aria-describedby="tippy-tooltip-2" + aria-describedby="tippy-tooltip-1" class="" data-original-title="This is the transaction number of an account. Nonce for the first transaction is 0 and it increases in sequential order." data-tooltipped="" @@ -166,19 +168,8 @@ exports[`<AdvancedDetails /> 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 </p> - <button - aria-label="Edit" - class="mm-box mm-button-icon mm-button-icon--size-sm edit-nonce-btn mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent mm-box--rounded-lg" - data-testid="edit-nonce-icon" - style="margin-left: -4px;" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/edit.svg');" - /> - </button> </div> </div> </div> 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('<AdvancedDetails />', () => { 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( <AdvancedDetails />, @@ -20,16 +30,18 @@ describe('<AdvancedDetails />', () => { 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( <AdvancedDetails />, @@ -38,4 +50,25 @@ describe('<AdvancedDetails />', () => { 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( + <AdvancedDetails overrideVisibility />, + 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 ( <> <NonceDetails /> 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[`<SendHeading /> renders component 1`] = ` <div> <div - class="mm-box mm-box--padding-top-4 mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-center mm-box--align-items-center" + class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-token mm-text--body-lg-medium mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-muted mm-box--background-color-overlay-default mm-box--rounded-full" + <svg + class="preloader__icon" + fill="none" + height="20" + viewBox="0 0 16 16" + width="20" + xmlns="http://www.w3.org/2000/svg" > - ? - </div> - <h2 - class="mm-box mm-text mm-text--heading-lg mm-box--margin-top-3 mm-box--color-inherit" - > - Unknown - </h2> + <path + clip-rule="evenodd" + d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" + fill="var(--color-primary-muted)" + fill-rule="evenodd" + /> + <mask + height="16" + id="mask0" + mask-type="alpha" + maskUnits="userSpaceOnUse" + width="16" + x="0" + y="0" + > + <path + clip-rule="evenodd" + d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" + fill="var(--color-primary-default)" + fill-rule="evenodd" + /> + </mask> + <g + mask="url(#mask0)" + > + <path + d="M6.85718 17.9999V11.4285V8.28564H-4.85711V17.9999H6.85718Z" + fill="var(--color-primary-default)" + /> + </g> + </svg> </div> </div> `; 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<typeof SendHeading>) => ( - <Provider store={store}>{story()}</Provider> + <Provider store={store}> + <ConfirmContextProvider>{story()}</ConfirmContextProvider> + </Provider> ), ], }; 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 = ( <AvatarToken @@ -58,7 +57,9 @@ const SendHeading = () => { variant={TextVariant.headingLg} color={TextColor.inherit} marginTop={3} - >{`${tokenBalance || ''} ${selectedToken?.symbol || t('unknown')}`}</Text> + >{`${decodedTransferValue || ''} ${ + selectedToken?.symbol || t('unknown') + }`}</Text> {fiatDisplayValue && ( <Text variant={TextVariant.bodyMd} color={TextColor.textAlternative}> {fiatDisplayValue} @@ -67,13 +68,17 @@ const SendHeading = () => { </> ); + if (pending) { + return <ConfirmLoader />; + } + return ( <Box display={Display.Flex} flexDirection={FlexDirection.Column} justifyContent={JustifyContent.center} alignItems={AlignItems.center} - paddingTop={4} + padding={4} > {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`] = ` +<div> + <div + class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" + data-testid="confirmation__transaction-flow" + > + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + style="overflow-wrap: anywhere; min-height: 24px; position: relative;" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Network + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" + > + G + </div> + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + Goerli + </p> + </div> + </div> + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + style="overflow-wrap: anywhere; min-height: 24px; position: relative;" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Interacting with + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-account mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" + > + <div + class="mm-avatar-account__jazzicon" + > + <div + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 16px; height: 16px; display: inline-block; background: rgb(1, 142, 119);" + > + <svg + height="16" + width="16" + x="0" + y="0" + > + <rect + fill="#FB188D" + height="16" + transform="translate(-0.03888059901998558 -0.3611719722869608) rotate(292.9 8 8)" + width="16" + x="0" + y="0" + /> + <rect + fill="#F90E01" + height="16" + transform="translate(6.546320180202683 5.992209110554258) rotate(106.5 8 8)" + width="16" + x="0" + y="0" + /> + <rect + fill="#F26E02" + height="16" + transform="translate(8.96370841230271 -9.40372192765907) rotate(416.7 8 8)" + width="16" + x="0" + y="0" + /> + </svg> + </div> + </div> + </div> + <p + class="mm-box mm-text mm-text--body-md mm-box--margin-left-2 mm-box--color-inherit" + data-testid="confirm-info-row-display-name" + > + 0x07614...3ad68 + </p> + </div> + </div> + </div> + </div> +</div> +`; 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`] = ` <div> <div - class="mm-box mm-box--padding-top-4 mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-center mm-box--align-items-center" + class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + > + <svg + class="preloader__icon" + fill="none" + height="20" + viewBox="0 0 16 16" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + clip-rule="evenodd" + d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" + fill="var(--color-primary-muted)" + fill-rule="evenodd" + /> + <mask + height="16" + id="mask0" + mask-type="alpha" + maskUnits="userSpaceOnUse" + width="16" + x="0" + y="0" + > + <path + clip-rule="evenodd" + d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" + fill="var(--color-primary-default)" + fill-rule="evenodd" + /> + </mask> + <g + mask="url(#mask0)" + > + <path + d="M6.85718 17.9999V11.4285V8.28564H-4.85711V17.9999H6.85718Z" + fill="var(--color-primary-default)" + /> + </g> + </svg> + </div> + <div + class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + > + <svg + class="preloader__icon" + fill="none" + height="20" + viewBox="0 0 16 16" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + clip-rule="evenodd" + d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" + fill="var(--color-primary-muted)" + fill-rule="evenodd" + /> + <mask + height="16" + id="mask0" + mask-type="alpha" + maskUnits="userSpaceOnUse" + width="16" + x="0" + y="0" + > + <path + clip-rule="evenodd" + d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" + fill="var(--color-primary-default)" + fill-rule="evenodd" + /> + </mask> + <g + mask="url(#mask0)" + > + <path + d="M6.85718 17.9999V11.4285V8.28564H-4.85711V17.9999H6.85718Z" + fill="var(--color-primary-default)" + /> + </g> + </svg> + </div> + <div + class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" + data-testid="confirmation__transaction-flow" > <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-token mm-text--body-lg-medium mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-muted mm-box--background-color-overlay-default mm-box--rounded-full" + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + style="overflow-wrap: anywhere; min-height: 24px; position: relative;" > - ? + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Network + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default box--border-style-solid box--border-width-1" + > + G + </div> + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + Goerli + </p> + </div> </div> - <h2 - class="mm-box mm-text mm-text--heading-lg mm-box--margin-top-3 mm-box--color-inherit" + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + style="overflow-wrap: anywhere; min-height: 24px; position: relative;" > - Unknown - </h2> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Interacting with + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-account mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" + > + <div + class="mm-avatar-account__jazzicon" + > + <div + style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 16px; height: 16px; display: inline-block; background: rgb(1, 142, 119);" + > + <svg + height="16" + width="16" + x="0" + y="0" + > + <rect + fill="#FB188D" + height="16" + transform="translate(-0.03888059901998558 -0.3611719722869608) rotate(292.9 8 8)" + width="16" + x="0" + y="0" + /> + <rect + fill="#F90E01" + height="16" + transform="translate(6.546320180202683 5.992209110554258) rotate(106.5 8 8)" + width="16" + x="0" + y="0" + /> + <rect + fill="#F26E02" + height="16" + transform="translate(8.96370841230271 -9.40372192765907) rotate(416.7 8 8)" + width="16" + x="0" + y="0" + /> + </svg> + </div> + </div> + </div> + <p + class="mm-box mm-text mm-text--body-md mm-box--margin-left-2 mm-box--color-inherit" + data-testid="confirm-info-row-display-name" + > + 0x07614...3ad68 + </p> + </div> + </div> + </div> + </div> + <div + class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" + data-testid="gas-fee-section" + > + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + data-testid="edit-gas-fees-row" + style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Network fee + </p> + <div> + <div + aria-describedby="tippy-tooltip-1" + class="" + data-original-title="Amount paid to process the transaction on network." + data-tooltipped="" + style="display: flex;" + tabindex="0" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" + data-testid="edit-gas-fees-row-tooltip" + style="mask-image: url('./images/icons/question.svg');" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center mm-box--text-align-center" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--margin-right-1 mm-box--color-text-default" + data-testid="first-gas-field" + > + 0.0001 ETH + </p> + <p + class="mm-box mm-text mm-text--body-md mm-box--margin-right-2 mm-box--color-text-alternative" + data-testid="native-currency" + > + $0.04 + </p> + <button + class="mm-box mm-text mm-button-base mm-button-base--size-sm mm-button-link mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" + data-testid="edit-gas-fee-icon" + style="text-decoration: none;" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-end-1 mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/edit.svg');" + /> + <span + class="mm-box mm-text mm-text--inherit mm-box--color-primary-default" + /> + </button> + </div> + </div> + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + data-testid="gas-fee-details-speed" + style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Speed + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-wrap-wrap" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--padding-inline-end-1 mm-box--color-text-alternative" + > + 🦊 Market + </p> + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + > + <span + data-testid="gas-timing-time" + > + ~ + 0 sec + </span> + </p> + </div> + </div> + </div> </div> </div> `; 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[`<TransactionFlowSection /> renders correctly 1`] = ` +<div> + <div + class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" + data-testid="confirmation__transaction-flow" + > + <div + class="mm-box mm-box--padding-3 mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" + > + <div + class="mm-box mm-box--display-flex" + > + <div + class="name name__missing" + > + <span + class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/question.svg');" + /> + <p + class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default" + > + 0x2e0D7...5d09B + </p> + </div> + </div> + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-muted" + style="mask-image: url('./images/icons/arrow-right.svg');" + /> + <div + class="mm-box mm-box--display-flex" + > + <div + class="name name__missing" + > + <span + class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/question.svg');" + /> + <p + class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default" + > + 0x6B175...71d0F + </p> + </div> + </div> + </div> + </div> +</div> +`; 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( + <TokenDetailsSection />, + 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<TransactionMeta>(); + + const { chainId } = transactionMeta; + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const networkName = networkConfigurations[chainId].name; + + const networkRow = ( + <ConfirmInfoRow label={t('transactionFlowNetwork')}> + <Box + display={Display.Flex} + alignItems={AlignItems.center} + flexWrap={FlexWrap.Wrap} + gap={2} + minWidth={BlockSize.Zero} + > + <AvatarNetwork + borderColor={BorderColor.backgroundDefault} + size={AvatarNetworkSize.Sm} + src={ + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ] + } + name={networkName} + /> + <Text variant={TextVariant.bodyMd} color={TextColor.textDefault}> + {networkName} + </Text> + </Box> + </ConfirmInfoRow> + ); + + const tokenRow = ( + <ConfirmInfoRow label={t('interactingWith')}> + <ConfirmInfoRowAddress address={transactionMeta.txParams.to as string} /> + </ConfirmInfoRow> + ); + + return ( + <ConfirmInfoSection data-testid="confirmation__transaction-flow"> + {networkRow} + {tokenRow} + </ConfirmInfoSection> + ); +}; 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) => ( <Provider store={store}> - <ConfirmContextProvider>{story()}</ConfirmContextProvider> + <ConfirmContextProvider> + <Box + display={Display.Flex} + justifyContent={JustifyContent.center} + alignItems={AlignItems.center} + flexDirection={FlexDirection.Column} + > + {story()} + </Box> + </ConfirmContextProvider> </Provider> ), ], 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 <SendHeading />; + return ( + <> + <SendHeading /> + <TransactionFlowSection /> + <TokenDetailsSection /> + <GasFeesSection /> + <AdvancedDetails /> + </> + ); }; 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('<TransactionFlowSection />', () => { + 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( + <TransactionFlowSection />, + 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<TransactionMeta>(); + + const { value, pending } = useDecodedTransactionData(); + + const recipientAddress = value?.data[0].params.find( + (param) => param.type === 'address', + )?.value; + + if (pending) { + return <ConfirmLoader />; + } + + return ( + <ConfirmInfoSection data-testid="confirmation__transaction-flow"> + <Box + display={Display.Flex} + flexDirection={FlexDirection.Row} + justifyContent={JustifyContent.spaceBetween} + alignItems={AlignItems.center} + padding={3} + > + <Name + value={transactionMeta.txParams.from} + type={NameType.ETHEREUM_ADDRESS} + /> + <Icon + name={IconName.ArrowRight} + size={IconSize.Md} + color={IconColor.iconMuted} + /> + {recipientAddress && ( + <Name value={recipientAddress} type={NameType.ETHEREUM_ADDRESS} /> + )} + </Box> + </ConfirmInfoSection> + ); +}; 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[]>((): Alert[] => { if (originUndefinedOrValid) { From 93fbaaf3604c1455339a1f59d794f905ff76d163 Mon Sep 17 00:00:00 2001 From: Derek Brans <dbrans@gmail.com> Date: Thu, 17 Oct 2024 09:04:31 -0400 Subject: [PATCH 181/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 { <div className="settings-page__content-item-col"> <ToggleButton - value={smartTransactionsOptInStatus} + value={smartTransactionsEnabled} onToggle={(oldValue) => { 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(<AdvancedTab />, 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<void, MetaMaskReduxState, unknown, AnyAction> { 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 <kanthesha.devaramane@consensys.net> Date: Thu, 17 Oct 2024 16:28:26 +0100 Subject: [PATCH 182/226] feat: convert AlertController to typescript (#27764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** As a prerequisite for migrating AlertController to BaseController v2, and to support the TypeScript migration effort, we want to convert AlertController to TypeScript. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. --- .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<AlertControllerState>) => { + 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<string, boolean>; + unconnectedAccountAlertShownOrigins: Record<string, boolean>; + web3ShimUsageOrigins?: Record<string, number>; +}; + +/** + * The alert controller options + * + * @property state - The initial controller state + * @property controllerMessenger - The controller messenger + */ +type AlertControllerOptions = { + state?: Partial<AlertControllerState>; + controllerMessenger: AlertControllerMessenger; +}; + +const defaultState: AlertControllerState = { + alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( + (alertEnabledness: Record<string, boolean>, alertType: string) => { + alertEnabledness[alertType] = true; + return alertEnabledness; + }, + {}, + ), + unconnectedAccountAlertShownOrigins: {}, + web3ShimUsageOrigins: {}, +}; + +/** + * Controller responsible for maintaining alert-related state. + */ +export class AlertController { + store: ObservableStore<AlertControllerState>; + + 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 <sahar.fehri@consensys.net> Date: Thu, 17 Oct 2024 17:36:21 +0200 Subject: [PATCH 183/226] =?UTF-8?q?fix:=20fix=20currency=20display=20when?= =?UTF-8?q?=20tokenToFiatConversion=20rate=20is=20not=20avai=E2=80=A6=20(#?= =?UTF-8?q?27893)?= 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/1b97544b-adcd-424b-9da4-e360b7b94968 ### **After** <!-- [screenshots/recordings] --> 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 = ( <CurrencyInput className="asset-picker-amount__input" - isFiatPreferred={isFiatPrimary} + isFiatPreferred={ + isFiatPrimary && Boolean(tokenToFiatConversionRate?.toNumber()) + } onChange={onAmountChange} // onChange controls disabled state, disabled if undefined hexValue={value} swapIcon={(onClick: React.MouseEventHandler) => ( From fac442247f0180eb05973611e5d35db2fb1d3c21 Mon Sep 17 00:00:00 2001 From: Jyoti Puri <jyotipuri@gmail.com> Date: Thu, 17 Oct 2024 21:50:30 +0530 Subject: [PATCH 184/226] 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** <img width="355" alt="Screenshot 2024-10-14 at 5 40 21 PM" src="https://github.com/user-attachments/assets/53bde7f8-56aa-4f84-b748-dfa08d0bcef2"> ## **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`] = ` </div> </div> `; + +exports[`PermitSimulation renders correctly for NFT permit 1`] = ` +<div> + <div + class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" + data-testid="confirmation__simulation_section" + > + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + style="overflow-wrap: anywhere; min-height: 24px; position: relative;" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Estimated changes + </p> + <div> + <div + aria-describedby="tippy-tooltip-3" + class="" + data-original-title="Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee." + data-tooltipped="" + style="display: flex;" + tabindex="0" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" + style="mask-image: url('./images/icons/question.svg');" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-inherit" + style="white-space: pre-wrap;" + > + You're giving someone else permission to withdraw NFTs from your account. + </p> + </div> + </div> + <div + class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" + style="overflow-wrap: anywhere; min-height: 24px; position: relative;" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" + > + <div + class="mm-box mm-box--display-flex mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" + > + Withdraw + </p> + </div> + </div> + <div + class="mm-box" + style="margin-left: auto; max-width: 100%;" + > + <div + class="mm-box" + > + <div + class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" + > + <div + class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0" + > + <div> + <p + class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl" + data-testid="simulation-token-value" + style="padding-top: 1px; padding-bottom: 1px;" + > + #3606393 + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex" + > + <div + class="name name__missing" + > + <span + class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/question.svg');" + /> + <p + class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default" + > + 0xC3644...1FE88 + </p> + </div> + </div> + </div> + <div + class="mm-box" + /> + </div> + </div> + </div> + </div> +</div> +`; 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( + <PermitSimulation />, + 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<object> = () => { 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<object> = () => { ); const SpendingCapRow = ( - <ConfirmInfoRow label={t('spendingCap')}> + <ConfirmInfoRow + label={t(isNFT ? 'simulationApproveHeading' : 'spendingCap')} + > <Box style={{ marginLeft: 'auto', maxWidth: '100%' }}> {Array.isArray(tokenDetails) ? ( <Box @@ -89,6 +93,7 @@ const PermitSimulation: React.FC<object> = () => { <PermitSimulationValueDisplay tokenContract={verifyingContract} value={message.value} + tokenId={message.tokenId} /> )} </Box> @@ -99,7 +104,9 @@ const PermitSimulation: React.FC<object> = () => { <StaticSimulation title={t('simulationDetailsTitle')} titleTooltip={t('simulationDetailsTitleTooltip')} - description={t('permitSimulationDetailInfo')} + description={t( + isNFT ? 'simulationDetailsApproveDesc' : 'permitSimulationDetailInfo', + )} simulationElements={SpendingCapRow} /> ); 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`] = ` </div> </div> `; + +exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = ` +<div> + <div + class="mm-box" + > + <div + class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" + > + <div + class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0" + > + <div> + <p + class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl" + data-testid="simulation-token-value" + style="padding-top: 1px; padding-bottom: 1px;" + > + #4321 + </p> + </div> + </div> + <div + class="mm-box mm-box--display-flex" + > + <div + class="name name__missing" + > + <span + class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/question.svg');" + /> + <p + class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default" + > + 0xA0b86...6eB48 + </p> + </div> + </div> + </div> + <div + class="mm-box" + /> + </div> +</div> +`; 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( + <PermitSimulationValueDisplay + tokenContract="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + tokenId="4321" + />, + 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}`} </Text> </Tooltip> </Box> From 654dff7f918bb32a5051443609b424b2559080b2 Mon Sep 17 00:00:00 2001 From: sahar-fehri <sahar.fehri@consensys.net> Date: Thu, 17 Oct 2024 19:43:39 +0200 Subject: [PATCH 185/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> 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 @@ +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + width="100%" viewBox="0 0 1280 1280" enable-background="new 0 0 1280 1280" xml:space="preserve"> +<path fill="#FFFFFF" opacity="1.000000" stroke="none" + d=" +M591.000000,1281.000000 + C394.000000,1281.000000 197.500000,1281.000000 1.000000,1281.000000 + C1.000000,854.333313 1.000000,427.666656 1.000000,1.000000 + C427.666656,1.000000 854.333313,1.000000 1281.000000,1.000000 + C1281.000000,427.666656 1281.000000,854.333313 1281.000000,1281.000000 + C1051.166626,1281.000000 821.333313,1281.000000 591.000000,1281.000000 +M495.003510,881.729492 + C495.239380,882.670166 495.475220,883.610779 495.711090,884.551453 + C496.177246,884.316772 496.643372,884.082092 497.109528,883.847412 + C496.406342,882.898254 495.703186,881.949158 494.998047,880.181030 + C494.859100,878.623169 494.720123,877.065247 494.581177,875.507385 + C493.915436,875.717529 493.249725,875.927673 492.584015,876.137817 + C493.389343,877.758545 494.194672,879.379272 495.003510,881.729492 +M495.710205,848.878784 + C498.872192,844.081604 497.647552,835.415833 492.871185,833.332153 + C493.614807,838.804688 494.307526,843.902344 494.264526,849.150085 + C493.826538,850.817017 493.388519,852.483887 492.950500,854.150818 + C493.633606,854.239136 494.316711,854.327393 494.999817,854.415649 + C494.999817,852.610413 494.999817,850.805237 495.710205,848.878784 +M976.070129,616.767151 + C960.382690,616.713867 944.695312,616.660522 928.796204,615.945496 + C926.920959,615.239502 925.045715,614.533508 923.302612,613.877258 + C923.669006,612.009766 924.335083,610.602234 924.102417,609.363586 + C923.690613,607.172302 922.102783,605.085693 922.095825,602.951721 + C921.968018,563.980469 922.002502,525.008667 922.002502,486.036957 + C922.002502,484.237335 922.002441,482.437744 922.002441,480.638123 + C922.418335,480.958008 922.834290,481.277863 923.250183,481.597748 + C925.166992,480.101593 927.083740,478.605438 929.922668,476.927826 + C949.299500,476.912415 968.676514,476.920319 988.053040,476.841431 + C989.773865,476.834442 991.492371,476.262482 993.715820,475.308838 + C993.804443,456.162750 993.899658,437.016663 993.951538,417.870453 + C993.953186,417.265167 993.509705,416.658722 993.141113,415.428497 + C992.756592,414.720245 992.372009,414.011993 991.867493,412.451141 + C990.257141,411.967438 988.652649,411.089172 987.035522,411.065369 + C977.877686,410.930359 968.716858,411.000000 959.557007,411.000000 + C924.749695,411.000000 889.942261,410.967896 855.135437,411.096283 + C853.423462,411.102600 851.716614,412.510071 849.190308,413.478241 + C847.789368,413.494629 846.388489,413.510986 845.001343,412.664490 + C845.346741,411.218353 846.017334,409.755951 845.946167,408.330566 + C845.874756,406.899902 845.063904,405.506134 844.577148,404.096191 + C842.547241,404.842102 839.279358,405.091370 838.740295,406.435333 + C837.547546,409.408691 837.726746,412.932373 837.299377,416.638794 + C842.643555,415.244324 843.055298,418.239105 843.053528,422.363434 + C842.986938,573.449402 842.998535,724.535461 842.984070,875.621460 + C842.983887,877.104858 843.319580,878.974792 842.580505,880.007202 + C838.631104,885.525269 842.763000,889.430664 845.239746,893.456909 + C845.950134,894.611633 848.433594,894.675659 850.971436,895.532654 + C850.020569,893.146179 849.574036,892.025574 848.767944,890.002441 + C851.365112,890.002441 853.290466,890.002502 855.215759,890.002502 + C899.358887,890.002502 943.502014,890.026001 987.644897,889.917175 + C989.426697,889.912781 991.205383,888.656311 993.049561,887.223083 + C993.122498,886.465088 993.195496,885.707031 993.744019,884.286072 + C993.828125,865.397949 993.927002,846.509888 993.948792,827.621704 + C993.949768,826.759338 993.256714,825.896179 992.164429,824.997620 + C991.445923,824.934082 990.727417,824.870544 990.020081,824.058044 + C990.161499,822.831421 990.302856,821.604858 990.448242,820.343445 + C982.647644,819.881042 975.221252,819.203003 967.784363,819.056091 + C956.798950,818.839172 945.806152,819.005676 934.816467,818.997986 + C927.887390,818.993103 927.003418,818.126038 927.002502,811.279541 + C926.997314,772.649170 926.977905,734.018738 927.050171,695.388550 + C927.054932,692.844177 927.780212,690.301147 928.170349,687.757568 + C926.761414,687.420166 925.436646,686.179260 925.334961,684.845093 + C925.280945,684.135986 927.249023,683.025024 928.479065,682.525269 + C930.236572,681.811157 932.150146,681.086304 934.004028,681.068054 + C946.657532,680.943481 959.353394,680.416260 971.948059,681.286194 + C977.121155,681.643555 983.255981,680.408142 987.788086,684.999695 + C988.296753,685.515015 990.272949,684.581909 991.568542,684.320496 + C991.068237,683.183777 990.856934,681.690552 989.999023,680.994812 + C988.750793,679.982544 987.036621,679.544800 985.460693,677.993042 + C985.535950,675.654846 985.611145,673.316711 986.528198,670.919006 + C987.350037,669.682129 988.867126,668.455627 988.885803,667.206726 + C989.066467,655.119934 989.097961,643.027222 988.851135,630.942810 + C988.824097,629.614624 986.788635,628.327515 985.479248,626.136841 + C985.456238,623.167908 985.433228,620.198975 986.221069,617.109436 + C986.984436,616.792419 987.747864,616.475464 988.511292,616.158508 + C988.365112,615.589355 988.218994,615.020203 988.072815,614.451111 + C984.379333,615.169067 980.685852,615.887085 976.070129,616.767151 +M321.141083,588.679810 + C321.041016,589.116699 320.940948,589.553528 320.150482,590.137390 + C319.113373,592.086426 317.018951,594.149231 317.214386,595.965637 + C318.310913,606.157593 315.244781,615.442139 311.997528,624.972229 + C310.374664,624.428772 308.944733,623.950012 307.684265,623.527954 + C307.684265,629.031677 307.684265,634.196167 307.684265,639.360657 + C310.072083,631.049011 310.072083,631.049011 314.536682,630.452820 + C313.330841,636.995300 312.210571,643.228760 311.023132,649.449402 + C310.442200,652.492798 308.784882,655.696899 309.327728,658.496033 + C310.006134,661.994324 309.392914,663.824463 305.747650,664.880432 + C306.810547,668.741882 307.912994,672.316223 308.714813,675.956787 + C308.920197,676.889343 308.508606,678.495422 307.809998,678.996155 + C304.849457,681.118530 305.735809,683.746216 306.253174,686.514343 + C306.674530,688.768494 307.047028,691.119446 306.865295,693.381287 + C306.417267,698.957275 305.716766,704.517334 304.963593,710.062927 + C304.482635,713.603821 304.766693,717.874695 302.861908,720.456421 + C299.614014,724.858704 297.772339,731.837952 299.770477,736.908203 + C300.811890,739.550659 300.747650,742.628906 301.182800,745.510376 + C300.331024,745.720764 299.479248,745.931152 298.627472,746.141541 + C297.761200,743.259155 296.894928,740.376831 296.028656,737.494507 + C295.402374,746.601624 289.043213,755.129578 294.556549,765.744995 + C294.897736,760.421204 295.154510,756.414368 295.411316,752.407532 + C295.933594,752.441467 296.455902,752.475464 296.978180,752.509399 + C299.841370,760.890747 296.534607,769.243713 296.104706,777.888489 + C294.650360,777.206360 293.606476,776.363098 293.119049,775.271362 + C292.616730,774.146301 292.656403,772.779236 292.458954,771.518066 + C292.125519,771.591675 291.792114,771.665222 291.458679,771.738831 + C291.458679,777.366943 291.458679,782.995056 291.458679,789.077271 + C293.207489,788.443542 294.396423,788.012756 296.297241,787.323975 + C295.556641,789.502380 295.489410,791.312012 294.550598,792.197205 + C288.934570,797.492126 290.080383,803.610168 291.895020,809.949097 + C292.223053,811.094971 292.157867,812.415710 292.011902,813.624084 + C291.398468,818.701599 290.458801,823.751160 290.101227,828.843445 + C289.912689,831.528931 290.676147,834.281372 291.136444,837.752747 + C291.016968,838.498047 290.897491,839.243347 290.098907,840.152466 + C290.076569,841.772949 290.054230,843.393372 290.159790,845.762817 + C290.032501,846.505310 289.905212,847.247864 289.054077,848.190308 + C287.537872,851.351135 286.021667,854.512024 284.505463,857.672852 + C284.227478,857.534668 283.949493,857.396484 283.671539,857.258301 + C281.890839,865.512878 280.110138,873.767395 278.195435,882.643127 + C277.059174,883.373413 274.863831,884.784302 271.917450,886.677856 + C275.604309,888.293396 278.866150,888.665710 279.834320,890.353638 + C282.311340,894.671997 286.128326,894.921753 290.036133,894.944824 + C309.348022,895.059082 328.660980,895.006409 347.973480,894.972595 + C349.122864,894.970581 350.271667,894.620850 351.420776,894.432922 + C351.384705,893.956116 351.348633,893.479248 351.312561,893.002441 + C344.450470,893.002441 337.588348,893.002502 330.726257,893.002502 + C323.567291,893.002502 316.407104,893.084961 309.249725,892.978333 + C302.570129,892.878784 295.819824,893.445129 289.200073,891.575623 + C290.861633,890.356934 292.516968,890.029846 294.173126,890.025757 + C309.989349,889.986755 325.807953,890.155273 341.620880,889.923950 + C347.594452,889.836609 353.162994,889.888733 357.011536,895.434937 + C357.490448,896.125122 358.576538,896.393921 359.369751,896.851318 + C362.997253,891.136963 362.866211,890.164673 358.132446,886.646973 + C358.253143,886.214355 358.235565,885.546936 358.541046,885.325989 + C359.306549,884.772034 360.281647,884.510071 361.055695,883.964905 + C362.039581,883.272034 363.741791,881.764893 363.708008,881.721436 + C359.557770,876.401306 369.067749,869.034729 361.131012,864.260803 + C360.448639,863.850403 360.304169,862.076721 360.377991,860.967529 + C360.584015,857.872437 361.036163,854.793762 361.402863,851.606750 + C362.825653,852.021118 364.088654,852.388977 365.752563,852.873657 + C365.567627,844.402832 360.964294,836.712585 364.004181,827.628845 + C364.833771,829.849915 365.332642,831.185486 366.306183,833.791992 + C367.384491,824.466553 368.290070,816.635010 369.195648,808.803528 + C368.622040,808.729553 368.048462,808.655518 367.474854,808.581543 + C367.133087,811.208191 366.791321,813.834778 366.449554,816.461426 + C365.971039,816.408264 365.492554,816.355042 365.014038,816.301880 + C365.014038,811.712830 364.268188,806.966187 365.196136,802.573120 + C366.455872,796.609436 368.996490,790.916199 371.814392,784.940002 + C373.214386,784.994690 374.614349,785.049377 376.632904,785.654968 + C382.776154,785.769958 388.919006,785.940491 395.062775,785.986206 + C403.036835,786.045532 403.835907,787.429199 401.980713,795.125610 + C401.083313,798.848450 400.128754,803.880859 401.861847,806.661072 + C406.665466,814.367065 405.850067,822.517822 405.895355,830.703491 + C405.901428,831.804932 404.810089,833.207031 403.834290,833.963135 + C401.110718,836.073242 401.232025,838.370605 404.507935,840.396179 + C404.894623,838.755798 405.273376,837.149109 405.652100,835.542419 + C406.008331,835.584778 406.364563,835.627197 406.720795,835.669617 + C407.133148,840.250366 407.545532,844.831177 407.957916,849.411926 + C407.565125,849.454590 407.172333,849.497253 406.779541,849.539917 + C405.878113,848.483459 404.976715,847.427002 403.724426,845.959351 + C403.724426,857.676331 403.724426,868.730591 403.724426,879.784790 + C404.345612,879.847168 404.966766,879.909546 405.587952,879.971924 + C406.289001,875.137085 406.990051,870.302246 407.691101,865.467407 + C408.286133,865.528992 408.881134,865.590637 409.476166,865.652222 + C409.476166,872.863403 409.476166,880.074585 409.476166,887.873718 + C405.223694,890.163330 404.200714,892.436401 406.225006,896.776062 + C407.220490,896.240540 416.118835,894.885864 417.907410,894.991028 + C418.239319,895.010559 418.573242,894.998779 418.906189,894.996643 + C439.534515,894.862000 460.162872,894.724854 480.791260,894.601685 + C481.398315,894.598083 482.036743,894.648499 482.608124,894.834534 + C484.419342,895.424561 487.365875,897.061218 487.791595,896.552124 + C489.272583,894.781372 490.747833,892.236755 490.647186,890.059326 + C490.366760,883.993713 489.294128,877.964661 488.540680,871.971741 + C490.056702,870.557983 491.648712,869.073364 493.240753,867.588684 + C492.695068,867.524231 492.149414,867.459778 491.603729,867.395325 + C490.732513,864.866211 489.908722,862.319153 488.966309,859.816833 + C488.596375,858.834656 488.194336,857.547424 487.406677,857.093323 + C483.596039,854.896423 484.894165,852.108215 485.900787,848.848450 + C487.611023,849.580017 488.895752,850.129578 490.229279,850.699951 + C491.909576,843.460449 491.360931,838.624390 488.713928,836.552124 + C488.095520,838.952576 487.476562,841.355103 486.857635,843.757629 + C486.237579,843.655273 485.617523,843.552917 484.997467,843.450562 + C484.997467,841.377380 484.856537,839.292297 485.031097,837.233948 + C485.281250,834.283630 484.364075,832.363953 480.991547,831.262390 + C480.988190,830.511414 480.984833,829.760437 481.580414,828.434387 + C481.505066,824.494568 481.875641,820.484070 481.265381,816.628967 + C479.815155,807.467896 477.937073,798.390137 479.416290,789.027466 + C479.594574,787.898987 478.968964,786.535461 478.428772,785.419922 + C475.859924,780.115356 474.423218,774.884033 477.966156,769.342834 + C478.640625,768.287964 478.856537,766.401978 478.397156,765.254578 + C476.574554,760.701782 476.034302,756.298523 477.566559,751.475586 + C478.550781,748.377625 478.793701,744.975525 478.891510,741.695496 + C478.936249,740.196533 477.829468,738.663330 477.244934,737.145630 + C476.497009,738.667114 475.247955,740.142517 475.103088,741.719360 + C474.777954,745.259338 475.001190,748.849609 475.001190,752.419434 + C474.513397,752.397827 474.025635,752.376160 473.537842,752.354553 + C473.064148,749.453796 471.712402,746.356262 472.308868,743.695374 + C473.382904,738.903992 471.366638,733.431885 475.863434,729.169800 + C477.127716,727.971619 476.343872,724.317688 476.045929,721.844788 + C475.714844,719.096191 474.482574,716.429871 474.340668,713.691528 + C474.171204,710.421753 475.011292,707.105774 474.915344,703.824402 + C474.822296,700.641541 473.559906,697.948181 469.635864,698.000366 + C466.333557,698.044250 466.280884,695.873596 465.908081,693.347839 + C465.294678,689.191833 464.089874,685.123047 462.894928,680.372314 + C462.922821,679.917847 462.950745,679.463379 463.565765,678.463135 + C463.565765,668.815552 463.565765,659.168030 463.565765,649.062744 + C463.168671,650.154602 462.795135,651.181702 462.421570,652.208801 + C461.825043,651.855408 461.228546,651.501953 460.632019,651.148560 + C461.484711,644.676453 462.337402,638.204407 463.380646,630.286072 + C461.346771,632.169495 460.562195,632.895996 458.981476,634.359741 + C458.820221,631.466492 458.989410,629.510986 458.520844,627.722595 + C458.165894,626.367859 456.997681,625.226196 455.973816,623.266541 + C455.972229,622.515747 455.970673,621.764893 456.577118,620.434998 + C455.797089,607.792175 455.017090,595.149414 454.223267,582.282654 + C455.353485,582.282654 457.041992,582.282654 460.375122,582.282654 + C458.179230,577.639648 456.385101,573.829102 454.574921,570.026184 + C453.737640,568.267151 452.955597,566.473694 451.991974,564.785156 + C450.860107,562.801880 449.558136,560.915710 448.064056,558.222046 + C448.049530,557.146362 448.034973,556.070679 448.583710,554.473450 + C448.350708,548.709839 448.039215,542.947815 447.920288,537.181824 + C447.866699,534.584473 448.387726,531.972900 448.301025,529.379333 + C448.211273,526.694397 447.395813,524.015198 447.469543,521.349243 + C447.530670,519.138000 448.422638,516.949829 449.036530,514.373779 + C447.140167,514.373779 445.793488,514.373779 443.870270,514.373779 + C448.112823,503.415680 446.794586,492.555328 446.720459,481.742584 + C446.254822,481.713867 445.789185,481.685181 445.323547,481.656464 + C445.098633,484.663971 444.873718,487.671509 444.648834,490.679016 + C444.060150,490.854553 443.471466,491.030090 442.882812,491.205597 + C442.257111,489.728088 441.169342,488.275604 441.087585,486.768616 + C440.772827,480.965332 443.021667,474.924072 439.143311,469.462097 + C438.671875,468.798157 438.857025,467.451233 439.111847,466.523560 + C440.822601,460.295288 442.478851,454.044373 444.470551,447.903870 + C445.709747,444.083374 444.357605,435.600372 442.065704,433.803009 + C440.743896,436.259186 439.376862,438.799377 437.525116,442.240265 + C435.592163,437.703613 434.375031,434.847046 432.939301,431.265350 + C432.953705,430.513458 432.968109,429.761536 433.582611,428.429352 + C433.582611,422.447754 433.582611,416.466125 433.582611,410.459442 + C430.634552,410.640167 429.090546,410.734802 426.760315,410.877655 + C428.542450,408.093964 429.847900,406.054871 431.651306,403.237946 + C429.232513,403.237946 427.942139,403.113831 426.683228,403.260223 + C422.111908,403.791779 417.555908,404.873901 412.985535,404.921387 + C394.370270,405.114807 375.751495,405.050476 357.135406,404.903931 + C355.735992,404.892914 354.333435,403.514221 352.958954,402.724915 + C352.059692,402.208466 351.185394,401.139221 350.342529,401.180786 + C349.540955,401.220337 348.251984,402.286072 348.114960,403.065430 + C347.393585,407.169128 344.094360,408.292847 340.961212,408.868622 + C336.185608,409.746277 337.016174,412.884674 337.277679,416.026459 + C337.648102,420.476410 338.009583,424.927124 338.359161,429.378784 + C338.584747,432.251160 338.782288,435.125702 339.110352,438.665955 + C339.030396,439.107513 338.950439,439.549103 338.159698,440.159180 + C338.122223,442.112762 338.084747,444.066345 338.170502,446.684082 + C338.073120,447.119812 337.975739,447.555573 337.144012,448.170135 + C336.525574,450.262360 335.907135,452.354584 335.254272,454.563354 + C333.798370,454.084320 332.333740,453.602386 330.998444,453.163025 + C329.992554,457.759857 328.484253,461.976410 328.285400,466.253845 + C328.048523,471.349609 325.281799,476.387268 328.465485,481.770477 + C329.743774,483.931824 328.675964,487.480743 328.675964,490.398102 + C328.357941,490.396637 328.039886,490.395142 327.721863,490.393677 + C326.988739,487.731659 326.255646,485.069641 325.316071,481.657959 + C324.464569,489.004425 322.837921,495.374847 323.256683,501.607849 + C323.665314,507.689423 326.179016,513.626709 327.720490,519.639709 + C327.866943,520.210999 327.670624,520.870178 327.618561,521.735229 + C326.009277,521.735229 324.653076,521.735229 322.325287,521.735229 + C323.324127,525.628357 324.190796,529.006165 325.084351,532.488831 + C324.246674,532.903687 323.162415,533.440613 323.394104,533.325867 + C323.691986,536.732544 324.070404,539.203735 324.052399,541.672058 + C324.043915,542.829285 323.091522,543.948792 322.877716,545.141113 + C322.207703,548.877136 321.655212,552.635376 321.126648,556.394836 + C320.989655,557.369202 320.690216,558.711182 321.174652,559.319397 + C324.446930,563.428040 321.849030,567.424072 321.417114,571.561462 + C320.849487,576.999268 321.117157,582.524231 321.141083,588.679810 +M653.149658,722.312500 + C653.149658,774.794556 653.149658,827.276611 653.149658,880.256775 + C647.727295,879.030457 647.986572,875.950806 647.987488,872.934448 + C648.001831,825.440674 647.930054,777.946716 648.102356,730.453552 + C648.119446,725.728821 649.403198,720.963074 650.507141,716.313293 + C650.877869,714.751709 652.268494,712.627502 653.618835,712.238342 + C660.593384,710.228638 667.705688,707.396057 674.684448,712.561462 + C675.304749,713.020569 676.809937,712.377625 677.873474,712.119934 + C679.112549,711.819580 680.313721,711.069336 681.538452,711.053223 + C687.839417,710.969971 694.142700,711.053589 700.444885,711.110046 + C700.715759,711.112488 700.983887,711.421936 701.404297,711.681702 + C701.132141,712.607361 700.849792,713.567810 700.426025,715.009216 + C702.164673,714.818420 703.377136,714.685364 703.258667,714.698364 + C704.511841,711.019775 704.778687,706.476929 706.550537,705.784302 + C710.796082,704.124695 715.092285,699.985718 720.180237,703.192932 + C723.628113,701.053528 723.894165,696.043335 729.368225,695.284729 + C732.357239,694.870544 735.412354,690.923401 737.361389,687.880249 + C741.007019,682.188232 744.680176,676.686829 750.229614,672.644592 + C751.355591,671.824341 752.174683,670.582947 752.312378,670.432800 + C751.533997,667.516846 750.368042,665.556641 750.692810,663.886292 + C751.476868,659.854553 752.929993,655.949585 754.186951,652.016968 + C754.372131,651.437744 754.991455,650.519836 755.353882,650.545593 + C761.754028,651.000732 759.448730,646.911133 759.077881,643.711548 + C758.995667,643.002380 759.616333,642.211670 760.054932,641.099548 + C761.111023,644.311646 761.811523,647.014221 762.988831,649.489990 + C763.197937,649.929810 766.496948,649.470520 766.501526,649.353333 + C766.596252,646.929443 767.324585,643.862366 766.141846,642.172180 + C761.044067,634.886841 759.275085,626.487122 759.184631,618.192627 + C758.763428,579.535583 759.045654,540.871338 758.932678,502.210052 + C758.920532,498.051208 759.669250,494.528992 762.185669,491.096985 + C765.427002,486.676270 764.283813,478.800201 759.831604,475.711121 + C756.574951,473.451569 752.428528,472.324738 754.579590,466.958801 + C754.729370,466.585175 753.521545,465.801453 753.230469,465.091400 + C752.414246,463.100494 751.217957,461.067139 751.144836,459.015869 + C751.032593,455.865723 751.732971,452.686584 752.078125,449.612854 + C749.692932,449.066010 747.476501,449.238007 746.641541,448.246582 + C743.336792,444.322937 740.508789,440.001587 737.403381,435.903900 + C736.556091,434.785950 735.490417,433.332214 734.295654,433.067474 + C729.613464,432.030060 727.754822,428.014130 724.724487,425.125732 + C721.714722,422.256927 721.107361,416.578674 714.377747,417.423462 + C708.798401,418.123901 702.912781,416.385040 695.722290,415.561920 + C698.492798,411.456970 700.457153,408.546387 702.574097,405.409821 + C699.764404,405.073456 697.222534,403.857880 695.350891,407.520844 + C694.804443,408.590393 691.166748,409.422546 690.149231,408.690674 + C683.720154,404.066071 676.144165,405.459351 669.376831,405.300598 + C640.063477,404.612885 610.724792,404.999786 581.395386,405.037628 + C580.451111,405.038849 579.507324,405.492676 578.563354,405.735535 + C578.610291,406.155792 578.657288,406.576080 578.704285,406.996338 + C580.447083,406.996338 582.189880,406.996338 583.932678,406.996338 + C612.262268,406.996338 640.592163,406.943390 668.921082,407.083282 + C671.577942,407.096405 674.228760,408.326508 676.882385,408.991577 + C676.838257,409.656525 676.794189,410.321442 676.750061,410.986389 + C675.473450,411.261139 674.187683,411.801147 672.922058,411.757385 + C669.956604,411.654846 667.001343,411.045532 664.041077,411.039032 + C636.711487,410.978851 609.381592,411.026245 582.052124,410.946411 + C580.405090,410.941589 578.698303,410.270630 577.133972,409.629639 + C575.830750,409.095642 574.696167,408.150146 573.486816,407.386902 + C573.324524,408.758820 573.162231,410.130707 572.814270,413.071747 + C571.442078,411.504303 570.338135,410.606110 569.711365,409.449341 + C568.524231,407.258545 566.995239,406.269501 565.262817,408.442078 + C564.100464,409.899750 563.260132,411.951599 563.106628,413.806427 + C562.782837,417.718323 563.002686,421.675385 563.002686,425.613922 + C563.002380,575.760803 563.002319,725.907715 563.004761,876.054626 + C563.004761,877.720459 563.004517,879.387878 563.080811,881.051025 + C563.100769,881.486633 563.407837,881.908997 563.623108,882.435486 + C564.608765,882.104919 565.471375,881.815552 567.565613,881.113159 + C566.692932,883.973877 565.987427,885.996155 565.474915,888.066162 + C565.117004,889.511658 564.400269,891.366150 564.977478,892.412842 + C565.623535,893.584290 567.483826,894.524231 568.931213,894.722778 + C571.799316,895.116211 574.746765,894.975708 577.661438,894.977661 + C600.158447,894.992737 622.655701,895.042664 645.152283,894.947571 + C649.867920,894.927612 655.635620,887.472839 655.103516,882.689575 + C654.920288,881.041443 655.001160,879.361328 655.001038,877.695801 + C654.996826,827.369080 655.010986,777.042358 654.939087,726.715698 + C654.936646,725.028076 654.072205,723.341675 653.149658,722.312500 +M841.000000,519.500000 + C841.000000,486.232758 841.000000,452.965485 841.000000,419.698242 + C840.565613,419.702942 840.131287,419.707642 839.696899,419.712341 + C839.696899,572.571655 839.696899,725.430908 839.696899,878.290222 + C840.131287,878.289978 840.565613,878.289795 841.000000,878.289612 + C841.000000,759.026428 841.000000,639.763184 841.000000,519.500000 +M988.076660,405.000732 + C942.283997,405.000732 896.491333,405.000732 850.698669,405.000732 + C850.704224,405.432587 850.709778,405.864410 850.715332,406.296265 + C897.578491,406.296265 944.441650,406.296265 991.304810,406.296265 + C991.313354,405.989868 991.321838,405.683441 991.330322,405.377045 + C990.538635,405.252594 989.746948,405.128143 988.076660,405.000732 +M761.005066,516.834595 + C761.005066,552.323792 761.005066,587.813049 761.005066,623.302246 + C761.901855,623.297302 762.798645,623.292358 763.695435,623.287415 + C763.695435,582.763428 763.695435,542.239380 763.695435,502.420807 + C762.892883,506.451111 761.950745,511.182373 761.005066,516.834595 +M953.500000,687.000000 + C960.781372,687.000000 968.062744,687.000000 975.344116,687.000000 + C975.322815,686.562317 975.301575,686.124695 975.280273,685.687012 + C960.418823,685.687012 945.557373,685.687012 930.695923,685.687012 + C930.695374,686.124695 930.694824,686.562317 930.694336,687.000000 + C937.962891,687.000000 945.231445,687.000000 953.500000,687.000000 +M683.761169,720.929260 + C687.423950,719.429688 691.086731,717.930115 694.749512,716.430542 + C694.629639,715.934998 694.509705,715.439453 694.389771,714.943909 + C689.524231,714.180542 684.658630,713.417114 679.304932,712.577148 + C680.943848,716.205933 681.964661,718.466248 683.761169,720.929260 +M310.681122,596.469421 + C312.368500,600.022827 308.536316,605.315369 314.733673,608.653076 + C314.380676,603.403015 314.182739,599.210510 313.728149,595.046021 + C313.660065,594.422363 312.388916,593.929932 311.674316,593.376831 + C311.246277,594.161560 310.818207,594.946289 310.681122,596.469421 +M460.002441,616.343201 + C460.002441,618.765198 460.002441,621.187195 460.002441,623.609192 + C460.668152,623.671997 461.333862,623.734802 461.999573,623.797607 + C462.646149,619.060242 463.292725,614.322876 463.992615,609.194580 + C458.730927,609.577515 460.392456,613.085144 460.002441,616.343201 +M448.733398,460.694885 + C447.132172,460.571808 444.483093,459.817352 444.110992,460.434906 + C442.498505,463.111023 442.062439,466.123932 445.217621,469.288757 + C446.591278,466.344788 447.734406,463.894867 448.733398,460.694885 +M745.414246,684.902466 + C744.563599,685.874512 743.712952,686.846558 742.531433,688.196777 + C748.711731,689.271606 750.632385,686.561218 748.812073,679.475952 + C747.554626,681.463135 746.670532,682.860352 745.414246,684.902466 +M289.002441,746.197571 + C289.157928,747.617493 289.313416,749.037354 289.468933,750.457275 + C289.789734,750.396301 290.110565,750.335327 290.431396,750.274292 + C290.431396,744.745789 290.431396,739.217285 290.431396,733.688782 + C289.955078,733.688904 289.478760,733.688965 289.002441,733.689087 + C289.002441,737.550293 289.002441,741.411560 289.002441,746.197571 +M669.822876,714.971008 + C670.023560,714.355042 670.224182,713.739136 670.424866,713.123169 + C666.509583,713.123169 662.594299,713.123169 658.679016,713.123169 + C658.674500,713.746460 658.669922,714.369751 658.665405,714.993042 + C662.103333,714.993042 665.541260,714.993042 669.822876,714.971008 +M730.355774,700.666870 + C732.171326,700.592163 734.147583,700.912659 735.731628,700.268616 + C736.474121,699.966675 736.460083,697.804260 736.788879,696.485046 + C736.284241,696.186340 735.779602,695.887573 735.274963,695.588867 + C733.726624,697.064209 732.178284,698.539551 730.355774,700.666870 +M273.274658,862.929749 + C273.927216,865.515076 274.579773,868.100403 275.232300,870.685669 + C275.727844,870.529053 276.223358,870.372375 276.718872,870.215698 + C276.676636,867.291199 278.303284,863.810974 273.274658,862.929749 +M310.665344,612.515381 + C309.887390,611.372803 309.109436,610.230164 308.331512,609.087585 + C307.917633,609.265564 307.503754,609.443542 307.089874,609.621460 + C307.192657,611.663940 307.295471,613.706360 307.398254,615.748840 + C307.849854,615.862793 308.301422,615.976807 308.753021,616.090820 + C309.302551,615.126526 309.852081,614.162231 310.665344,612.515381 +M305.002472,648.363037 + C305.171661,649.733154 305.340881,651.103271 305.510101,652.473389 + C305.844421,652.407593 306.178741,652.341736 306.513062,652.275879 + C306.513062,649.077332 306.513062,645.878845 306.513062,642.680298 + C306.109741,642.652588 305.706451,642.624817 305.303131,642.597107 + C305.202911,644.231384 305.102692,645.865723 305.002472,648.363037 +M761.232361,467.254822 + C762.390991,468.262238 763.549561,469.269684 764.708191,470.277100 + C764.842651,464.603333 764.494568,464.385956 761.232361,467.254822 +M322.635315,464.549042 + C322.706909,466.562531 322.778473,468.576019 322.850067,470.589478 + C323.664703,470.458588 324.479370,470.327667 325.294037,470.196747 + C324.654602,468.270477 324.015198,466.344177 322.635315,464.549042 +M748.272949,437.581024 + C748.012756,436.104248 747.752502,434.627502 747.492249,433.150757 + C746.776184,433.403625 746.060120,433.656525 745.344055,433.909363 + C746.079956,435.213379 746.815857,436.517426 748.272949,437.581024 +M765.005798,628.502686 + C765.190674,629.510681 765.375549,630.518677 765.560425,631.526672 + C765.977234,631.491943 766.393982,631.457275 766.810791,631.422546 + C766.810791,629.479370 766.810791,627.536133 766.810791,625.592957 + C766.352295,625.580200 765.893738,625.567383 765.435242,625.554626 + C765.293030,626.290039 765.150818,627.025452 765.005798,628.502686 +M293.571167,726.413452 + C296.854980,724.331238 296.865448,723.915344 293.163605,721.172791 + C293.163605,722.980652 293.163605,724.401367 293.571167,726.413452 +M367.002991,877.393799 + C367.218262,878.414734 367.433533,879.435669 367.648804,880.456604 + C367.922180,880.386902 368.195557,880.317261 368.468933,880.247620 + C368.468933,878.042908 368.468933,875.838196 368.468933,873.633545 + C368.158783,873.618591 367.848663,873.603699 367.538513,873.588806 + C367.359985,874.594055 367.181488,875.599365 367.002991,877.393799 +M460.177338,594.135559 + C460.177338,595.609009 460.177338,597.082520 460.177338,598.555969 + C463.174652,596.616577 464.682770,594.765503 460.177338,594.135559 +M448.819366,438.814850 + C448.819366,437.065155 448.819366,435.315430 448.819366,433.565735 + C448.145508,433.687317 447.471649,433.808868 446.797821,433.930450 + C447.317413,435.753906 447.837006,437.577393 448.819366,438.814850 +M477.036224,707.945374 + C477.116669,709.440857 477.197113,710.936340 477.277557,712.431824 + C477.918671,712.228882 478.559784,712.025879 479.200867,711.822876 + C478.522461,710.284363 477.844055,708.745850 477.036224,707.945374 +M480.997223,779.930542 + C480.943573,779.284180 480.889923,778.637817 480.836273,777.991455 + C480.485291,777.965698 480.134277,777.939880 479.783264,777.914124 + C479.602997,779.699158 479.422699,781.484131 479.242432,783.269104 + C479.686829,783.319336 480.131226,783.369507 480.575623,783.419678 + C480.715759,782.496582 480.855927,781.573425 480.997223,779.930542 +M833.287720,886.127686 + C834.129272,885.977905 834.970825,885.828125 835.812378,885.678284 + C835.345886,884.927185 834.879395,884.176086 834.412903,883.425110 + C833.971497,884.098145 833.530029,884.771179 833.287720,886.127686 +M735.170044,421.220215 + C734.556824,421.838379 733.943604,422.456573 733.330383,423.074768 + C734.175354,423.315125 735.020325,423.555481 735.865295,423.795837 + C735.842773,423.041504 735.820190,422.287170 735.170044,421.220215 +M689.791321,401.055878 + C689.388672,401.428680 688.986023,401.801483 688.583374,402.174286 + C689.847656,402.371246 691.112000,402.568176 692.376282,402.765106 + C692.423157,402.375610 692.469971,401.986115 692.516846,401.596619 + C691.844543,401.406250 691.172302,401.215851 689.791321,401.055878 +M992.362976,673.575928 + C991.905823,674.764282 991.448669,675.952698 990.991577,677.141113 + C991.527222,677.248291 992.062927,677.355469 992.598572,677.462646 + C992.668884,676.346680 992.739136,675.230652 992.362976,673.575928 +M767.054626,485.209137 + C767.427734,485.611969 767.800842,486.014801 768.173950,486.417633 + C768.371094,485.152740 768.568176,483.887817 768.765259,482.622925 + C768.375549,482.576019 767.985840,482.529114 767.596130,482.482208 + C767.405518,483.154816 767.214844,483.827454 767.054626,485.209137 +M767.209045,473.078003 + C767.323730,474.218140 767.438416,475.358276 767.553101,476.498444 + C768.087646,476.352600 768.622192,476.206757 769.156677,476.060913 + C768.660278,474.890594 768.163879,473.720306 767.209045,473.078003 +M753.385925,687.106567 + C752.724670,686.998047 752.063354,686.889587 751.402161,686.781128 + C751.467163,687.351379 751.532166,687.921570 751.597107,688.491821 + C752.282593,688.226990 752.968140,687.962158 753.385925,687.106567 +M978.645935,688.927063 + C979.234070,688.580505 979.822266,688.233948 980.410461,687.887451 + C979.904785,687.619263 979.130432,687.006836 978.946350,687.150574 + C978.486084,687.509949 978.293640,688.212341 978.645935,688.927063 +M725.359497,703.675049 + C726.039490,703.946472 726.719482,704.217957 727.399475,704.489441 + C727.468750,703.919617 727.537964,703.349792 727.607239,702.779968 + C726.947876,702.881714 726.288513,702.983459 725.359497,703.675049 +M988.532776,613.303894 + C989.201416,612.958496 989.870056,612.613159 990.538635,612.267761 + C990.310669,611.914185 990.082703,611.560547 989.854675,611.207031 + C989.292419,611.749207 988.730164,612.291382 988.532776,613.303894 +M448.731171,444.902802 + C448.232147,444.957764 447.733124,445.012726 447.234100,445.067688 + C447.348846,445.514801 447.463562,445.961914 447.578278,446.409058 + C448.004303,446.106445 448.430328,445.803833 448.731171,444.902802 +M401.142670,819.490356 + C401.569366,819.793640 401.996063,820.096863 402.422760,820.400085 + C402.536438,819.951416 402.650085,819.502686 402.763733,819.054016 + C402.264160,819.000366 401.764618,818.946655 401.142670,819.490356 +M765.143860,495.490173 + C765.569946,495.793030 765.995972,496.095886 766.422058,496.398743 + C766.535461,495.950592 766.648865,495.502441 766.762268,495.054352 + C766.263489,495.000763 765.764771,494.947174 765.143860,495.490173 +M463.142853,575.490540 + C463.569366,575.793640 463.995880,576.096741 464.422424,576.399780 + C464.535950,575.951294 464.649475,575.502747 464.763000,575.054260 + C464.263702,575.000671 463.764404,574.947021 463.142853,575.490540 +M495.275421,890.881104 + C495.467651,891.424805 495.659882,891.968567 495.852081,892.512268 + C496.130768,892.384827 496.409424,892.257385 496.688110,892.129883 + C496.362213,891.570190 496.036316,891.010498 495.275421,890.881104 +M991.703064,624.570007 + C991.989624,624.296509 992.276123,624.023010 992.562683,623.749573 + C992.449219,623.631042 992.243530,623.403198 992.235718,623.409729 + C991.933594,623.664978 991.649353,623.941467 991.703064,624.570007 +M495.432281,862.703918 + C495.704712,862.989258 495.977142,863.274597 496.249542,863.559937 + C496.367645,863.446960 496.594574,863.242004 496.588104,863.234314 + C496.333893,862.933350 496.058380,862.650391 495.432281,862.703918 +M992.296814,619.430969 + C992.010681,619.704102 991.724548,619.977173 991.438416,620.250244 + C991.551636,620.368652 991.757080,620.596252 991.764832,620.589722 + C992.066589,620.334839 992.350342,620.058594 992.296814,619.430969 +M977.430786,609.703308 + C977.703918,609.989441 977.977051,610.275574 978.250183,610.561707 + C978.368591,610.448425 978.596191,610.242981 978.589661,610.235229 + C978.334778,609.933472 978.058533,609.649719 977.430786,609.703308 +M927.430298,609.703064 + C927.703613,609.989441 927.976990,610.275818 928.250305,610.562073 + C928.368835,610.448730 928.596558,610.243103 928.590088,610.235474 + C928.334961,609.933411 928.058533,609.649414 927.430298,609.703064 +M463.391937,580.202026 + C463.785858,580.206177 464.179779,580.210327 464.573730,580.214539 + C464.575256,580.051086 464.587708,579.745483 464.576691,579.744629 + C464.184723,579.714478 463.790527,579.712830 463.391937,580.202026 +M571.295410,401.434235 + C571.010864,401.705750 570.726257,401.977295 570.441711,402.248810 + C570.554321,402.366577 570.758606,402.592896 570.766235,402.586456 + C571.066345,402.333038 571.348572,402.058472 571.295410,401.434235 +z"/> +<path fill="#0455F9" opacity="1.000000" stroke="none" + d=" +M320.840912,589.990417 + C320.940948,589.553528 321.041016,589.116699 321.282532,588.004639 + C325.278198,555.265930 329.161438,523.205933 332.973450,491.137390 + C334.682007,476.764099 336.247803,462.373840 337.878357,447.991302 + C337.975739,447.555573 338.073120,447.119812 338.301788,446.026459 + C338.578888,443.576111 338.724701,441.783386 338.870483,439.990662 + C338.950439,439.549103 339.030396,439.107513 339.238953,437.985596 + C340.349884,429.603516 341.332245,421.901825 342.302612,414.294067 + C372.070343,414.294067 401.383453,414.294067 430.540558,414.294067 + C431.409393,419.529785 432.195984,424.269714 432.982544,429.009644 + C432.968109,429.761536 432.953705,430.513458 432.990479,432.059143 + C436.259186,459.908722 439.428833,486.970337 442.714233,514.017883 + C444.375000,527.690002 446.245026,541.336792 448.020416,554.994995 + C448.034973,556.070679 448.049530,557.146362 448.128174,559.024170 + C449.535095,571.034058 450.821655,582.248901 452.238098,593.447327 + C453.401672,602.646362 454.719604,611.825928 455.969116,621.014099 + C455.970673,621.764893 455.972229,622.515747 456.031616,624.053589 + C456.402985,627.471680 456.696716,630.105286 457.033386,632.733276 + C459.009583,648.159180 460.995789,663.583862 462.978638,679.008911 + C462.950745,679.463379 462.922821,679.917847 462.957397,681.125366 + C467.541168,719.556885 472.039642,757.238098 476.597656,794.912109 + C477.973999,806.288147 479.515594,817.644226 480.981445,829.009460 + C480.984833,829.760437 480.988190,830.511414 481.038513,832.049683 + C483.248413,851.070190 485.411346,869.303345 487.598206,887.738098 + C462.232330,887.738098 437.818695,887.738098 412.804779,887.738098 + C410.011200,853.416199 407.235718,819.316406 404.451080,785.104126 + C394.617676,785.104126 385.316010,785.104126 376.014343,785.104126 + C374.614349,785.049377 373.214386,784.994690 370.971832,784.929810 + C368.366058,784.919678 366.602814,784.919678 364.661255,784.919678 + C361.853058,819.625549 359.096344,853.695007 356.355225,887.571655 + C332.250122,887.571655 309.038086,887.571655 285.511566,887.571655 + C286.965759,874.080200 288.371857,861.035278 289.777893,847.990356 + C289.905212,847.247864 290.032501,846.505310 290.270660,845.068726 + C290.513702,842.912659 290.645874,841.450684 290.778015,839.988647 + C290.897491,839.243347 291.016968,838.498047 291.264587,837.033569 + C295.188873,805.267456 299.019104,774.224670 302.772156,743.172546 + C307.045502,707.815491 311.277618,672.453491 315.471649,637.086914 + C317.332306,621.396484 319.055359,605.689758 320.840912,589.990417 +M396.284454,687.029175 + C392.485443,638.966553 388.686462,590.903931 384.887451,542.841309 + C384.403320,542.856018 383.919189,542.870728 383.435059,542.885437 + C378.972534,602.429565 374.510010,661.973633 370.039307,721.626770 + C380.025177,721.626770 389.410980,721.626770 399.267029,721.626770 + C398.299713,710.118774 397.367371,699.026428 396.284454,687.029175 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M653.379639,721.983582 + C654.072205,723.341675 654.936646,725.028076 654.939087,726.715698 + C655.010986,777.042358 654.996826,827.369080 655.001038,877.695801 + C655.001160,879.361328 654.920288,881.041443 655.103516,882.689575 + C655.635620,887.472839 649.867920,894.927612 645.152283,894.947571 + C622.655701,895.042664 600.158447,894.992737 577.661438,894.977661 + C574.746765,894.975708 571.799316,895.116211 568.931213,894.722778 + C567.483826,894.524231 565.623535,893.584290 564.977478,892.412842 + C564.400269,891.366150 565.117004,889.511658 565.474915,888.066162 + C565.987427,885.996155 566.692932,883.973877 567.565613,881.113159 + C565.471375,881.815552 564.608765,882.104919 563.623108,882.435486 + C563.407837,881.908997 563.100769,881.486633 563.080811,881.051025 + C563.004517,879.387878 563.004761,877.720459 563.004761,876.054626 + C563.002319,725.907715 563.002380,575.760803 563.002686,425.613922 + C563.002686,421.675385 562.782837,417.718323 563.106628,413.806427 + C563.260132,411.951599 564.100464,409.899750 565.262817,408.442078 + C566.995239,406.269501 568.524231,407.258545 569.711365,409.449341 + C570.338135,410.606110 571.442078,411.504303 572.814270,413.071747 + C573.162231,410.130707 573.324524,408.758820 573.486816,407.386902 + C574.696167,408.150146 575.830750,409.095642 577.133972,409.629639 + C578.698303,410.270630 580.405090,410.941589 582.052124,410.946411 + C609.381592,411.026245 636.711487,410.978851 664.041077,411.039032 + C667.001343,411.045532 669.956604,411.654846 672.922058,411.757385 + C674.187683,411.801147 675.473450,411.261139 676.750061,410.986389 + C676.794189,410.321442 676.838257,409.656525 676.882385,408.991577 + C674.228760,408.326508 671.577942,407.096405 668.921082,407.083282 + C640.592163,406.943390 612.262268,406.996338 583.932678,406.996338 + C582.189880,406.996338 580.447083,406.996338 578.704285,406.996338 + C578.657288,406.576080 578.610291,406.155792 578.563354,405.735535 + C579.507324,405.492676 580.451111,405.038849 581.395386,405.037628 + C610.724792,404.999786 640.063477,404.612885 669.376831,405.300598 + C676.144165,405.459351 683.720154,404.066071 690.149231,408.690674 + C691.166748,409.422546 694.804443,408.590393 695.350891,407.520844 + C697.222534,403.857880 699.764404,405.073456 702.574097,405.409821 + C700.457153,408.546387 698.492798,411.456970 695.722290,415.561920 + C702.912781,416.385040 708.798401,418.123901 714.377747,417.423462 + C721.107361,416.578674 721.714722,422.256927 724.724487,425.125732 + C727.754822,428.014130 729.613464,432.030060 734.295654,433.067474 + C735.490417,433.332214 736.556091,434.785950 737.403381,435.903900 + C740.508789,440.001587 743.336792,444.322937 746.641541,448.246582 + C747.476501,449.238007 749.692932,449.066010 752.078125,449.612854 + C751.732971,452.686584 751.032593,455.865723 751.144836,459.015869 + C751.217957,461.067139 752.414246,463.100494 753.230469,465.091400 + C753.521545,465.801453 754.729370,466.585175 754.579590,466.958801 + C752.428528,472.324738 756.574951,473.451569 759.831604,475.711121 + C764.283813,478.800201 765.427002,486.676270 762.185669,491.096985 + C759.669250,494.528992 758.920532,498.051208 758.932678,502.210052 + C759.045654,540.871338 758.763428,579.535583 759.184631,618.192627 + C759.275085,626.487122 761.044067,634.886841 766.141846,642.172180 + C767.324585,643.862366 766.596252,646.929443 766.501526,649.353333 + C766.496948,649.470520 763.197937,649.929810 762.988831,649.489990 + C761.811523,647.014221 761.111023,644.311646 760.054932,641.099548 + C759.616333,642.211670 758.995667,643.002380 759.077881,643.711548 + C759.448730,646.911133 761.754028,651.000732 755.353882,650.545593 + C754.991455,650.519836 754.372131,651.437744 754.186951,652.016968 + C752.929993,655.949585 751.476868,659.854553 750.692810,663.886292 + C750.368042,665.556641 751.533997,667.516846 752.312378,670.432800 + C752.174683,670.582947 751.355591,671.824341 750.229614,672.644592 + C744.680176,676.686829 741.007019,682.188232 737.361389,687.880249 + C735.412354,690.923401 732.357239,694.870544 729.368225,695.284729 + C723.894165,696.043335 723.628113,701.053528 720.180237,703.192932 + C715.092285,699.985718 710.796082,704.124695 706.550537,705.784302 + C704.778687,706.476929 704.511841,711.019775 703.258667,714.698364 + C703.377136,714.685364 702.164673,714.818420 700.426025,715.009216 + C700.849792,713.567810 701.132141,712.607361 701.404297,711.681702 + C700.983887,711.421936 700.715759,711.112488 700.444885,711.110046 + C694.142700,711.053589 687.839417,710.969971 681.538452,711.053223 + C680.313721,711.069336 679.112549,711.819580 677.873474,712.119934 + C676.809937,712.377625 675.304749,713.020569 674.684448,712.561462 + C667.705688,707.396057 660.593384,710.228638 653.618835,712.238342 + C652.268494,712.627502 650.877869,714.751709 650.507141,716.313293 + C649.403198,720.963074 648.119446,725.728821 648.102356,730.453552 + C647.930054,777.946716 648.001831,825.440674 647.987488,872.934448 + C647.986572,875.950806 647.727295,879.030457 653.149658,880.256775 + C653.149658,827.276611 653.149658,774.794556 653.379639,721.983582 +M644.798157,800.500427 + C644.798157,770.206970 644.798157,739.913574 644.798157,708.985962 + C655.330627,708.985962 665.306030,709.342957 675.246826,708.908752 + C693.738342,708.101074 711.156799,703.563416 726.061768,691.955200 + C746.173645,676.291687 754.996765,654.672913 755.781067,630.101746 + C756.801208,598.140259 756.299316,566.128845 756.353943,534.138733 + C756.377014,520.644287 756.669678,507.120300 755.917847,493.661194 + C754.663086,471.198975 747.864380,450.759491 730.805725,435.028259 + C713.669006,419.225006 692.636047,413.976898 670.004761,413.874237 + C638.178406,413.729858 606.350830,413.831482 574.523804,413.827911 + C572.750854,413.827728 570.977966,413.827850 569.073975,413.827850 + C569.073975,572.189697 569.073975,729.870789 569.073975,887.690063 + C594.380310,887.690063 619.298035,887.690063 644.798706,887.690063 + C644.798706,858.751892 644.798706,830.126160 644.798157,800.500427 +M759.235107,638.596802 + C759.349487,637.148926 759.463867,635.701050 759.578247,634.253174 + C758.814026,634.331238 758.049744,634.409241 757.285522,634.487244 + C757.689941,635.921692 758.094299,637.356079 759.235107,638.596802 +z"/> +<path fill="#356DE2" opacity="1.000000" stroke="none" + d=" +M993.211914,475.952942 + C991.492371,476.262482 989.773865,476.834442 988.053040,476.841431 + C968.676514,476.920319 949.299500,476.912415 929.025452,476.931396 + C925.760315,476.964813 923.392395,476.994629 920.941528,476.937500 + C920.858582,476.850555 920.693604,476.675873 920.857300,476.360382 + C923.266052,471.957672 926.492004,475.267792 929.106079,475.230255 + C948.432373,474.952850 967.765686,474.965027 987.093750,475.170319 + C991.155457,475.213440 992.160034,473.883240 992.116882,470.032074 + C991.935852,453.869995 991.843811,437.700897 992.160767,421.542999 + C992.255493,416.714600 990.580383,415.812927 986.187317,415.827118 + C942.028137,415.969910 897.868286,415.913849 853.708618,415.914642 + C845.935242,415.914795 845.931702,415.916656 845.931641,423.498779 + C845.930969,574.141541 845.930603,724.784363 845.937195,875.427124 + C845.937317,877.257629 845.840271,879.117432 846.118286,880.912476 + C846.663391,884.432190 848.331482,886.175659 852.446350,886.161438 + C897.272034,886.005920 942.098511,886.028137 986.924500,886.131897 + C990.654480,886.140564 992.199585,885.392578 992.140381,881.209412 + C991.902100,864.382080 991.936951,847.548340 992.121033,830.719666 + C992.162170,826.959839 991.014038,825.893250 987.297058,825.929138 + C969.301575,826.103210 951.303406,826.030334 933.306458,825.975952 + C931.539917,825.970581 929.774902,825.493835 928.466980,825.132874 + C949.286194,824.955566 969.647522,824.881348 990.008911,824.807007 + C990.727417,824.870544 991.445923,824.934082 992.600098,825.476685 + C993.113281,845.620117 993.190857,865.284607 993.268433,884.949036 + C993.195496,885.707031 993.122498,886.465088 992.552551,887.654419 + C942.835754,888.085693 893.615967,888.085693 844.106201,888.085693 + C844.106201,885.666382 844.106201,884.181763 844.106262,882.697205 + C844.113220,727.890381 844.117493,573.083496 844.162659,418.276672 + C844.163147,416.693512 844.700745,415.110474 844.987549,413.527344 + C846.388489,413.510986 847.789368,413.494629 850.056641,413.481873 + C895.625305,413.505371 940.327576,413.528931 985.029907,413.534698 + C987.349060,413.535004 989.668274,413.384216 991.987427,413.303772 + C992.372009,414.011993 992.756592,414.720245 993.137573,416.200745 + C993.159912,436.632965 993.185913,456.292969 993.211914,475.952942 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M990.014526,824.432556 + C969.647522,824.881348 949.286194,824.955566 928.099976,825.074890 + C926.535645,825.160583 925.796204,825.201233 924.664185,825.136597 + C923.189331,825.017944 922.107117,825.004578 921.015686,824.993530 + C921.006470,824.995850 921.004211,824.977417 920.950256,824.513184 + C920.894104,776.029175 920.891846,728.009399 920.941650,679.986938 + C920.993713,679.984253 921.002258,679.880371 921.470215,679.902222 + C941.246094,679.923035 960.554077,679.951233 979.861633,679.874634 + C981.749451,679.867188 983.634705,679.215027 985.521179,678.862061 + C987.036621,679.544800 988.750793,679.982544 989.999023,680.994812 + C990.856934,681.690552 991.068237,683.183777 991.568542,684.320496 + C990.272949,684.581909 988.296753,685.515015 987.788086,684.999695 + C983.255981,680.408142 977.121155,681.643555 971.948059,681.286194 + C959.353394,680.416260 946.657532,680.943481 934.004028,681.068054 + C932.150146,681.086304 930.236572,681.811157 928.479065,682.525269 + C927.249023,683.025024 925.280945,684.135986 925.334961,684.845093 + C925.436646,686.179260 926.761414,687.420166 928.170349,687.757568 + C927.780212,690.301147 927.054932,692.844177 927.050171,695.388550 + C926.977905,734.018738 926.997314,772.649170 927.002502,811.279541 + C927.003418,818.126038 927.887390,818.993103 934.816467,818.997986 + C945.806152,819.005676 956.798950,818.839172 967.784363,819.056091 + C975.221252,819.203003 982.647644,819.881042 990.448242,820.343445 + C990.302856,821.604858 990.161499,822.831421 990.014526,824.432556 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M376.323608,785.379517 + C385.316010,785.104126 394.617676,785.104126 404.451080,785.104126 + C407.235718,819.316406 410.011200,853.416199 412.804779,887.738098 + C437.818695,887.738098 462.232330,887.738098 487.598206,887.738098 + C485.411346,869.303345 483.248413,851.070190 481.144836,832.412109 + C484.364075,832.363953 485.281250,834.283630 485.031097,837.233948 + C484.856537,839.292297 484.997467,841.377380 484.997467,843.450562 + C485.617523,843.552917 486.237579,843.655273 486.857635,843.757629 + C487.476562,841.355103 488.095520,838.952576 488.713928,836.552124 + C491.360931,838.624390 491.909576,843.460449 490.229279,850.699951 + C488.895752,850.129578 487.611023,849.580017 485.900787,848.848450 + C484.894165,852.108215 483.596039,854.896423 487.406677,857.093323 + C488.194336,857.547424 488.596375,858.834656 488.966309,859.816833 + C489.908722,862.319153 490.732513,864.866211 491.603729,867.395325 + C492.149414,867.459778 492.695068,867.524231 493.240753,867.588684 + C491.648712,869.073364 490.056702,870.557983 488.540680,871.971741 + C489.294128,877.964661 490.366760,883.993713 490.647186,890.059326 + C490.747833,892.236755 489.272583,894.781372 487.791595,896.552124 + C487.365875,897.061218 484.419342,895.424561 482.608124,894.834534 + C482.036743,894.648499 481.398315,894.598083 480.791260,894.601685 + C460.162872,894.724854 439.534515,894.862000 418.906189,894.996643 + C418.573242,894.998779 418.239319,895.010559 417.907410,894.991028 + C416.118835,894.885864 407.220490,896.240540 406.225006,896.776062 + C404.200714,892.436401 405.223694,890.163330 409.476166,887.873718 + C409.476166,880.074585 409.476166,872.863403 409.476166,865.652222 + C408.881134,865.590637 408.286133,865.528992 407.691101,865.467407 + C406.990051,870.302246 406.289001,875.137085 405.587952,879.971924 + C404.966766,879.909546 404.345612,879.847168 403.724426,879.784790 + C403.724426,868.730591 403.724426,857.676331 403.724426,845.959351 + C404.976715,847.427002 405.878113,848.483459 406.779541,849.539917 + C407.172333,849.497253 407.565125,849.454590 407.957916,849.411926 + C407.545532,844.831177 407.133148,840.250366 406.720795,835.669617 + C406.364563,835.627197 406.008331,835.584778 405.652100,835.542419 + C405.273376,837.149109 404.894623,838.755798 404.507935,840.396179 + C401.232025,838.370605 401.110718,836.073242 403.834290,833.963135 + C404.810089,833.207031 405.901428,831.804932 405.895355,830.703491 + C405.850067,822.517822 406.665466,814.367065 401.861847,806.661072 + C400.128754,803.880859 401.083313,798.848450 401.980713,795.125610 + C403.835907,787.429199 403.036835,786.045532 395.062775,785.986206 + C388.919006,785.940491 382.776154,785.769958 376.323608,785.379517 +M405.259125,859.974365 + C406.479431,859.717896 407.699707,859.461426 408.920013,859.204895 + C408.284729,857.436035 407.649475,855.667175 407.014221,853.898315 + C406.362915,854.081360 405.711639,854.264343 405.060333,854.447388 + C405.060333,856.024719 405.060333,857.602051 405.259125,859.974365 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M289.416016,848.090332 + C288.371857,861.035278 286.965759,874.080200 285.511566,887.571655 + C309.038086,887.571655 332.250122,887.571655 356.355225,887.571655 + C359.096344,853.695007 361.853058,819.625549 364.661255,784.919678 + C366.602814,784.919678 368.366058,784.919678 370.558472,785.013306 + C368.996490,790.916199 366.455872,796.609436 365.196136,802.573120 + C364.268188,806.966187 365.014038,811.712830 365.014038,816.301880 + C365.492554,816.355042 365.971039,816.408264 366.449554,816.461426 + C366.791321,813.834778 367.133087,811.208191 367.474854,808.581543 + C368.048462,808.655518 368.622040,808.729553 369.195648,808.803528 + C368.290070,816.635010 367.384491,824.466553 366.306183,833.791992 + C365.332642,831.185486 364.833771,829.849915 364.004181,827.628845 + C360.964294,836.712585 365.567627,844.402832 365.752563,852.873657 + C364.088654,852.388977 362.825653,852.021118 361.402863,851.606750 + C361.036163,854.793762 360.584015,857.872437 360.377991,860.967529 + C360.304169,862.076721 360.448639,863.850403 361.131012,864.260803 + C369.067749,869.034729 359.557770,876.401306 363.708008,881.721436 + C363.741791,881.764893 362.039581,883.272034 361.055695,883.964905 + C360.281647,884.510071 359.306549,884.772034 358.541046,885.325989 + C358.235565,885.546936 358.253143,886.214355 358.132446,886.646973 + C362.866211,890.164673 362.997253,891.136963 359.369751,896.851318 + C358.576538,896.393921 357.490448,896.125122 357.011536,895.434937 + C353.162994,889.888733 347.594452,889.836609 341.620880,889.923950 + C325.807953,890.155273 309.989349,889.986755 294.173126,890.025757 + C292.516968,890.029846 290.861633,890.356934 289.200073,891.575623 + C295.819824,893.445129 302.570129,892.878784 309.249725,892.978333 + C316.407104,893.084961 323.567291,893.002502 330.726257,893.002502 + C337.588348,893.002502 344.450470,893.002441 351.312561,893.002441 + C351.348633,893.479248 351.384705,893.956116 351.420776,894.432922 + C350.271667,894.620850 349.122864,894.970581 347.973480,894.972595 + C328.660980,895.006409 309.348022,895.059082 290.036133,894.944824 + C286.128326,894.921753 282.311340,894.671997 279.834320,890.353638 + C278.866150,888.665710 275.604309,888.293396 271.917450,886.677856 + C274.863831,884.784302 277.059174,883.373413 278.195435,882.643127 + C280.110138,873.767395 281.890839,865.512878 283.671539,857.258301 + C283.949493,857.396484 284.227478,857.534668 284.505463,857.672852 + C286.021667,854.512024 287.537872,851.351135 289.416016,848.090332 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M320.495697,590.063904 + C319.055359,605.689758 317.332306,621.396484 315.471649,637.086914 + C311.277618,672.453491 307.045502,707.815491 302.772156,743.172546 + C299.019104,774.224670 295.188873,805.267456 291.201233,836.658936 + C290.676147,834.281372 289.912689,831.528931 290.101227,828.843445 + C290.458801,823.751160 291.398468,818.701599 292.011902,813.624084 + C292.157867,812.415710 292.223053,811.094971 291.895020,809.949097 + C290.080383,803.610168 288.934570,797.492126 294.550598,792.197205 + C295.489410,791.312012 295.556641,789.502380 296.297241,787.323975 + C294.396423,788.012756 293.207489,788.443542 291.458679,789.077271 + C291.458679,782.995056 291.458679,777.366943 291.458679,771.738831 + C291.792114,771.665222 292.125519,771.591675 292.458954,771.518066 + C292.656403,772.779236 292.616730,774.146301 293.119049,775.271362 + C293.606476,776.363098 294.650360,777.206360 296.104706,777.888489 + C296.534607,769.243713 299.841370,760.890747 296.978180,752.509399 + C296.455902,752.475464 295.933594,752.441467 295.411316,752.407532 + C295.154510,756.414368 294.897736,760.421204 294.556549,765.744995 + C289.043213,755.129578 295.402374,746.601624 296.028656,737.494507 + C296.894928,740.376831 297.761200,743.259155 298.627472,746.141541 + C299.479248,745.931152 300.331024,745.720764 301.182800,745.510376 + C300.747650,742.628906 300.811890,739.550659 299.770477,736.908203 + C297.772339,731.837952 299.614014,724.858704 302.861908,720.456421 + C304.766693,717.874695 304.482635,713.603821 304.963593,710.062927 + C305.716766,704.517334 306.417267,698.957275 306.865295,693.381287 + C307.047028,691.119446 306.674530,688.768494 306.253174,686.514343 + C305.735809,683.746216 304.849457,681.118530 307.809998,678.996155 + C308.508606,678.495422 308.920197,676.889343 308.714813,675.956787 + C307.912994,672.316223 306.810547,668.741882 305.747650,664.880432 + C309.392914,663.824463 310.006134,661.994324 309.327728,658.496033 + C308.784882,655.696899 310.442200,652.492798 311.023132,649.449402 + C312.210571,643.228760 313.330841,636.995300 314.536682,630.452820 + C310.072083,631.049011 310.072083,631.049011 307.684265,639.360657 + C307.684265,634.196167 307.684265,629.031677 307.684265,623.527954 + C308.944733,623.950012 310.374664,624.428772 311.997528,624.972229 + C315.244781,615.442139 318.310913,606.157593 317.214386,595.965637 + C317.018951,594.149231 319.113373,592.086426 320.495697,590.063904 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M433.282593,428.719482 + C432.195984,424.269714 431.409393,419.529785 430.540558,414.294067 + C401.383453,414.294067 372.070343,414.294067 342.302612,414.294067 + C341.332245,421.901825 340.349884,429.603516 339.179993,437.652252 + C338.782288,435.125702 338.584747,432.251160 338.359161,429.378784 + C338.009583,424.927124 337.648102,420.476410 337.277679,416.026459 + C337.016174,412.884674 336.185608,409.746277 340.961212,408.868622 + C344.094360,408.292847 347.393585,407.169128 348.114960,403.065430 + C348.251984,402.286072 349.540955,401.220337 350.342529,401.180786 + C351.185394,401.139221 352.059692,402.208466 352.958954,402.724915 + C354.333435,403.514221 355.735992,404.892914 357.135406,404.903931 + C375.751495,405.050476 394.370270,405.114807 412.985535,404.921387 + C417.555908,404.873901 422.111908,403.791779 426.683228,403.260223 + C427.942139,403.113831 429.232513,403.237946 431.651306,403.237946 + C429.847900,406.054871 428.542450,408.093964 426.760315,410.877655 + C429.090546,410.734802 430.634552,410.640167 433.582611,410.459442 + C433.582611,416.466125 433.582611,422.447754 433.282593,428.719482 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M841.000000,520.000000 + C841.000000,639.763184 841.000000,759.026428 841.000000,878.289612 + C840.565613,878.289795 840.131287,878.289978 839.696899,878.290222 + C839.696899,725.430908 839.696899,572.571655 839.696899,419.712341 + C840.131287,419.707642 840.565613,419.702942 841.000000,419.698242 + C841.000000,452.965485 841.000000,486.232758 841.000000,520.000000 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M844.994446,413.095947 + C844.700745,415.110474 844.163147,416.693512 844.162659,418.276672 + C844.117493,573.083496 844.113220,727.890381 844.106262,882.697205 + C844.106201,884.181763 844.106201,885.666382 844.106201,888.085693 + C893.615967,888.085693 942.835754,888.085693 992.520508,888.034424 + C991.205383,888.656311 989.426697,889.912781 987.644897,889.917175 + C943.502014,890.026001 899.358887,890.002502 855.215759,890.002502 + C853.290466,890.002502 851.365112,890.002441 848.767944,890.002441 + C849.574036,892.025574 850.020569,893.146179 850.971436,895.532654 + C848.433594,894.675659 845.950134,894.611633 845.239746,893.456909 + C842.763000,889.430664 838.631104,885.525269 842.580505,880.007202 + C843.319580,878.974792 842.983887,877.104858 842.984070,875.621460 + C842.998535,724.535461 842.986938,573.449402 843.053528,422.363434 + C843.055298,418.239105 842.643555,415.244324 837.299377,416.638794 + C837.726746,412.932373 837.547546,409.408691 838.740295,406.435333 + C839.279358,405.091370 842.547241,404.842102 844.577148,404.096191 + C845.063904,405.506134 845.874756,406.899902 845.946167,408.330566 + C846.017334,409.755951 845.346741,411.218353 844.994446,413.095947 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M337.511169,448.080719 + C336.247803,462.373840 334.682007,476.764099 332.973450,491.137390 + C329.161438,523.205933 325.278198,555.265930 321.224792,587.670593 + C321.117157,582.524231 320.849487,576.999268 321.417114,571.561462 + C321.849030,567.424072 324.446930,563.428040 321.174652,559.319397 + C320.690216,558.711182 320.989655,557.369202 321.126648,556.394836 + C321.655212,552.635376 322.207703,548.877136 322.877716,545.141113 + C323.091522,543.948792 324.043915,542.829285 324.052399,541.672058 + C324.070404,539.203735 323.691986,536.732544 323.394104,533.325867 + C323.162415,533.440613 324.246674,532.903687 325.084351,532.488831 + C324.190796,529.006165 323.324127,525.628357 322.325287,521.735229 + C324.653076,521.735229 326.009277,521.735229 327.618561,521.735229 + C327.670624,520.870178 327.866943,520.210999 327.720490,519.639709 + C326.179016,513.626709 323.665314,507.689423 323.256683,501.607849 + C322.837921,495.374847 324.464569,489.004425 325.316071,481.657959 + C326.255646,485.069641 326.988739,487.731659 327.721863,490.393677 + C328.039886,490.395142 328.357941,490.396637 328.675964,490.398102 + C328.675964,487.480743 329.743774,483.931824 328.465485,481.770477 + C325.281799,476.387268 328.048523,471.349609 328.285400,466.253845 + C328.484253,461.976410 329.992554,457.759857 330.998444,453.163025 + C332.333740,453.602386 333.798370,454.084320 335.254272,454.563354 + C335.907135,452.354584 336.525574,450.262360 337.511169,448.080719 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M481.280945,828.721924 + C479.515594,817.644226 477.973999,806.288147 476.597656,794.912109 + C472.039642,757.238098 467.541168,719.556885 463.077118,681.447998 + C464.089874,685.123047 465.294678,689.191833 465.908081,693.347839 + C466.280884,695.873596 466.333557,698.044250 469.635864,698.000366 + C473.559906,697.948181 474.822296,700.641541 474.915344,703.824402 + C475.011292,707.105774 474.171204,710.421753 474.340668,713.691528 + C474.482574,716.429871 475.714844,719.096191 476.045929,721.844788 + C476.343872,724.317688 477.127716,727.971619 475.863434,729.169800 + C471.366638,733.431885 473.382904,738.903992 472.308868,743.695374 + C471.712402,746.356262 473.064148,749.453796 473.537842,752.354553 + C474.025635,752.376160 474.513397,752.397827 475.001190,752.419434 + C475.001190,748.849609 474.777954,745.259338 475.103088,741.719360 + C475.247955,740.142517 476.497009,738.667114 477.244934,737.145630 + C477.829468,738.663330 478.936249,740.196533 478.891510,741.695496 + C478.793701,744.975525 478.550781,748.377625 477.566559,751.475586 + C476.034302,756.298523 476.574554,760.701782 478.397156,765.254578 + C478.856537,766.401978 478.640625,768.287964 477.966156,769.342834 + C474.423218,774.884033 475.859924,780.115356 478.428772,785.419922 + C478.968964,786.535461 479.594574,787.898987 479.416290,789.027466 + C477.937073,798.390137 479.815155,807.467896 481.265381,816.628967 + C481.875641,820.484070 481.505066,824.494568 481.280945,828.721924 +z"/> +<path fill="#356DE2" opacity="1.000000" stroke="none" + d=" +M985.490967,678.427551 + C983.634705,679.215027 981.749451,679.867188 979.861633,679.874634 + C960.554077,679.951233 941.246094,679.923035 921.558472,679.516602 + C922.882568,678.764709 924.585266,678.128235 926.290588,678.120911 + C943.944031,678.045349 961.598816,677.979248 979.251038,678.144104 + C982.922485,678.178406 984.164795,677.175476 984.119751,673.379639 + C983.932129,657.560120 984.073608,641.737000 984.007812,625.915527 + C984.000366,624.123718 983.729187,622.215027 983.044373,620.585876 + C982.594360,619.515259 981.186890,618.118591 980.198547,618.110962 + C962.380066,617.973206 944.554138,617.759216 926.743469,618.165039 + C919.437500,618.331543 919.063110,618.650330 919.062561,611.429504 + C919.059509,569.793274 919.093140,528.156982 918.989136,486.520996 + C918.981567,483.495636 917.982483,480.472748 917.418335,477.302002 + C918.116211,477.168579 919.404907,476.922241 920.693604,476.675873 + C920.693604,476.675873 920.858582,476.850555 920.890503,477.402222 + C920.922485,524.101318 920.922485,570.248718 920.922485,616.607178 + C924.092834,616.607178 926.550354,616.607178 929.007874,616.607178 + C944.695312,616.660522 960.382690,616.713867 976.959595,616.768433 + C980.369446,616.923218 982.889832,617.076660 985.410217,617.230042 + C985.433228,620.198975 985.456238,623.167908 985.476074,627.036011 + C985.544067,642.282898 985.615234,656.630737 985.686401,670.978516 + C985.611145,673.316711 985.535950,675.654846 985.490967,678.427551 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M448.302063,554.734253 + C446.245026,541.336792 444.375000,527.690002 442.714233,514.017883 + C439.428833,486.970337 436.259186,459.908722 433.099792,432.421692 + C434.375031,434.847046 435.592163,437.703613 437.525116,442.240265 + C439.376862,438.799377 440.743896,436.259186 442.065704,433.803009 + C444.357605,435.600372 445.709747,444.083374 444.470551,447.903870 + C442.478851,454.044373 440.822601,460.295288 439.111847,466.523560 + C438.857025,467.451233 438.671875,468.798157 439.143311,469.462097 + C443.021667,474.924072 440.772827,480.965332 441.087585,486.768616 + C441.169342,488.275604 442.257111,489.728088 442.882812,491.205597 + C443.471466,491.030090 444.060150,490.854553 444.648834,490.679016 + C444.873718,487.671509 445.098633,484.663971 445.323547,481.656464 + C445.789185,481.685181 446.254822,481.713867 446.720459,481.742584 + C446.794586,492.555328 448.112823,503.415680 443.870270,514.373779 + C445.793488,514.373779 447.140167,514.373779 449.036530,514.373779 + C448.422638,516.949829 447.530670,519.138000 447.469543,521.349243 + C447.395813,524.015198 448.211273,526.694397 448.301025,529.379333 + C448.387726,531.972900 447.866699,534.584473 447.920288,537.181824 + C448.039215,542.947815 448.350708,548.709839 448.302063,554.734253 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M988.515991,405.002197 + C989.746948,405.128143 990.538635,405.252594 991.330322,405.377045 + C991.321838,405.683441 991.313354,405.989868 991.304810,406.296265 + C944.441650,406.296265 897.578491,406.296265 850.715332,406.296265 + C850.709778,405.864410 850.704224,405.432587 850.698669,405.000732 + C896.491333,405.000732 942.283997,405.000732 988.515991,405.002197 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M991.927490,412.877441 + C989.668274,413.384216 987.349060,413.535004 985.029907,413.534698 + C940.327576,413.528931 895.625305,413.505371 850.465210,413.375305 + C851.716614,412.510071 853.423462,411.102600 855.135437,411.096283 + C889.942261,410.967896 924.749695,411.000000 959.557007,411.000000 + C968.716858,411.000000 977.877686,410.930359 987.035522,411.065369 + C988.652649,411.089172 990.257141,411.967438 991.927490,412.877441 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M761.006836,516.374146 + C761.950745,511.182373 762.892883,506.451111 763.695435,502.420807 + C763.695435,542.239380 763.695435,582.763428 763.695435,623.287415 + C762.798645,623.292358 761.901855,623.297302 761.005066,623.302246 + C761.005066,587.813049 761.005066,552.323792 761.006836,516.374146 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M456.273132,620.724548 + C454.719604,611.825928 453.401672,602.646362 452.238098,593.447327 + C450.821655,582.248901 449.535095,571.034058 448.261108,559.406860 + C449.558136,560.915710 450.860107,562.801880 451.991974,564.785156 + C452.955597,566.473694 453.737640,568.267151 454.574921,570.026184 + C456.385101,573.829102 458.179230,577.639648 460.375122,582.282654 + C457.041992,582.282654 455.353485,582.282654 454.223267,582.282654 + C455.017090,595.149414 455.797089,607.792175 456.273132,620.724548 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M928.902039,616.276367 + C926.550354,616.607178 924.092834,616.607178 920.922485,616.607178 + C920.922485,570.248718 920.922485,524.101318 920.973511,477.489197 + C923.392395,476.994629 925.760315,476.964813 928.564392,477.022156 + C927.083740,478.605438 925.166992,480.101593 923.250183,481.597748 + C922.834290,481.277863 922.418335,480.958008 922.002441,480.638123 + C922.002441,482.437744 922.002502,484.237335 922.002502,486.036957 + C922.002502,525.008667 921.968018,563.980469 922.095825,602.951721 + C922.102783,605.085693 923.690613,607.172302 924.102417,609.363586 + C924.335083,610.602234 923.669006,612.009766 923.302612,613.877258 + C925.045715,614.533508 926.920959,615.239502 928.902039,616.276367 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M463.272186,678.736023 + C460.995789,663.583862 459.009583,648.159180 457.033386,632.733276 + C456.696716,630.105286 456.402985,627.471680 456.140381,624.415222 + C456.997681,625.226196 458.165894,626.367859 458.520844,627.722595 + C458.989410,629.510986 458.820221,631.466492 458.981476,634.359741 + C460.562195,632.895996 461.346771,632.169495 463.380646,630.286072 + C462.337402,638.204407 461.484711,644.676453 460.632019,651.148560 + C461.228546,651.501953 461.825043,651.855408 462.421570,652.208801 + C462.795135,651.181702 463.168671,650.154602 463.565765,649.062744 + C463.565765,659.168030 463.565765,668.815552 463.272186,678.736023 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M986.107300,670.948730 + C985.615234,656.630737 985.544067,642.282898 985.578796,627.478271 + C986.788635,628.327515 988.824097,629.614624 988.851135,630.942810 + C989.097961,643.027222 989.066467,655.119934 988.885803,667.206726 + C988.867126,668.455627 987.350037,669.682129 986.107300,670.948730 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M953.000000,687.000000 + C945.231445,687.000000 937.962891,687.000000 930.694336,687.000000 + C930.694824,686.562317 930.695374,686.124695 930.695923,685.687012 + C945.557373,685.687012 960.418823,685.687012 975.280273,685.687012 + C975.301575,686.124695 975.322815,686.562317 975.344116,687.000000 + C968.062744,687.000000 960.781372,687.000000 953.000000,687.000000 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M683.373352,720.827881 + C681.964661,718.466248 680.943848,716.205933 679.304932,712.577148 + C684.658630,713.417114 689.524231,714.180542 694.389771,714.943909 + C694.509705,715.439453 694.629639,715.934998 694.749512,716.430542 + C691.086731,717.930115 687.423950,719.429688 683.373352,720.827881 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M993.463867,475.630890 + C993.185913,456.292969 993.159912,436.632965 993.203857,416.512878 + C993.509705,416.658722 993.953186,417.265167 993.951538,417.870453 + C993.899658,437.016663 993.804443,456.162750 993.463867,475.630890 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M993.506226,884.617554 + C993.190857,865.284607 993.113281,845.620117 992.961304,825.494507 + C993.256714,825.896179 993.949768,826.759338 993.948792,827.621704 + C993.927002,846.509888 993.828125,865.397949 993.506226,884.617554 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M310.535645,596.100220 + C310.818207,594.946289 311.246277,594.161560 311.674316,593.376831 + C312.388916,593.929932 313.660065,594.422363 313.728149,595.046021 + C314.182739,599.210510 314.380676,603.403015 314.733673,608.653076 + C308.536316,605.315369 312.368500,600.022827 310.535645,596.100220 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M495.000214,849.000000 + C494.307526,843.902344 493.614807,838.804688 492.871185,833.332153 + C497.647552,835.415833 498.872192,844.081604 495.355103,848.939392 + C495.000000,849.000000 495.000214,849.000000 495.000214,849.000000 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M460.002441,615.908447 + C460.392456,613.085144 458.730927,609.577515 463.992615,609.194580 + C463.292725,614.322876 462.646149,619.060242 461.999573,623.797607 + C461.333862,623.734802 460.668152,623.671997 460.002441,623.609192 + C460.002441,621.187195 460.002441,618.765198 460.002441,615.908447 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M448.805481,461.069946 + C447.734406,463.894867 446.591278,466.344788 445.217621,469.288757 + C442.062439,466.123932 442.498505,463.111023 444.110992,460.434906 + C444.483093,459.817352 447.132172,460.571808 448.805481,461.069946 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M745.600342,684.580017 + C746.670532,682.860352 747.554626,681.463135 748.812073,679.475952 + C750.632385,686.561218 748.711731,689.271606 742.531433,688.196777 + C743.712952,686.846558 744.563599,685.874512 745.600342,684.580017 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M289.002441,745.735229 + C289.002441,741.411560 289.002441,737.550293 289.002441,733.689087 + C289.478760,733.688965 289.955078,733.688904 290.431396,733.688782 + C290.431396,739.217285 290.431396,744.745789 290.431396,750.274292 + C290.110565,750.335327 289.789734,750.396301 289.468933,750.457275 + C289.313416,749.037354 289.157928,747.617493 289.002441,745.735229 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M669.401001,714.982056 + C665.541260,714.993042 662.103333,714.993042 658.665405,714.993042 + C658.669922,714.369751 658.674500,713.746460 658.679016,713.123169 + C662.594299,713.123169 666.509583,713.123169 670.424866,713.123169 + C670.224182,713.739136 670.023560,714.355042 669.401001,714.982056 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M730.492859,700.340881 + C732.178284,698.539551 733.726624,697.064209 735.274963,695.588867 + C735.779602,695.887573 736.284241,696.186340 736.788879,696.485046 + C736.460083,697.804260 736.474121,699.966675 735.731628,700.268616 + C734.147583,700.912659 732.171326,700.592163 730.492859,700.340881 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M273.530884,862.652954 + C278.303284,863.810974 276.676636,867.291199 276.718872,870.215698 + C276.223358,870.372375 275.727844,870.529053 275.232300,870.685669 + C274.579773,868.100403 273.927216,865.515076 273.530884,862.652954 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M985.815674,617.169739 + C982.889832,617.076660 980.369446,616.923218 977.420715,616.687439 + C980.685852,615.887085 984.379333,615.169067 988.072815,614.451111 + C988.218994,615.020203 988.365112,615.589355 988.511292,616.158508 + C987.747864,616.475464 986.984436,616.792419 985.815674,617.169739 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M310.533478,612.856689 + C309.852081,614.162231 309.302551,615.126526 308.753021,616.090820 + C308.301422,615.976807 307.849854,615.862793 307.398254,615.748840 + C307.295471,613.706360 307.192657,611.663940 307.089874,609.621460 + C307.503754,609.443542 307.917633,609.265564 308.331512,609.087585 + C309.109436,610.230164 309.887390,611.372803 310.533478,612.856689 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M305.002441,647.931519 + C305.102692,645.865723 305.202911,644.231384 305.303131,642.597107 + C305.706451,642.624817 306.109741,642.652588 306.513062,642.680298 + C306.513062,645.878845 306.513062,649.077332 306.513062,652.275879 + C306.178741,652.341736 305.844421,652.407593 305.510101,652.473389 + C305.340881,651.103271 305.171661,649.733154 305.002441,647.931519 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M761.193848,466.887573 + C764.494568,464.385956 764.842651,464.603333 764.708191,470.277100 + C763.549561,469.269684 762.390991,468.262238 761.193848,466.887573 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M323.005554,464.483459 + C324.015198,466.344177 324.654602,468.270477 325.294037,470.196747 + C324.479370,470.327667 323.664703,470.458588 322.850067,470.589478 + C322.778473,468.576019 322.706909,466.562531 323.005554,464.483459 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M747.912354,437.701233 + C746.815857,436.517426 746.079956,435.213379 745.344055,433.909363 + C746.060120,433.656525 746.776184,433.403625 747.492249,433.150757 + C747.752502,434.627502 748.012756,436.104248 747.912354,437.701233 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M765.007202,628.131775 + C765.150818,627.025452 765.293030,626.290039 765.435242,625.554626 + C765.893738,625.567383 766.352295,625.580200 766.810791,625.592957 + C766.810791,627.536133 766.810791,629.479370 766.810791,631.422546 + C766.393982,631.457275 765.977234,631.491943 765.560425,631.526672 + C765.375549,630.518677 765.190674,629.510681 765.007202,628.131775 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M293.367371,726.117798 + C293.163605,724.401367 293.163605,722.980652 293.163605,721.172791 + C296.865448,723.915344 296.854980,724.331238 293.367371,726.117798 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M367.002991,876.999207 + C367.181488,875.599365 367.359985,874.594055 367.538513,873.588806 + C367.848663,873.603699 368.158783,873.618591 368.468933,873.633545 + C368.468933,875.838196 368.468933,878.042908 368.468933,880.247620 + C368.195557,880.317261 367.922180,880.386902 367.648804,880.456604 + C367.433533,879.435669 367.218262,878.414734 367.002991,876.999207 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M460.386230,593.844360 + C464.682770,594.765503 463.174652,596.616577 460.177338,598.555969 + C460.177338,597.082520 460.177338,595.609009 460.386230,593.844360 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M448.588013,439.107849 + C447.837006,437.577393 447.317413,435.753906 446.797821,433.930450 + C447.471649,433.808868 448.145508,433.687317 448.819366,433.565735 + C448.819366,435.315430 448.819366,437.065155 448.588013,439.107849 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M477.100922,707.576355 + C477.844055,708.745850 478.522461,710.284363 479.200867,711.822876 + C478.559784,712.025879 477.918671,712.228882 477.277557,712.431824 + C477.197113,710.936340 477.116669,709.440857 477.100922,707.576355 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M480.996643,780.290405 + C480.855927,781.573425 480.715759,782.496582 480.575623,783.419678 + C480.131226,783.369507 479.686829,783.319336 479.242432,783.269104 + C479.422699,781.484131 479.602997,779.699158 479.783264,777.914124 + C480.134277,777.939880 480.485291,777.965698 480.836273,777.991455 + C480.889923,778.637817 480.943573,779.284180 480.996643,780.290405 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M494.999908,849.000000 + C494.999817,850.805237 494.999817,852.610413 494.999817,854.415649 + C494.316711,854.327393 493.633606,854.239136 492.950500,854.150818 + C493.388519,852.483887 493.826538,850.817017 494.632385,849.075073 + C495.000214,849.000000 495.000000,849.000000 494.999908,849.000000 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M338.515076,440.074921 + C338.724701,441.783386 338.578888,443.576111 338.240173,445.694397 + C338.084747,444.066345 338.122223,442.112762 338.515076,440.074921 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M494.999023,880.590515 + C494.194672,879.379272 493.389343,877.758545 492.584015,876.137817 + C493.249725,875.927673 493.915436,875.717529 494.581177,875.507385 + C494.720123,877.065247 494.859100,878.623169 494.999023,880.590515 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M833.188171,885.786011 + C833.530029,884.771179 833.971497,884.098145 834.412842,883.425049 + C834.879395,884.176086 835.345886,884.927185 835.812378,885.678284 + C834.970825,885.828125 834.129272,885.977905 833.188171,885.786011 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M735.483887,421.376526 + C735.820190,422.287170 735.842773,423.041504 735.865295,423.795837 + C735.020325,423.555481 734.175354,423.315125 733.330383,423.074768 + C733.943604,422.456573 734.556824,421.838379 735.483887,421.376526 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M690.145630,401.040680 + C691.172302,401.215851 691.844543,401.406250 692.516846,401.596619 + C692.469971,401.986115 692.423157,402.375610 692.376282,402.765106 + C691.112000,402.568176 689.847656,402.371246 688.583374,402.174286 + C688.986023,401.801483 689.388672,401.428680 690.145630,401.040680 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M290.438477,840.070557 + C290.645874,841.450684 290.513702,842.912659 290.206726,844.694275 + C290.054230,843.393372 290.076569,841.772949 290.438477,840.070557 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M992.586182,673.845337 + C992.739136,675.230652 992.668884,676.346680 992.598572,677.462646 + C992.062927,677.355469 991.527222,677.248291 990.991577,677.141113 + C991.448669,675.952698 991.905823,674.764282 992.586182,673.845337 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M767.039429,484.854614 + C767.214844,483.827454 767.405518,483.154816 767.596130,482.482208 + C767.985840,482.529114 768.375549,482.576019 768.765259,482.622925 + C768.568176,483.887817 768.371094,485.152740 768.173950,486.417633 + C767.800842,486.014801 767.427734,485.611969 767.039429,484.854614 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M767.438232,472.813995 + C768.163879,473.720306 768.660278,474.890594 769.156677,476.060913 + C768.622192,476.206757 768.087646,476.352600 767.553101,476.498444 + C767.438416,475.358276 767.323730,474.218140 767.438232,472.813995 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M495.001770,881.364746 + C495.703186,881.949158 496.406342,882.898254 497.109528,883.847412 + C496.643372,884.082092 496.177246,884.316772 495.711090,884.551453 + C495.475220,883.610779 495.239380,882.670166 495.001770,881.364746 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M753.519775,687.401978 + C752.968140,687.962158 752.282593,688.226990 751.597168,688.491821 + C751.532166,687.921570 751.467163,687.351379 751.402100,686.781128 + C752.063354,686.889587 752.724670,686.998047 753.519775,687.401978 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M978.319824,688.852051 + C978.293640,688.212341 978.486084,687.509949 978.946350,687.150574 + C979.130432,687.006836 979.904785,687.619263 980.410461,687.887451 + C979.822266,688.233948 979.234070,688.580505 978.319824,688.852051 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M725.494324,703.380127 + C726.288513,702.983459 726.947876,702.881714 727.607239,702.779968 + C727.537964,703.349792 727.468750,703.919617 727.399475,704.489441 + C726.719482,704.217957 726.039490,703.946472 725.494324,703.380127 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M988.350342,613.068726 + C988.730164,612.291382 989.292419,611.749207 989.854675,611.207031 + C990.082703,611.560547 990.310669,611.914185 990.538635,612.267822 + C989.870056,612.613159 989.201416,612.958496 988.350342,613.068726 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M448.793762,445.202026 + C448.430328,445.803833 448.004303,446.106445 447.578278,446.409058 + C447.463562,445.961914 447.348846,445.514801 447.234100,445.067688 + C447.733124,445.012726 448.232147,444.957764 448.793762,445.202026 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M401.203857,819.191650 + C401.764618,818.946655 402.264160,819.000366 402.763733,819.054016 + C402.650085,819.502686 402.536438,819.951416 402.422760,820.400085 + C401.996063,820.096863 401.569366,819.793640 401.203857,819.191650 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M765.204956,495.191895 + C765.764771,494.947174 766.263489,495.000763 766.762268,495.054321 + C766.648865,495.502441 766.535461,495.950592 766.422058,496.398743 + C765.995972,496.095886 765.569946,495.793030 765.204956,495.191895 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M463.203979,575.192017 + C463.764404,574.947021 464.263702,575.000671 464.763000,575.054260 + C464.649475,575.502747 464.535950,575.951294 464.422424,576.399780 + C463.995880,576.096741 463.569366,575.793640 463.203979,575.192017 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M495.492920,890.666016 + C496.036316,891.010498 496.362213,891.570190 496.688110,892.129883 + C496.409424,892.257385 496.130768,892.384827 495.852081,892.512268 + C495.659882,891.968567 495.467651,891.424805 495.492920,890.666016 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M991.532959,624.392578 + C991.649353,623.941467 991.933594,623.664978 992.235718,623.409729 + C992.243530,623.403198 992.449219,623.631042 992.562622,623.749512 + C992.276123,624.023010 991.989624,624.296509 991.532959,624.392578 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M495.608978,862.534485 + C496.058380,862.650391 496.333893,862.933350 496.588104,863.234314 + C496.594574,863.242004 496.367645,863.446960 496.249542,863.559937 + C495.977142,863.274597 495.704712,862.989258 495.608978,862.534485 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M992.466614,619.608032 + C992.350342,620.058594 992.066589,620.334839 991.764832,620.589722 + C991.757080,620.596252 991.551636,620.368652 991.438416,620.250244 + C991.724548,619.977173 992.010681,619.704102 992.466614,619.608032 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M977.607910,609.533447 + C978.058533,609.649719 978.334778,609.933472 978.589661,610.235229 + C978.596191,610.242981 978.368591,610.448425 978.250183,610.561707 + C977.977051,610.275574 977.703918,609.989441 977.607910,609.533447 +z"/> +<path fill="#F9FCFD" opacity="1.000000" stroke="none" + d=" +M927.607544,609.533081 + C928.058533,609.649414 928.334961,609.933411 928.590088,610.235474 + C928.596558,610.243103 928.368835,610.448730 928.250366,610.562134 + C927.976990,610.275818 927.703613,609.989441 927.607544,609.533081 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M463.394287,579.954590 + C463.790527,579.712830 464.184723,579.714478 464.576691,579.744629 + C464.587708,579.745483 464.575256,580.051086 464.573730,580.214478 + C464.179779,580.210327 463.785858,580.206177 463.394287,579.954590 +z"/> +<path fill="#F2F8FC" opacity="1.000000" stroke="none" + d=" +M571.464233,401.610443 + C571.348572,402.058472 571.066345,402.333038 570.766235,402.586456 + C570.758606,402.592896 570.554321,402.366577 570.441711,402.248810 + C570.726257,401.977295 571.010864,401.705750 571.464233,401.610443 +z"/> +<path fill="#F4FAFC" opacity="1.000000" stroke="none" + d=" +M396.359741,687.481689 + C397.367371,699.026428 398.299713,710.118774 399.267029,721.626770 + C389.410980,721.626770 380.025177,721.626770 370.039307,721.626770 + C374.510010,661.973633 378.972534,602.429565 383.435059,542.885437 + C383.919189,542.870728 384.403320,542.856018 384.887451,542.841309 + C388.686462,590.903931 392.485443,638.966553 396.359741,687.481689 +z"/> +<path fill="#0355FA" opacity="1.000000" stroke="none" + d=" +M644.798462,801.000488 + C644.798706,830.126160 644.798706,858.751892 644.798706,887.690063 + C619.298035,887.690063 594.380310,887.690063 569.073975,887.690063 + C569.073975,729.870789 569.073975,572.189697 569.073975,413.827850 + C570.977966,413.827850 572.750854,413.827728 574.523804,413.827911 + C606.350830,413.831482 638.178406,413.729858 670.004761,413.874237 + C692.636047,413.976898 713.669006,419.225006 730.805725,435.028259 + C747.864380,450.759491 754.663086,471.198975 755.917847,493.661194 + C756.669678,507.120300 756.377014,520.644287 756.353943,534.138733 + C756.299316,566.128845 756.801208,598.140259 755.781067,630.101746 + C754.996765,654.672913 746.173645,676.291687 726.061768,691.955200 + C711.156799,703.563416 693.738342,708.101074 675.246826,708.908752 + C665.306030,709.342957 655.330627,708.985962 644.798157,708.985962 + C644.798157,739.913574 644.798157,770.206970 644.798462,801.000488 +M644.072388,571.500000 + C644.065491,596.815491 644.128235,622.131409 643.988831,647.446167 + C643.963135,652.115723 645.542603,654.272705 650.372131,653.985596 + C654.189392,653.758667 658.030640,653.943604 661.861267,653.932312 + C670.776733,653.905945 678.553589,647.847473 679.799561,639.264221 + C680.891235,631.744507 681.788086,624.123657 681.848633,616.540161 + C682.081116,587.395813 682.044922,558.248108 681.897034,529.102661 + C681.829651,515.818054 681.501770,502.524719 680.860291,489.256256 + C680.310303,477.879852 673.343506,467.669342 659.080017,469.008087 + C654.949890,469.395721 650.752563,469.067566 646.586060,469.067566 + C645.149597,470.409851 644.123840,473.236694 644.114380,476.066895 + C644.009766,507.544281 644.062378,539.022217 644.072388,571.500000 +z"/> +<path fill="#FFFFFF" opacity="1.000000" stroke="none" + d=" +M758.866882,638.693604 + C758.094299,637.356079 757.689941,635.921692 757.285522,634.487244 + C758.049744,634.409241 758.814026,634.331238 759.578247,634.253174 + C759.463867,635.701050 759.349487,637.148926 758.866882,638.693604 +z"/> +<path fill="#0255FB" opacity="1.000000" stroke="none" + d=" +M920.857300,476.360382 + C919.404907,476.922241 918.116211,477.168579 917.418335,477.302002 + C917.982483,480.472748 918.981567,483.495636 918.989136,486.520996 + C919.093140,528.156982 919.059509,569.793274 919.062561,611.429504 + C919.063110,618.650330 919.437500,618.331543 926.743469,618.165039 + C944.554138,617.759216 962.380066,617.973206 980.198547,618.110962 + C981.186890,618.118591 982.594360,619.515259 983.044373,620.585876 + C983.729187,622.215027 984.000366,624.123718 984.007812,625.915527 + C984.073608,641.737000 983.932129,657.560120 984.119751,673.379639 + C984.164795,677.175476 982.922485,678.178406 979.251038,678.144104 + C961.598816,677.979248 943.944031,678.045349 926.290588,678.120911 + C924.585266,678.128235 922.882568,678.764709 921.090454,679.494751 + C921.002258,679.880371 920.993713,679.984253 920.580627,680.048218 + C919.792480,681.733398 919.417358,683.354675 919.026245,685.446411 + C919.006042,730.528503 918.991638,775.140198 919.035767,819.751770 + C919.037476,821.495911 919.600159,823.239441 920.177246,825.004395 + C920.636780,825.009460 920.820496,824.993469 921.004211,824.977417 + C921.004211,824.977417 921.006470,824.995850 921.050537,825.340942 + C921.876221,826.394775 922.657776,827.103516 923.439331,827.812256 + C923.978516,826.955505 924.517639,826.098694 925.056763,825.241821 + C925.796204,825.201233 926.535645,825.160583 927.642090,825.177917 + C929.774902,825.493835 931.539917,825.970581 933.306458,825.975952 + C951.303406,826.030334 969.301575,826.103210 987.297058,825.929138 + C991.014038,825.893250 992.162170,826.959839 992.121033,830.719666 + C991.936951,847.548340 991.902100,864.382080 992.140381,881.209412 + C992.199585,885.392578 990.654480,886.140564 986.924500,886.131897 + C942.098511,886.028137 897.272034,886.005920 852.446350,886.161438 + C848.331482,886.175659 846.663391,884.432190 846.118286,880.912476 + C845.840271,879.117432 845.937317,877.257629 845.937195,875.427124 + C845.930603,724.784363 845.930969,574.141541 845.931641,423.498779 + C845.931702,415.916656 845.935242,415.914795 853.708618,415.914642 + C897.868286,415.913849 942.028137,415.969910 986.187317,415.827118 + C990.580383,415.812927 992.255493,416.714600 992.160767,421.542999 + C991.843811,437.700897 991.935852,453.869995 992.116882,470.032074 + C992.160034,473.883240 991.155457,475.213440 987.093750,475.170319 + C967.765686,474.965027 948.432373,474.952850 929.106079,475.230255 + C926.492004,475.267792 923.266052,471.957672 920.857300,476.360382 +z"/> +<path fill="#356DE2" opacity="1.000000" stroke="none" + d=" +M920.950256,824.513184 + C920.820496,824.993469 920.636780,825.009460 920.180664,824.538208 + C919.910767,779.609192 919.924011,735.167419 919.878601,690.725708 + C919.876648,688.808838 919.333374,686.892456 919.042236,684.975891 + C919.417358,683.354675 919.792480,681.733398 920.528564,680.050903 + C920.891846,728.009399 920.894104,776.029175 920.950256,824.513184 +z"/> +<path fill="#356DE2" opacity="1.000000" stroke="none" + d=" +M924.664185,825.136597 + C924.517639,826.098694 923.978516,826.955505 923.439331,827.812256 + C922.657776,827.103516 921.876221,826.394775 921.059753,825.338623 + C922.107117,825.004578 923.189331,825.017944 924.664185,825.136597 +z"/> +<path fill="#FFFFFF" opacity="1.000000" stroke="none" + d=" +M405.159729,859.576904 + C405.060333,857.602051 405.060333,856.024719 405.060333,854.447388 + C405.711639,854.264343 406.362915,854.081360 407.014221,853.898315 + C407.649475,855.667175 408.284729,857.436035 408.920013,859.204895 + C407.699707,859.461426 406.479431,859.717896 405.159729,859.576904 +z"/> +<path fill="#356DE2" opacity="1.000000" stroke="none" + d=" +M644.072449,571.000000 + C644.062378,539.022217 644.009766,507.544281 644.114380,476.066895 + C644.123840,473.236694 645.149597,470.409851 646.586060,469.067566 + C650.752563,469.067566 654.949890,469.395721 659.080017,469.008087 + C673.343506,467.669342 680.310303,477.879852 680.860291,489.256256 + C681.501770,502.524719 681.829651,515.818054 681.897034,529.102661 + C682.044922,558.248108 682.081116,587.395813 681.848633,616.540161 + C681.788086,624.123657 680.891235,631.744507 679.799561,639.264221 + C678.553589,647.847473 670.776733,653.905945 661.861267,653.932312 + C658.030640,653.943604 654.189392,653.758667 650.372131,653.985596 + C645.542603,654.272705 643.963135,652.115723 643.988831,647.446167 + C644.128235,622.131409 644.065491,596.815491 644.072449,571.000000 +M645.793091,476.598083 + C645.793091,534.845032 645.793091,593.092041 645.793091,651.762878 + C652.519104,651.762878 658.880798,652.378906 665.032898,651.510376 + C668.244995,651.056885 671.713318,648.545471 673.974548,645.995789 + C679.521606,639.740845 679.830750,631.575806 679.866211,623.799683 + C680.055542,582.334412 680.068359,540.867126 679.818970,499.402405 + C679.783569,493.515961 678.490845,487.493805 676.854736,481.800385 + C675.210205,476.077759 671.099609,471.786652 664.870605,471.224487 + C658.671204,470.665039 652.381897,471.101959 645.992737,471.101959 + C645.906433,472.998505 645.846680,474.310333 645.793091,476.598083 +z"/> +<path fill="#0455F9" opacity="1.000000" stroke="none" + d=" +M919.026306,685.446411 + C919.333374,686.892456 919.876648,688.808838 919.878601,690.725708 + C919.924011,735.167419 919.910767,779.609192 919.904907,824.517090 + C919.600159,823.239441 919.037476,821.495911 919.035767,819.751770 + C918.991638,775.140198 919.006042,730.528503 919.026306,685.446411 +z"/> +<path fill="#FBFEFE" opacity="1.000000" stroke="none" + d=" +M645.790039,476.110107 + C645.846680,474.310333 645.906433,472.998505 645.992737,471.101959 + C652.381897,471.101959 658.671204,470.665039 664.870605,471.224487 + C671.099609,471.786652 675.210205,476.077759 676.854736,481.800385 + C678.490845,487.493805 679.783569,493.515961 679.818970,499.402405 + C680.068359,540.867126 680.055542,582.334412 679.866211,623.799683 + C679.830750,631.575806 679.521606,639.740845 673.974548,645.995789 + C671.713318,648.545471 668.244995,651.056885 665.032898,651.510376 + C658.880798,652.378906 652.519104,651.762878 645.793091,651.762878 + C645.793091,593.092041 645.793091,534.845032 645.790039,476.110107 +z"/> +</svg> \ 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 186/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 <metamaskbot@users.noreply.github.com> --- 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 <david@timechaser.org> Date: Thu, 17 Oct 2024 23:04:01 +0200 Subject: [PATCH 187/226] 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 = ({ </Text> </Box> </Box> - {showInfo && ( + {showInfo && !hidden && ( <Box marginLeft="auto"> <AvatarIcon className="snaps-authorship-header__button" diff --git a/ui/pages/snaps/snap-view/snap-view.js b/ui/pages/snaps/snap-view/snap-view.js index 3cc5cd999047..b8849179cabd 100644 --- a/ui/pages/snaps/snap-view/snap-view.js +++ b/ui/pages/snaps/snap-view/snap-view.js @@ -105,12 +105,14 @@ function SnapView() { showInfo={false} startAccessory={renderBackButton()} endAccessory={ - <SnapHomeMenu - snapId={snapId} - onSettingsClick={handleSettingsClick} - onRemoveClick={handleSnapRemove} - isSettingsAvailable={!snap.preinstalled} - /> + !snap.hidden && ( + <SnapHomeMenu + snapId={snapId} + onSettingsClick={handleSettingsClick} + onRemoveClick={handleSnapRemove} + isSettingsAvailable={!snap.preinstalled} + /> + ) } /> )} 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 <matteo.scurati@gmail.com> Date: Fri, 18 Oct 2024 06:52:19 +0200 Subject: [PATCH 188/226] 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<PreinstalledSnap[]>([ - 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 189/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [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 <chloe.gao@consensys.net> --- 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<void> { @@ -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?= <joao.tavares@consensys.net> Date: Fri, 18 Oct 2024 11:25:06 +0100 Subject: [PATCH 190/226] 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`] = ` </div> </div> `; - -exports[`NFT Details should match minimal props and state snapshot 2`] = ` -<div> - <div - class="mm-box multichain-page mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--width-full mm-box--height-full mm-box--background-color-background-alternative" - > - <div - class="mm-box multichain-page__inner-container mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--height-full mm-box--background-color-background-default" - > - <div - class="mm-box multichain-page-content nft-details__content mm-box--padding-4 mm-box--display-flex mm-box--flex-direction-column mm-box--width-full mm-box--height-full" - > - <div - class="mm-box mm-box--display-flex mm-box--justify-content-space-between" - > - <button - aria-label="Back" - class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-alternative mm-box--background-color-transparent mm-box--rounded-lg" - data-testid="nft__back" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/arrow-left.svg');" - /> - </button> - <div> - <button - aria-label="NFT Options" - class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-transparent mm-box--rounded-lg" - data-testid="nft-options__button" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/more-vertical.svg');" - /> - </button> - </div> - </div> - <div - class="mm-box mm-box--margin-top-1 mm-box--margin-bottom-8 mm-box--display-flex mm-box--justify-content-center" - > - <div - class="mm-box nft-details__nft-item" - > - <button - class="mm-box nft-item__container" - data-testid="nft-item" - > - <div - class="mm-box mm-badge-wrapper nft-item__badge-wrapper nft-item__badge-wrapper__clickable mm-box--display-block" - > - <img - alt="MUNK #1 1" - class="mm-box nft-item__item nft-item__item-image mm-box--display-block mm-box--justify-content-center" - data-testid="nft-image" - src="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link" - /> - <div - class="mm-box mm-badge-wrapper__badge-container" - style="top: -4px; right: -4px;" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-avatar-network nft-item__network-badge mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default mm-box--border-width-2 box--border-style-solid" - data-testid="nft-network-badge" - > - G - </div> - </div> - </div> - </button> - </div> - </div> - <div - class="mm-box" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <h3 - class="mm-box mm-text mm-text--heading-md mm-text--font-weight-bold mm-text--font-style-normal mm-box--color-text-default" - data-testid="nft-details__name" - style="font-size: 24px;" - > - MUNK #1 - </h3> - </div> - <div - class="mm-box nft-details__show-more mm-box--margin-top-2" - style="position: relative; overflow: hidden;" - > - <p - class="mm-box mm-text mm-text--body-sm mm-text--font-weight-medium mm-box--color-text-alternative" - data-testid="nft-details__description" - /> - </div> - <div - class="mm-box mm-box--margin-top-4 mm-box--margin-bottom-4 mm-box--display-flex mm-box--gap-4 mm-box--flex-wrap-wrap" - /> - <div - class="mm-box mm-box--margin-top-2 mm-box--display-flex mm-box--justify-content-space-between" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative" - > - Contract address - </p> - <div - class="mm-box mm-box--display-flex" - > - <button - class="nft-details__addressButton" - > - <p - class="mm-box mm-text mm-text--body-sm-medium mm-text--font-style-normal mm-box--color-primary-default" - > - 0xDc738...06414 - </p> - </button> - <button - aria-label="copy" - class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-flex-end mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent mm-box--rounded-lg" - data-testid="nft-address-copy" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/copy.svg');" - /> - </button> - </div> - </div> - <div - class="mm-box mm-box--margin-top-2 mm-box--display-flex mm-box--justify-content-space-between" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative" - > - Token ID - </p> - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative" - > - 1 - </p> - </div> - <div - class="mm-box mm-box--margin-top-2 mm-box--display-flex mm-box--justify-content-space-between" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative" - > - Token standard - </p> - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative" - > - ERC721 - </p> - </div> - <div - class="mm-box mm-box--margin-top-4 mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap" - > - - </div> - <div - class="mm-box mm-box--margin-top-4" - > - <h6 - class="mm-box mm-text mm-text--body-sm mm-box--color-text-alternative" - > - Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted. - </h6> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`NFT Details should match minimal props and state snapshot 3`] = `<div />`; 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`] = ` </div> </div> `; - -exports[`NFT full image should match snapshot 2`] = `<div />`; 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`] = ` </div> </body> `; - -exports[`Connect More Accounts Modal should render correctly 2`] = ` -<body> - <div - id="popover-content" - /> -</body> -`; 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`] = ` </div> </div> `; - -exports[`ConfirmLegacyGasDisplay should match snapshot 2`] = `<div />`; 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[`<BaseTransactionInfo /> renders component for contract interaction requ </div> </div> `; - -exports[`<BaseTransactionInfo /> renders component for contract interaction request 2`] = ` -<div> - <div - class="mm-box mm-box--margin-bottom-4 mm-box--padding-0 mm-box--background-color-background-default mm-box--rounded-md" - > - <div - class="mm-box simulation-details-layout mm-box--padding-3 mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column mm-box--rounded-lg mm-box--border-color-transparent box--border-style-solid box--border-width-1" - data-testid="simulation-details-layout" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-default" - > - Estimated changes - </p> - <div> - <div - aria-describedby="tippy-tooltip-1" - class="info-tooltip__tooltip-container" - data-original-title="null" - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column" - > - <div - class="mm-box" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column" - data-testid="simulation-rows-outgoing" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--align-items-flex-start" - data-testid="simulation-details-balance-change-row" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" - style="white-space: nowrap;" - > - You send - </p> - <div - class="mm-box mm-box--margin-left-auto mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column" - style="min-width: 0;" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center mm-box--background-color-error-muted mm-box--rounded-pill" - data-testid="simulation-details-amount-pill" - style="padding: 0px 8px; flex-shrink: 1; flex-basis: auto; min-width: 0;" - > - <div - style="min-width: 0;" - > - <div - aria-describedby="tippy-tooltip-5" - class="" - data-original-title="4" - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <p - class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-error-alternative" - > - - 4 - </p> - </div> - </div> - </div> - <div - class="mm-box" - data-testid="simulation-details-asset-pill" - style="flex-shrink: 1; flex-basis: auto; min-width: 0;" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center mm-box--background-color-background-alternative mm-box--rounded-pill" - style="padding: 1px 8px 1px 4px;" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-default box--border-style-solid box--border-width-1" - > - <img - alt="ETH logo" - class="mm-avatar-network__network-image" - /> - </div> - <p - class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default" - > - ETH - </p> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" - data-testid="transaction-details-section" - > - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="transaction-details-origin-row" - style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Request from - </p> - <div> - <div - aria-describedby="tippy-tooltip-2" - class="" - data-original-title="This is the site asking for your confirmation." - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - data-testid="transaction-details-origin-row-tooltip" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--color-inherit" - > - metamask.github.io - </p> - </div> - </div> - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="transaction-details-recipient-row" - style="overflow-wrap: anywhere; min-height: 24px; position: relative;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Interacting with - </p> - <div> - <div - aria-describedby="tippy-tooltip-3" - class="" - data-original-title="This is the contract you're interacting with. Protect yourself from scammers by verifying the details." - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - data-testid="transaction-details-recipient-row-tooltip" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-account mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" - > - <div - class="mm-avatar-account__jazzicon" - > - <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 16px; height: 16px; display: inline-block; background: rgb(200, 20, 98);" - > - <svg - height="16" - width="16" - x="0" - y="0" - > - <rect - fill="#018E82" - height="16" - transform="translate(0.09338133124744565 -0.04490608666778058) rotate(456.7 8 8)" - width="16" - x="0" - y="0" - /> - <rect - fill="#FB187B" - height="16" - transform="translate(-6.3240369287990985 -1.0053822898747908) rotate(305.5 8 8)" - width="16" - x="0" - y="0" - /> - <rect - fill="#237CE1" - height="16" - transform="translate(4.0809955633515935 -14.427164334848008) rotate(395.8 8 8)" - width="16" - x="0" - y="0" - /> - </svg> - </div> - </div> - </div> - <p - class="mm-box mm-text mm-text--body-md mm-box--margin-left-2 mm-box--color-inherit" - data-testid="confirm-info-row-display-name" - > - 0x88AA6...A5125 - </p> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" - data-testid="gas-fee-section" - > - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="edit-gas-fees-row" - style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Network fee - </p> - <div> - <div - aria-describedby="tippy-tooltip-4" - class="" - data-original-title="Amount paid to process the transaction on network." - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - data-testid="edit-gas-fees-row-tooltip" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center mm-box--text-align-center" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--margin-right-1 mm-box--color-text-default" - data-testid="first-gas-field" - > - 0.0001 ETH - </p> - <p - class="mm-box mm-text mm-text--body-md mm-box--margin-right-2 mm-box--color-text-alternative" - data-testid="native-currency" - > - $0.04 - </p> - <button - class="mm-box mm-text mm-button-base mm-button-base--size-sm mm-button-link mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" - data-testid="edit-gas-fee-icon" - style="text-decoration: none;" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-end-1 mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/edit.svg');" - /> - <span - class="mm-box mm-text mm-text--inherit mm-box--color-primary-default" - /> - </button> - </div> - </div> - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="gas-fee-details-speed" - style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Speed - </p> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-wrap-wrap" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--padding-inline-end-1 mm-box--color-text-alternative" - > - 🦊 Market - </p> - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" - > - <span - data-testid="gas-timing-time" - > - ~ - 0 sec - </span> - </p> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`<BaseTransactionInfo /> renders component for contract interaction request 3`] = ` -<div> - <div - class="mm-box mm-box--margin-bottom-4 mm-box--padding-0 mm-box--background-color-background-default mm-box--rounded-md" - > - <div - class="mm-box simulation-details-layout mm-box--padding-3 mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column mm-box--rounded-lg mm-box--border-color-transparent box--border-style-solid box--border-width-1" - data-testid="simulation-details-layout" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-default" - > - Estimated changes - </p> - <div> - <div - aria-describedby="tippy-tooltip-1" - class="info-tooltip__tooltip-container" - data-original-title="null" - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column" - > - <div - class="mm-box" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column" - data-testid="simulation-rows-outgoing" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--align-items-flex-start" - data-testid="simulation-details-balance-change-row" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" - style="white-space: nowrap;" - > - You send - </p> - <div - class="mm-box mm-box--margin-left-auto mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column" - style="min-width: 0;" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center mm-box--background-color-error-muted mm-box--rounded-pill" - data-testid="simulation-details-amount-pill" - style="padding: 0px 8px; flex-shrink: 1; flex-basis: auto; min-width: 0;" - > - <div - style="min-width: 0;" - > - <div - aria-describedby="tippy-tooltip-5" - class="" - data-original-title="4" - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <p - class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-error-alternative" - > - - 4 - </p> - </div> - </div> - </div> - <div - class="mm-box" - data-testid="simulation-details-asset-pill" - style="flex-shrink: 1; flex-basis: auto; min-width: 0;" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center mm-box--background-color-background-alternative mm-box--rounded-pill" - style="padding: 1px 8px 1px 4px;" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-default box--border-style-solid box--border-width-1" - > - E - </div> - <p - class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default" - > - ETH - </p> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" - data-testid="transaction-details-section" - > - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="transaction-details-origin-row" - style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Request from - </p> - <div> - <div - aria-describedby="tippy-tooltip-2" - class="" - data-original-title="This is the site asking for your confirmation." - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - data-testid="transaction-details-origin-row-tooltip" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--color-inherit" - > - metamask.github.io - </p> - </div> - </div> - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="transaction-details-recipient-row" - style="overflow-wrap: anywhere; min-height: 24px; position: relative;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Interacting with - </p> - <div> - <div - aria-describedby="tippy-tooltip-3" - class="" - data-original-title="This is the contract you're interacting with. Protect yourself from scammers by verifying the details." - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - data-testid="transaction-details-recipient-row-tooltip" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center" - > - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-account mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" - > - <div - class="mm-avatar-account__jazzicon" - > - <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 16px; height: 16px; display: inline-block; background: rgb(200, 20, 98);" - > - <svg - height="16" - width="16" - x="0" - y="0" - > - <rect - fill="#018E82" - height="16" - transform="translate(0.09338133124744565 -0.04490608666778058) rotate(456.7 8 8)" - width="16" - x="0" - y="0" - /> - <rect - fill="#FB187B" - height="16" - transform="translate(-6.3240369287990985 -1.0053822898747908) rotate(305.5 8 8)" - width="16" - x="0" - y="0" - /> - <rect - fill="#237CE1" - height="16" - transform="translate(4.0809955633515935 -14.427164334848008) rotate(395.8 8 8)" - width="16" - x="0" - y="0" - /> - </svg> - </div> - </div> - </div> - <p - class="mm-box mm-text mm-text--body-md mm-box--margin-left-2 mm-box--color-inherit" - data-testid="confirm-info-row-display-name" - > - 0x88AA6...A5125 - </p> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" - data-testid="gas-fee-section" - > - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="edit-gas-fees-row" - style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Network fee - </p> - <div> - <div - aria-describedby="tippy-tooltip-4" - class="" - data-original-title="Amount paid to process the transaction on network." - data-tooltipped="" - style="display: flex;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" - data-testid="edit-gas-fees-row-tooltip" - style="mask-image: url('./images/icons/question.svg');" - /> - </div> - </div> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center mm-box--text-align-center" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--margin-right-1 mm-box--color-text-default" - data-testid="first-gas-field" - > - 0.0001 ETH - </p> - <p - class="mm-box mm-text mm-text--body-md mm-box--margin-right-2 mm-box--color-text-alternative" - data-testid="native-currency" - > - $0.04 - </p> - <button - class="mm-box mm-text mm-button-base mm-button-base--size-sm mm-button-link mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent" - data-testid="edit-gas-fee-icon" - style="text-decoration: none;" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--margin-inline-end-1 mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/edit.svg');" - /> - <span - class="mm-box mm-text mm-text--inherit mm-box--color-primary-default" - /> - </button> - </div> - </div> - <div - class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" - data-testid="gas-fee-details-speed" - style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent;" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default" - > - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit" - > - Speed - </p> - </div> - </div> - <div - class="mm-box mm-box--display-flex mm-box--align-items-center" - > - <div - class="mm-box mm-box--display-flex mm-box--flex-wrap-wrap" - > - <p - class="mm-box mm-text mm-text--body-md mm-box--padding-inline-end-1 mm-box--color-text-alternative" - > - 🦊 Market - </p> - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-default" - > - <span - data-testid="gas-timing-time" - > - ~ - 0 sec - </span> - </p> - </div> - </div> - </div> - </div> -</div> -`; 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`] = ` </div> </div> `; - -exports[`create-named-snap-account confirmation matches snapshot 2`] = ` -<div> - <div - class="confirmation-page" - > - <div - class="mm-box confirmation-page__content mm-box--padding-0" - style="margin-top: 0px; overflow-y: auto;" - > - <div - class="mm-box name-snap-account-page mm-box--padding-4" - > - <header - class="mm-box mm-header-base mm-modal-header mm-box--padding-4 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-space-between" - > - <div - class="mm-box mm-box--width-full" - > - <h4 - class="mm-box mm-text mm-text--heading-sm mm-text--text-align-center mm-box--color-text-default" - > - Add account to MetaMask - </h4> - </div> - <div - class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" - style="min-width: 0px;" - > - <button - aria-label="Close" - class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" - > - <span - class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/close.svg');" - /> - </button> - </div> - </header> - <form - class="mm-box" - > - <div - class="mm-box mm-form-text-field mm-box--display-flex mm-box--gap-2 mm-box--flex-direction-column" - > - <label - class="mm-box mm-text mm-label mm-label--html-for mm-form-text-field__label mm-text--body-md mm-text--font-weight-medium mm-box--display-inline-flex mm-box--align-items-center mm-box--color-text-default" - for="account-name" - > - Account name - </label> - <div - class="mm-box mm-text-field mm-text-field--size-lg mm-text-field--focused mm-text-field--truncate mm-form-text-field__text-field mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--align-items-center mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-width-1 box--border-style-solid" - > - <input - autocomplete="off" - class="mm-box mm-text mm-input mm-input--disable-state-styles mm-text-field__input mm-text--body-md mm-box--margin-0 mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--color-text-default mm-box--background-color-transparent mm-box--border-style-none" - focused="true" - id="account-name" - placeholder="Suggested Account Name" - type="text" - value="" - /> - </div> - <p - class="mm-box mm-text mm-help-text mm-form-text-field__help-text mm-text--body-xs mm-box--margin-top-1 mm-box--color-text-default" - > - ​ - </p> - </div> - <div - class="mm-box mm-box--margin-top-1 mm-box--display-flex mm-box--gap-2" - > - <button - class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--block mm-button-secondary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent mm-box--rounded-pill mm-box--border-color-primary-default box--border-style-solid box--border-width-1" - data-testid="cancel-add-account-with-name" - type="button" - > - Cancel - </button> - <button - class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--block mm-button-primary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" - data-testid="submit-add-account-with-name" - data-theme="light" - type="submit" - > - Add account - </button> - </div> - </form> - </div> - </div> - <div - class="confirmation-footer" - style="box-shadow: var(--shadow-size-lg) var(--color-shadow-default);" - /> - </div> -</div> -`; 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`] = ` </div> `; -exports[`switch-ethereum-chain confirmation should match snapshot 2`] = `<div />`; - exports[`switch-ethereum-chain confirmation should show alert if there are pending txs 1`] = ` <div> <div diff --git a/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap b/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap index ca6b1f7219b6..59120196cbdf 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap +++ b/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap @@ -55,57 +55,3 @@ exports[`Interactive Replacement Token Page should reject if there are errors 1` </div> </div> `; - -exports[`Interactive Replacement Token Page should reject if there are errors 2`] = ` -<div> - <div - class="mm-box page-container" - data-testid="interactive-replacement-token" - > - <div - class="mm-box page-container__header error" - > - <div - class="mm-box page-container__title" - > - Replace custodian token - - failed - </div> - </div> - <div - class="mm-box page-container__content" - > - <div - class="mm-box interactive-replacement-token-page mm-box--margin-right-7 mm-box--margin-left-7 mm-box--display-flex mm-box--color-text-alternative" - > - <p - class="mm-box mm-text mm-text--body-md mm-text--overflow-wrap-break-word mm-box--color-text-default" - data-testid="connect-error-message" - > - Please go to displayName and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again. - </p> - </div> - </div> - <footer - class="mm-box page-container__footer mm-box--padding-4" - > - <div - class="mm-box mm-box--display-flex mm-box--gap-4" - > - <button - class="mm-box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-secondary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent mm-box--rounded-pill mm-box--border-color-primary-default box--border-style-solid box--border-width-1" - > - Reject - </button> - <button - class="mm-box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-primary mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" - data-theme="light" - > - displayName - </button> - </div> - </footer> - </div> -</div> -`; 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 <matthew.walsh@consensys.net> Date: Fri, 18 Oct 2024 11:27:18 +0100 Subject: [PATCH 191/226] 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 <pedro.figueiredo@consensys.net> Date: Fri, 18 Oct 2024 11:33:17 +0100 Subject: [PATCH 192/226] feat: dapp initiated token transfer (#27875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** - Implements new header for dapp initiated token transfer confirmations - Enables simulations component conditionally - Includes e2e tests for said confirmation <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="472" alt="Screenshot 2024-10-15 at 16 20 37" src="https://github.com/user-attachments/assets/2a15b02f-1fd3-467d-9d91-91fd90d27359"> ## **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[`<DAppInitiatedHeader /> should match snapshot 1`] = ` +<div> + <div + class="mm-box mm-box--padding-3 mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-center mm-box--background-color-background-default" + style="z-index: 2; position: relative;" + > + <h3 + class="mm-box mm-text mm-text--heading-md mm-box--color-inherit" + > + Transfer request + </h3> + <div + class="mm-box mm-box--padding-right-3" + style="margin-left: auto; position: absolute; right: 0px;" + > + <div + class="mm-box mm-box--margin-right-1 mm-box--background-color-transparent mm-box--rounded-md" + > + <button + aria-label="Advanced tx details" + class="mm-box mm-button-icon mm-button-icon--size-md mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + data-testid="header-advanced-details-button" + > + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/customize.svg');" + /> + </button> + </div> + </div> + </div> +</div> +`; 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`] = ` <div> <div - class="mm-box confirm_header__wrapper mm-box--display-flex mm-box--justify-content-space-between mm-box--align-items-center" - data-testid="confirm-header" + class="mm-box mm-box--padding-3 mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-center mm-box--background-color-background-default" + style="z-index: 2; position: relative;" > - <div - class="mm-box mm-box--padding-4 mm-box--display-flex mm-box--align-items-flex-start" + <h3 + class="mm-box mm-text mm-text--heading-md mm-box--color-inherit" > - <div - class="mm-box mm-box--margin-top-2 mm-box--display-flex" - > - <div - class="" - > - <div - class="identicon" - style="height: 32px; width: 32px; border-radius: 16px;" - > - <div - style="border-radius: 50px; overflow: hidden; padding: 0px; margin: 0px; width: 32px; height: 32px; display: inline-block; background: rgb(24, 100, 242);" - > - <svg - height="32" - width="32" - x="0" - y="0" - > - <rect - fill="#C81420" - height="32" - transform="translate(-5.505236904904678 -5.265123363737541) rotate(385.9 16 16)" - width="32" - x="0" - y="0" - /> - <rect - fill="#E9F500" - height="32" - transform="translate(10.674469406726399 12.429928159193388) rotate(184.5 16 16)" - width="32" - x="0" - y="0" - /> - <rect - fill="#FAB300" - height="32" - transform="translate(1.815455199236231 -26.03028190637629) rotate(431.3 16 16)" - width="32" - x="0" - y="0" - /> - </svg> - </div> - </div> - </div> - <div - class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network confirm_header__avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-goerli mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1" - > - G - </div> - </div> - <div - class="mm-box mm-box--margin-inline-start-4" - > - <p - class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-default" - data-testid="header-account-name" - /> - <p - class="mm-box mm-text mm-text--body-md mm-box--color-text-alternative" - data-testid="header-network-display-name" - > - Goerli - </p> - </div> - </div> + Transfer request + </h3> <div - class="mm-box mm-box--padding-4 mm-box--display-flex mm-box--align-items-flex-end" + class="mm-box mm-box--padding-right-3" + style="margin-left: auto; position: absolute; right: 0px;" > <div - class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" - style="align-self: flex-end;" + class="mm-box mm-box--margin-right-1 mm-box--background-color-transparent mm-box--rounded-md" > - <div> - <div - aria-describedby="tippy-tooltip-3" - class="" - data-original-title="Account details" - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <button - aria-label="Account details" - class="mm-box mm-button-icon mm-button-icon--size-md mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" - data-testid="header-info__account-details-button" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </button> - </div> - </div> - <div - class="mm-box mm-box--margin-right-1 mm-box--background-color-transparent mm-box--rounded-md" + <button + aria-label="Advanced tx details" + class="mm-box mm-button-icon mm-button-icon--size-md mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" + data-testid="header-advanced-details-button" > - <button - aria-label="Advanced tx details" - class="mm-box mm-button-icon mm-button-icon--size-md mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg" - data-testid="header-advanced-details-button" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/customize.svg');" - /> - </button> - </div> + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/customize.svg');" + /> + </button> </div> </div> </div> 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(<DAppInitiatedHeader />, store); +}; + +describe('<DAppInitiatedHeader />', () => { + 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 ( + <Box + display={Display.Flex} + flexDirection={FlexDirection.Row} + justifyContent={JustifyContent.center} + alignItems={AlignItems.center} + backgroundColor={BackgroundColor.backgroundDefault} + padding={3} + style={{ zIndex: 2, position: 'relative' }} + > + <Text variant={TextVariant.headingMd} color={TextColor.inherit}> + {t('transferRequest')} + </Text> + <Box + paddingRight={3} + style={{ marginLeft: 'auto', position: 'absolute', right: 0 }} + > + <AdvancedDetailsButton /> + </Box> + </Box> + ); +}; 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 <WalletInitiatedHeader />; } + return <DAppInitiatedHeader />; } 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`] = ` <div> <div - class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + class="mm-box mm-box--padding-4 mm-box--display-flex mm-box--flex-direction-column mm-box--justify-content-center mm-box--align-items-center" > - <svg - class="preloader__icon" - fill="none" - height="20" - viewBox="0 0 16 16" - width="20" - xmlns="http://www.w3.org/2000/svg" + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-token mm-text--body-lg-medium mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-muted mm-box--background-color-overlay-default mm-box--rounded-full" > - <path - clip-rule="evenodd" - d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" - fill="var(--color-primary-muted)" - fill-rule="evenodd" - /> - <mask - height="16" - id="mask0" - mask-type="alpha" - maskUnits="userSpaceOnUse" - width="16" - x="0" - y="0" - > - <path - clip-rule="evenodd" - d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" - fill="var(--color-primary-default)" - fill-rule="evenodd" - /> - </mask> - <g - mask="url(#mask0)" - > - <path - d="M6.85718 17.9999V11.4285V8.28564H-4.85711V17.9999H6.85718Z" - fill="var(--color-primary-default)" - /> - </g> - </svg> + ? + </div> + <h2 + class="mm-box mm-text mm-text--heading-lg mm-box--margin-top-3 mm-box--color-inherit" + > + Unknown + </h2> </div> <div - class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" + data-testid="confirmation__transaction-flow" > - <svg - class="preloader__icon" - fill="none" - height="20" - viewBox="0 0 16 16" - width="20" - xmlns="http://www.w3.org/2000/svg" + <div + class="mm-box mm-box--padding-3 mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" > - <path - clip-rule="evenodd" - d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" - fill="var(--color-primary-muted)" - fill-rule="evenodd" + <div + class="mm-box mm-box--display-flex" + > + <div + class="name name__missing" + > + <span + class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/question.svg');" + /> + <p + class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default" + > + 0x2e0D7...5d09B + </p> + </div> + </div> + <span + class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-muted" + style="mask-image: url('./images/icons/arrow-right.svg');" /> - <mask - height="16" - id="mask0" - mask-type="alpha" - maskUnits="userSpaceOnUse" - width="16" - x="0" - y="0" + </div> + </div> + <div + class="mm-box mm-box--margin-bottom-4 mm-box--padding-0 mm-box--background-color-background-default mm-box--rounded-md" + > + <div + class="mm-box simulation-details-layout mm-box--padding-3 mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column mm-box--rounded-lg mm-box--border-color-transparent box--border-style-solid box--border-width-1" + data-testid="simulation-details-layout" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" > - <path - clip-rule="evenodd" - d="M8 13.7143C4.84409 13.7143 2.28571 11.1559 2.28571 8C2.28571 4.84409 4.84409 2.28571 8 2.28571C11.1559 2.28571 13.7143 4.84409 13.7143 8C13.7143 11.1559 11.1559 13.7143 8 13.7143ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" - fill="var(--color-primary-default)" - fill-rule="evenodd" - /> - </mask> - <g - mask="url(#mask0)" + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center" + > + <p + class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-default" + > + Estimated changes + </p> + <div> + <div + aria-describedby="tippy-tooltip-1" + class="info-tooltip__tooltip-container" + data-original-title="null" + data-tooltipped="" + style="display: flex;" + tabindex="0" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted" + style="mask-image: url('./images/icons/question.svg');" + /> + </div> + </div> + </div> + </div> + <div + class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column" > - <path - d="M6.85718 17.9999V11.4285V8.28564H-4.85711V17.9999H6.85718Z" - fill="var(--color-primary-default)" - /> - </g> - </svg> + <div + class="mm-box" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--flex-direction-column" + data-testid="simulation-rows-outgoing" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--align-items-flex-start" + data-testid="simulation-details-balance-change-row" + > + <p + class="mm-box mm-text mm-text--body-md mm-box--color-text-default" + style="white-space: nowrap;" + > + You send + </p> + <div + class="mm-box mm-box--margin-left-auto mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-column" + style="min-width: 0;" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row" + > + <div + class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--align-items-center mm-box--background-color-error-muted mm-box--rounded-pill" + data-testid="simulation-details-amount-pill" + style="padding: 0px 8px; flex-shrink: 1; flex-basis: auto; min-width: 0;" + > + <div + style="min-width: 0;" + > + <div + aria-describedby="tippy-tooltip-3" + class="" + data-original-title="4" + data-tooltipped="" + style="display: inline;" + tabindex="0" + > + <p + class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-error-alternative" + > + - 4 + </p> + </div> + </div> + </div> + <div + class="mm-box" + data-testid="simulation-details-asset-pill" + style="flex-shrink: 1; flex-basis: auto; min-width: 0;" + > + <div + class="mm-box mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--align-items-center mm-box--background-color-background-alternative mm-box--rounded-pill" + style="padding: 1px 8px 1px 4px;" + > + <div + class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-border-default box--border-style-solid box--border-width-1" + > + E + </div> + <p + class="mm-box mm-text mm-text--body-md mm-text--ellipsis mm-box--color-text-default" + > + ETH + </p> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> </div> <div class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" @@ -222,7 +295,7 @@ exports[`TokenTransferInfo renders correctly 1`] = ` </p> <div> <div - aria-describedby="tippy-tooltip-1" + aria-describedby="tippy-tooltip-2" class="" data-original-title="Amount paid to process the transaction on network." data-tooltipped="" 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 01efc5db0005..76c419cbb0be 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 @@ -1,7 +1,9 @@ +import { screen, waitFor } from '@testing-library/react'; 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 { tEn } from '../../../../../../../test/lib/i18n-helpers'; import TokenTransferInfo from './token-transfer'; jest.mock( @@ -22,13 +24,18 @@ jest.mock('../../../../../../store/actions', () => ({ })); describe('TokenTransferInfo', () => { - it('renders correctly', () => { + it('renders correctly', async () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( <TokenTransferInfo />, 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<TransactionMeta>(); + + const isWalletInitiated = transactionMeta.origin === 'metamask'; + return ( <> <SendHeading /> <TransactionFlowSection /> + {!isWalletInitiated && ( + <ConfirmInfoSection noPadding> + <SimulationDetails + simulationData={transactionMeta.simulationData} + transactionId={transactionMeta.id} + isTransactionsRedesign + /> + </ConfirmInfoSection> + )} <TokenDetailsSection /> <GasFeesSection /> <AdvancedDetails /> 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 193/226] 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<JsonRpcParams>, 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<unknown, void> { +}): JsonRpcMiddleware<JsonRpcParams, Json> { 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<string, (...args: unknown[]) => unknown | Promise<unknown>>} hooks - Required "hooks" into our * controllers. - * @returns {import('json-rpc-engine').JsonRpcMiddleware<unknown, unknown>} The method middleware function. + * @returns {import('@metamask/json-rpc-engine').JsonRpcMiddleware<unknown, unknown>} 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<string>).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<WatchAssetParam>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<WatchAssetParam>} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<MmiCheckIfTokenIsPresentParam>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<MmiCheckIfTokenIsPresentParam>} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<MmiPortfolioParam>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<MmiPortfolioParam>} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<MmiSetAccountAndNetworkParam>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<MmiSetAccountAndNetworkParam>} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<WatchAssetParam>} _req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<WatchAssetParam>} _req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<unknown>} _req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<unknown>} _req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<unknown>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<unknown>} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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<WatchAssetParam>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object. + * @param {import('@metamask/utils').JsonRpcRequest<WatchAssetParam>} req - The JSON-RPC request object. + * @param {import('@metamask/utils').JsonRpcResponse<true>} 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 <kanthesha.devaramane@consensys.net> Date: Fri, 18 Oct 2024 14:27:22 +0100 Subject: [PATCH 194/226] feat: Convert AppStateController to typescript (#27572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. --- .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<Browser>; + +describe('AppStateController', () => { + const createAppStateController = ( + initState: Partial<AppStateControllerState> = {}, + ): { + 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<AppStateControllerState>) => { + 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<AppStateControllerState>) => { + 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<string, string>; + 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<string, boolean>; + surveyLinkLastClickedOrClosed: number | null; + signatureSecurityAlertResponses: Record<string, SecurityAlertResponse>; + // States used for displaying the changed network toast + switchedNetworkDetails: Record<string, string> | 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<AppStateControllerState>; + + 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<Preferences> }) => { + 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<void> { + 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<void>; - ///: 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<void>} 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 195/226] =?UTF-8?q?feat:=20add=20=E2=80=9CIncomplete=20Ass?= =?UTF-8?q?et=20Displayed=E2=80=9D=20metric=20&=20fix:=20should=20only=20s?= =?UTF-8?q?et=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 <jyotipuri@gmail.com> --- .../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`] = ` </div> </div> `; - -exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = ` -<div> - <div - class="mm-box" - > - <div - class="mm-box mm-box--display-flex mm-box--justify-content-flex-end" - > - <div - class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0" - > - <div> - <p - class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl" - data-testid="simulation-token-value" - style="padding-top: 1px; padding-bottom: 1px;" - > - #4321 - </p> - </div> - </div> - <div - class="mm-box mm-box--display-flex" - > - <div - class="name name__missing" - > - <span - class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/question.svg');" - /> - <p - class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default" - > - 0xA0b86...6eB48 - </p> - </div> - </div> - </div> - <div - class="mm-box" - /> - </div> -</div> -`; 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( <PermitSimulationValueDisplay tokenContract="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - tokenId="4321" + value="4321" />, 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<string, TreeData> | TreeData[]; @@ -78,9 +77,9 @@ const NONE_DATE_VALUE = -1; * * @param dataTreeData */ -const getTokenDecimalsOfDataTree = async ( +const getTokenContractInDataTree = ( dataTreeData: Record<string, TreeData> | TreeData[], -): Promise<void | number> => { +): 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 ( <Box width={BlockSize.Full}> 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( <ConfirmInfoRowTypedSignDataV1 data={unapprovedTypedSignMsgV1.msgParams?.data as TypedSignDataV1Type} />, + mockStore, ); expect(container).toMatchSnapshot(); }); it('should return null if data is not defined', () => { - const { container } = render( + const { container } = renderWithProvider( <ConfirmInfoRowTypedSignDataV1 data={undefined} />, + 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<TokenDetailsERC20>( + 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<AssetsContractController['getERC20Standard']>['getDetails'] + > +> & { decimalsNumber: number }; + +export type TokenDetailsERC721 = Awaited< + ReturnType< + ReturnType<AssetsContractController['getERC721Standard']>['getDetails'] + > +>; + +export type TokenDetailsERC1155 = Awaited< + ReturnType< + ReturnType<AssetsContractController['getERC1155Standard']>['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<number> => { +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<TokenDetails | Record<string, never>> => { 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<number> => { + 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 196/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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 197/226] 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 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 <monte.lai@consensys.net> Date: Fri, 18 Oct 2024 23:38:01 +0800 Subject: [PATCH 198/226] feat: add BTC send flow (#27964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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(<BtcOverview />, 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 <jongsun.suh@icloud.com> Date: Fri, 18 Oct 2024 13:21:15 -0400 Subject: [PATCH 199/226] 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: <img width="1513" alt="Screenshot 2024-10-16 at 9 45 28 AM" src="https://github.com/user-attachments/assets/371cb728-5153-4520-9efb-d412f6e40baa"> - Load testing via revert of https://github.com/MetaMask/metamask-extension/pull/23933: <img width="1513" alt="Screenshot 2024-10-16 at 9 45 40 AM" src="https://github.com/user-attachments/assets/c51e6332-35c6-42cf-a1ee-b1f8f7e665b0"> ## **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 = ({ <ButtonBase className={classnames('multichain-account-picker', className)} data-testid="account-menu-icon" - onClick={onClick} + onClick={() => { + trace({ name: TraceName.AccountList }); + onClick(); + }} backgroundColor={BackgroundColor.transparent} borderRadius={BorderRadius.LG} ellipsis From a9df78b942df10c8eb25572feff22f045bee0a62 Mon Sep 17 00:00:00 2001 From: David Walsh <davidwalsh83@gmail.com> Date: Fri, 18 Oct 2024 13:44:34 -0500 Subject: [PATCH 200/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 <danjm.com@gmail.com> Date: Sat, 19 Oct 2024 02:33:30 -0230 Subject: [PATCH 201/226] 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 <markjstacey@gmail.com> --- 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 <guillaumeroux123@gmail.com> Date: Sat, 19 Oct 2024 12:09:35 +0200 Subject: [PATCH 202/226] 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 = () => ( <Container> <Box> <Input name="number-input" type="number" /> </Box> <Footer> <Button name="cancel">Cancel</Button> <Button name="confirm">Confirm</Button> </Footer> </Container> ); ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **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 <monte.lai@consensys.net> Date: Mon, 21 Oct 2024 16:51:53 +0800 Subject: [PATCH 203/226] feat: add migration 131 (#27364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. --------- Co-authored-by: Charly Chevalier <charly.chevalier@consensys.net> --- 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<string, unknown>; + }) => { + 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<string, unknown>; +}; + +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<VersionedData> { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record<string, unknown>): 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 204/226] 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 <charly.chevalier@consensys.net> --- .../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<string>} + */ + 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<void, MetaMaskReduxState, unknown, AnyAction> { + log.debug(`background.getDeviceNameForMetric`, deviceName, hdPath); + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(showLoadingIndication()); + + let result: string; + try { + result = await submitRequestToBackground<string>( + '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<void, MetaMaskReduxState, unknown, AnyAction> { From 29e1c5b31f1decbc3e6d50eac82d972727b6d81d Mon Sep 17 00:00:00 2001 From: Frederik Bolding <frederik.bolding@gmail.com> Date: Mon, 21 Oct 2024 12:13:05 +0200 Subject: [PATCH 205/226] fix: Automatically expand first insight (#27872) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **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<SnapInsightProps> = ({ snapId, interfaceId, loading, + isExpanded, }) => { const t = useI18nContext(); const { name: snapName } = useSelector((state) => @@ -57,6 +59,7 @@ export const SnapInsight: React.FunctionComponent<SnapInsightProps> = ({ <Delineator headerComponent={headerComponent} isLoading={loading} + isExpanded={isExpanded} contentBoxProps={ loading ? undefined diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx index 5cedcaffbbc6..b04865ce88f1 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { Text } from '@metamask/snaps-sdk/jsx'; -import { fireEvent } from '@testing-library/react'; import { getMockPersonalSignConfirmState, @@ -58,8 +57,6 @@ describe('SnapsSection', () => { 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) => ( <SnapInsight key={snapId} snapId={snapId} interfaceId={interfaceId} loading={loading} + isExpanded={index === 0} /> ))} </Box> From 81f46786467b9475b1f4ca6b08e0ed40323c102e Mon Sep 17 00:00:00 2001 From: Jyoti Puri <jyotipuri@gmail.com> Date: Mon, 21 Oct 2024 18:17:24 +0530 Subject: [PATCH 206/226] 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<DecodedTransactionDataResponse | undefined> { const { currentConfirmation } = useConfirmContext<TransactionMeta>(); + 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, ); From 6173a139097293e63e1d832256a4baabd88ae481 Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:53:38 -0400 Subject: [PATCH 207/226] test: Completing missing step for import ERC1155 token origin dapp in existing E2E test (#27680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Adding missing Watch ERC1155 Asset step and fully cover the manual scenario [here](https://github.com/MetaMask/metamask-extension/blob/develop/test/manual-scenarios/tokens/import%20erc1155%20token%20origin%20dapp.csv) in the E2E test `test/e2e/tests/tokens/nft/erc1155-interaction.spec.js` <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27680?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27371 ## **Manual testing steps** 1. Run `yarn test:e2e:single --browser=chrome test/e2e/tests/tokens/nft/erc1155-interaction.spec.js` 2. test should finish without failure 3. The last test run should watch asset erc1155. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js index 1fed3946dea9..c635d465353a 100644 --- a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js @@ -1,4 +1,5 @@ const { strict: assert } = require('assert'); +const { mockNetworkStateOld } = require('../../../../stub/networks'); const { withFixtures, DAPP_URL, @@ -19,6 +20,15 @@ describe('ERC1155 NFTs testdapp interaction', function () { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() + .withNetworkController( + mockNetworkStateOld({ + chainId: '0x539', + nickname: 'Localhost 8545', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io/', + }), + ) .build(), ganacheOptions: defaultGanacheOptions, smartContract, @@ -59,6 +69,36 @@ describe('ERC1155 NFTs testdapp interaction', function () { css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); + await driver.clickElement('[data-testid="activity-list-item-action"]'); + await driver.clickElement({ + text: 'View on block explorer', + tag: 'a', + }); + + // Switch to block explorer + await driver.switchToWindowWithTitle('E2E Test Page'); + await driver.findElement('[data-testid="empty-page-body"]'); + // Verify block explorer + await driver.waitForUrl({ + url: 'https://etherscan.io/tx/0xfe4428397f7913875783c5c0dad182937b596148295bc33c7f08d74fdee8897f', + }); + + // switch to Dapp + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.fill('#watchAssetInput', '1'); + await driver.clickElement('#watchAssetButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + await driver.clickElementSafe('[data-testid="popover-close"]'); + await driver.clickElement('[data-testid="account-overview__nfts-tab"]'); + await driver.clickElement('[data-testid="nft-item"]'); }, ); }); From f995e3cb193681e2186e53b1d673728eddd1202f Mon Sep 17 00:00:00 2001 From: Howard Braham <howrad@gmail.com> Date: Mon, 21 Oct 2024 11:03:06 -0700 Subject: [PATCH 208/226] ci: reduced Sentry frequency on CircleCI develop (#27912) ## **Description** Cut in half Sentry frequency on CircleCI develop [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27912?quickstart=1) ## **Related issues** ## **Manual testing steps** ## **Screenshots/Recordings** ## **Pre-merge author checklist** ## **Pre-merge reviewer checklist** --- app/scripts/lib/setupSentry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 627f46fa79fa..1b9e9f4ddbfc 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -125,7 +125,7 @@ function getTracesSampleRate(sentryTarget) { // Report very frequently on develop branch, and never on other branches // (Unless you use a `flags = {"sentry": {"tracesSampleRate": x.xx}}` override) if (flags.circleci.branch === 'develop') { - return 0.03; + return 0.015; } return 0; } From 30d7f1c6d0d5386eab3368691144c7625375b60c Mon Sep 17 00:00:00 2001 From: MetaMask Bot <metamaskbot@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:44:41 +0000 Subject: [PATCH 209/226] Version v12.6.0 --- CHANGELOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 2 files changed, 216 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b33f07fb3d5..fda366ce5558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,219 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.6.0] +### Uncategorized +- ci: reduced Sentry frequency on CircleCI develop ([#27912](https://github.com/MetaMask/metamask-extension/pull/27912)) +- chore:Master sync ([#27935](https://github.com/MetaMask/metamask-extension/pull/27935)) +- Merge origin/develop into master-sync +- test: Completing missing step for import ERC1155 token origin dapp in existing E2E test ([#27680](https://github.com/MetaMask/metamask-extension/pull/27680)) +- fix: error in navigating between transaction when one of the transaction is approve all ([#27985](https://github.com/MetaMask/metamask-extension/pull/27985)) +- fix: Automatically expand first insight ([#27872](https://github.com/MetaMask/metamask-extension/pull/27872)) +- feat(metametrics): use specific `account_hardware_type` for OneKey devices ([#27296](https://github.com/MetaMask/metamask-extension/pull/27296)) +- feat: add migration 131 ([#27364](https://github.com/MetaMask/metamask-extension/pull/27364)) +- fix(snaps): Remove arrows of custom UI inputs ([#27953](https://github.com/MetaMask/metamask-extension/pull/27953)) +- chore: Disable account syncing in prod ([#27943](https://github.com/MetaMask/metamask-extension/pull/27943)) +- test: Remove delays from onboarding tests ([#27961](https://github.com/MetaMask/metamask-extension/pull/27961)) +- perf: Create custom trace to measure performance of opening the account list ([#27907](https://github.com/MetaMask/metamask-extension/pull/27907)) +- feat: add BTC send flow ([#27964](https://github.com/MetaMask/metamask-extension/pull/27964)) +- fix: flaky test `Confirmation Redesign ERC721 Approve Component Submit an Approve transaction @no-mmi Sends a type 2 transaction (EIP1559)` ([#27928](https://github.com/MetaMask/metamask-extension/pull/27928)) +- fix: lint-lockfile flaky job by changing resources from medium to medium-plus ([#27950](https://github.com/MetaMask/metamask-extension/pull/27950)) +- feat: add “Incomplete Asset Displayed” metric & fix: should only set default decimals if ERC20 ([#27494](https://github.com/MetaMask/metamask-extension/pull/27494)) +- feat: Convert AppStateController to typescript ([#27572](https://github.com/MetaMask/metamask-extension/pull/27572)) +- chore(deps): upgrade from json-rpc-engine to @metamask/json-rpc-engine ([#22875](https://github.com/MetaMask/metamask-extension/pull/22875)) +- feat: dapp initiated token transfer ([#27875](https://github.com/MetaMask/metamask-extension/pull/27875)) +- chore: bump signature controller to remove message managers ([#27787](https://github.com/MetaMask/metamask-extension/pull/27787)) +- chore: add testing-library/dom dependency ([#27493](https://github.com/MetaMask/metamask-extension/pull/27493)) +- test: [POM] Migrate contract interaction with snap account e2e tests to page object modal ([#27924](https://github.com/MetaMask/metamask-extension/pull/27924)) +- fix: bump message signing snap to support portfolio automatic connections ([#27936](https://github.com/MetaMask/metamask-extension/pull/27936)) +- fix: hide options menu that was being shown for preinstalled Snaps ([#27937](https://github.com/MetaMask/metamask-extension/pull/27937)) +- fix: bump `@metamask/ppom-validator` from `0.34.0` to `0.35.1` ([#27939](https://github.com/MetaMask/metamask-extension/pull/27939)) +- fix: add APE network icon ([#27841](https://github.com/MetaMask/metamask-extension/pull/27841)) +- feat: NFT permit simulations ([#27825](https://github.com/MetaMask/metamask-extension/pull/27825)) +- fix: fix currency display when tokenToFiatConversion rate is not avai… ([#27893](https://github.com/MetaMask/metamask-extension/pull/27893)) +- feat: convert AlertController to typescript ([#27764](https://github.com/MetaMask/metamask-extension/pull/27764)) +- feat(TXL-435): turn smart transactions on by default for new users ([#27885](https://github.com/MetaMask/metamask-extension/pull/27885)) +- feat: Add transaction flow and details sections ([#27654](https://github.com/MetaMask/metamask-extension/pull/27654)) +- fix: flaky test `Vault Decryptor Page is able to decrypt the vault pasting the text in the vault-decryptor webapp` ([#27921](https://github.com/MetaMask/metamask-extension/pull/27921)) +- chore: bump `@metamask/eth-snap-keyring` to version 4.4.0 ([#27864](https://github.com/MetaMask/metamask-extension/pull/27864)) +- fix: flaky tests `Add existing token using search renders the balance for the chosen token` ([#27853](https://github.com/MetaMask/metamask-extension/pull/27853)) +- feat(logging): add extension request logging and retrieval ([#27655](https://github.com/MetaMask/metamask-extension/pull/27655)) +- test: Update test-dapp to verison 8.7.0 ([#27816](https://github.com/MetaMask/metamask-extension/pull/27816)) +- fix: fall back to bundled chainlist ([#23392](https://github.com/MetaMask/metamask-extension/pull/23392)) +- fix: SonarCloud for forks ([#27700](https://github.com/MetaMask/metamask-extension/pull/27700)) +- fix(deps): update from eth-rpc-errors to @metamask/rpc-errors (cause edition) ([#24496](https://github.com/MetaMask/metamask-extension/pull/24496)) +- fix: swapQuotesError as a property in the reported metric ([#27712](https://github.com/MetaMask/metamask-extension/pull/27712)) +- chore: Bump Snaps packages ([#27376](https://github.com/MetaMask/metamask-extension/pull/27376)) +- chore: update @metamask/bitcoin-wallet-snap to 0.7.0 ([#27730](https://github.com/MetaMask/metamask-extension/pull/27730)) +- fix: Onboarding: Code style nits ([#27767](https://github.com/MetaMask/metamask-extension/pull/27767)) +- fix: updated edit modals ([#27623](https://github.com/MetaMask/metamask-extension/pull/27623)) +- feat: use asset pickers with network dropdown in cross-chain swaps page ([#27522](https://github.com/MetaMask/metamask-extension/pull/27522)) +- test: set ENABLE_MV3 automatically ([#27748](https://github.com/MetaMask/metamask-extension/pull/27748)) +- feat: Adding typed sign support for NFT permit ([#27796](https://github.com/MetaMask/metamask-extension/pull/27796)) +- fix: Contract Interaction - cannot read the property `text_signature` ([#27686](https://github.com/MetaMask/metamask-extension/pull/27686)) +- feat: Use requested permissions as default selected values for AmonHenV2 connection flow with case insensitive address comparison ([#27517](https://github.com/MetaMask/metamask-extension/pull/27517)) +- test: [POM] Migrate signature with snap account e2e tests to page object modal ([#27829](https://github.com/MetaMask/metamask-extension/pull/27829)) +- fix: flaky test `ERC1155 NFTs testdapp interaction should batch transfers ERC1155 token` ([#27897](https://github.com/MetaMask/metamask-extension/pull/27897)) +- chore: Master sync following v12.4.1 ([#27793](https://github.com/MetaMask/metamask-extension/pull/27793)) +- fix: flaky test `Permissions sets permissions and connect to Dapp` ([#27888](https://github.com/MetaMask/metamask-extension/pull/27888)) +- fix: flaky test `ERC721 NFTs testdapp interaction should prompt users to add their NFTs to their wallet (all at once)` ([#27889](https://github.com/MetaMask/metamask-extension/pull/27889)) +- fix: flaky test `Wallet Revoke Permissions should revoke eth_accounts permissions via test dapp` ([#27894](https://github.com/MetaMask/metamask-extension/pull/27894)) +- fix: flaky test `Snap Account Signatures and Disconnects can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)` ([#27887](https://github.com/MetaMask/metamask-extension/pull/27887)) +- test(mock-e2e): add private domains logic for the privacy report ([#27844](https://github.com/MetaMask/metamask-extension/pull/27844)) +- fix: SENTRY_DSN_FAKE problem ([#27881](https://github.com/MetaMask/metamask-extension/pull/27881)) +- chore: remove unused swaps code ([#27679](https://github.com/MetaMask/metamask-extension/pull/27679)) +- test(TXL-308): initial e2e for stx using swaps ([#27215](https://github.com/MetaMask/metamask-extension/pull/27215)) +- feat: upgrade assets-controllers to v38.3.0 ([#27755](https://github.com/MetaMask/metamask-extension/pull/27755)) +- fix: nonce value when there are multiple transactions in parallel ([#27874](https://github.com/MetaMask/metamask-extension/pull/27874)) +- fix: phishing test to not check c2 domains ([#27846](https://github.com/MetaMask/metamask-extension/pull/27846)) +- feat: use messenger in AccountTracker to get Preferences state ([#27711](https://github.com/MetaMask/metamask-extension/pull/27711)) +- fix: "Update Network: should update added rpc url for exis..." flaky tests ([#27437](https://github.com/MetaMask/metamask-extension/pull/27437)) +- feat: update copy for 'Default settings' ([#27821](https://github.com/MetaMask/metamask-extension/pull/27821)) +- fix: updated permissions flow copy changes ([#27658](https://github.com/MetaMask/metamask-extension/pull/27658)) +- fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi` ([#27834](https://github.com/MetaMask/metamask-extension/pull/27834)) +- fix: hackily wait longer for linea swap approval tx to increase chance of success ([#27810](https://github.com/MetaMask/metamask-extension/pull/27810)) +- fix: flaky test `MultiRpc: should select rpc from settings @no-mmi` ([#27858](https://github.com/MetaMask/metamask-extension/pull/27858)) +- perf: include custom traces in benchmark results ([#27701](https://github.com/MetaMask/metamask-extension/pull/27701)) +- fix: Reset nonce as network is switched ([#27789](https://github.com/MetaMask/metamask-extension/pull/27789)) +- fix: dismiss addToken modal for mmi ([#27855](https://github.com/MetaMask/metamask-extension/pull/27855)) +- fix(multichain): fix eth send flow (from dapp) when a btc account is selected ([#27566](https://github.com/MetaMask/metamask-extension/pull/27566)) +- chore: Add react-beautiful-dnd to deprecated packages list ([#27856](https://github.com/MetaMask/metamask-extension/pull/27856)) +- feat: Create a quality gate for typescript coverage ([#27717](https://github.com/MetaMask/metamask-extension/pull/27717)) +- feat: preferences controller to base controller v2 ([#27398](https://github.com/MetaMask/metamask-extension/pull/27398)) +- revert: use networkClientId to resolve chainId in PPOM Middleware ([#27570](https://github.com/MetaMask/metamask-extension/pull/27570)) +- feat: Added metrics for edit networks and accounts ([#27820](https://github.com/MetaMask/metamask-extension/pull/27820)) +- fix: no connected state for permissions page ([#27660](https://github.com/MetaMask/metamask-extension/pull/27660)) +- feat: remove phishing detection from onboarding Security group ([#27819](https://github.com/MetaMask/metamask-extension/pull/27819)) +- ci: Revert minimum E2E timeout to 20 minutes ([#27827](https://github.com/MetaMask/metamask-extension/pull/27827)) +- fix: disable balance checker for Sepolia in account tracker ([#27763](https://github.com/MetaMask/metamask-extension/pull/27763)) +- ci: Improve validation for `sentry:publish` script ([#26580](https://github.com/MetaMask/metamask-extension/pull/26580)) +- test: Fix Vault Decryptor Page e2e test on develop branch ([#27794](https://github.com/MetaMask/metamask-extension/pull/27794)) +- chore: remove old token details page ([#27774](https://github.com/MetaMask/metamask-extension/pull/27774)) +- chore: remove token list display component ([#27772](https://github.com/MetaMask/metamask-extension/pull/27772)) +- chore: update Trezor Connect to v9.4.0, remove workarounds ([#27112](https://github.com/MetaMask/metamask-extension/pull/27112)) +- test: [POM] Migrate transaction with snap account e2e tests to page object modal ([#27760](https://github.com/MetaMask/metamask-extension/pull/27760)) +- fix(snaps): Restore confirmation switching on routed confirmation ([#27753](https://github.com/MetaMask/metamask-extension/pull/27753)) +- Merge origin/develop into master-sync +- test: Onboarding: Fix vault-decryption-chrome.spec.js ([#27779](https://github.com/MetaMask/metamask-extension/pull/27779)) +- feat: support gas fee flows in standard swaps ([#27612](https://github.com/MetaMask/metamask-extension/pull/27612)) +- feat: Token send heading component ([#27562](https://github.com/MetaMask/metamask-extension/pull/27562)) +- feat: adds the new default settings view to onboarding ([#24562](https://github.com/MetaMask/metamask-extension/pull/24562)) +- chore(3212): remove alert settings ([#27709](https://github.com/MetaMask/metamask-extension/pull/27709)) +- docs: remove outdated Medium link, update "Twitter" to "X" ([#26692](https://github.com/MetaMask/metamask-extension/pull/26692)) +- fix: Replace 'transaction fees' with 'network fees' in the insufficie… ([#27762](https://github.com/MetaMask/metamask-extension/pull/27762)) +- fix: issue with Snap title in Snap Authorship Header ([#27752](https://github.com/MetaMask/metamask-extension/pull/27752)) +- fix: SIWE signature page displays parsed URI instead of domain ([#27754](https://github.com/MetaMask/metamask-extension/pull/27754)) +- fix: updated toasts component and copy ([#27656](https://github.com/MetaMask/metamask-extension/pull/27656)) +- feat: add network picker to AssetPicker ([#26559](https://github.com/MetaMask/metamask-extension/pull/26559)) +- fix(btc): fix jazzicons generations ([#27662](https://github.com/MetaMask/metamask-extension/pull/27662)) +- feat: Release Chain Permissions ([#27561](https://github.com/MetaMask/metamask-extension/pull/27561)) +- feat: upgrade assets-controllers to v38.2.0 ([#27629](https://github.com/MetaMask/metamask-extension/pull/27629)) +- ci: followup to CircleCI Sentry reporting ([#27548](https://github.com/MetaMask/metamask-extension/pull/27548)) +- chore: Master sync ([#27729](https://github.com/MetaMask/metamask-extension/pull/27729)) +- fix(multichain): fix getMultichainCurrentCurrency selector ([#27726](https://github.com/MetaMask/metamask-extension/pull/27726)) +- fix: Limit amount of decimals on spending cap modal ([#27672](https://github.com/MetaMask/metamask-extension/pull/27672)) +- Merge origin/develop into master-sync +- test: [POM] Migrate create snap account e2e tests to page object modal ([#27697](https://github.com/MetaMask/metamask-extension/pull/27697)) +- fix: Prefer token symbol to token name ([#27693](https://github.com/MetaMask/metamask-extension/pull/27693)) +- fix(btc): fetch btc balance right after account creation ([#27628](https://github.com/MetaMask/metamask-extension/pull/27628)) +- fix: UI startup with no Sentry DSN ([#27714](https://github.com/MetaMask/metamask-extension/pull/27714)) +- feat: Sort/Import Tokens in Extension ([#27184](https://github.com/MetaMask/metamask-extension/pull/27184)) +- ci: make git-diff-develop work for PRs from foreign repos ([#27268](https://github.com/MetaMask/metamask-extension/pull/27268)) +- test: Convert json-rpc e2e tests to TypeScript ([#27659](https://github.com/MetaMask/metamask-extension/pull/27659)) +- fix: allow getAddTransactionRequest to pass through other params ([#27117](https://github.com/MetaMask/metamask-extension/pull/27117)) +- perf: add tags to UI startup trace ([#27550](https://github.com/MetaMask/metamask-extension/pull/27550)) +- fix: Disable redirecting Extension users using beta & flask build and dev env to the existing offboarding page ([#27226](https://github.com/MetaMask/metamask-extension/pull/27226)) +- feat(NOTIFY-1193): add profile sync dev menu ([#27666](https://github.com/MetaMask/metamask-extension/pull/27666)) +- refactor: Typescript conversion of log-web3-shim-usage.js ([#23732](https://github.com/MetaMask/metamask-extension/pull/23732)) +- test: removing race condition for asserting inner values (PR-#2) ([#27664](https://github.com/MetaMask/metamask-extension/pull/27664)) +- fix(btc): fix address validation ([#27690](https://github.com/MetaMask/metamask-extension/pull/27690)) +- chore: Update coverage.json ([#27696](https://github.com/MetaMask/metamask-extension/pull/27696)) +- fix: test coverage quality gate ([#27691](https://github.com/MetaMask/metamask-extension/pull/27691)) +- fix: banner alert to render multiple general alerts ([#27339](https://github.com/MetaMask/metamask-extension/pull/27339)) +- refactor: routes constants ([#27078](https://github.com/MetaMask/metamask-extension/pull/27078)) +- fix: Test coverage quality gate ([#27581](https://github.com/MetaMask/metamask-extension/pull/27581)) +- feat: Adding delete metametrics data to security and privacy tab ([#24571](https://github.com/MetaMask/metamask-extension/pull/24571)) +- feat(stx): animations and cosmetic changes to smart transaction status page ([#27650](https://github.com/MetaMask/metamask-extension/pull/27650)) +- build: add lottie-web dependency to extension ([#27632](https://github.com/MetaMask/metamask-extension/pull/27632)) +- fix(btc): do not show percentage for tokens ([#27637](https://github.com/MetaMask/metamask-extension/pull/27637)) +- feat: support Etherscan API keys ([#27611](https://github.com/MetaMask/metamask-extension/pull/27611)) +- feat: change survey timeout time from a week to a day ([#27603](https://github.com/MetaMask/metamask-extension/pull/27603)) +- fix: Design papercuts for redesigned transactions ([#27605](https://github.com/MetaMask/metamask-extension/pull/27605)) +- test: removing race condition for asserting inner values (PR-#1) ([#27606](https://github.com/MetaMask/metamask-extension/pull/27606)) +- test: [POM] Migrate Snap Simple Keyring page and Snap List page to page object modal ([#27327](https://github.com/MetaMask/metamask-extension/pull/27327)) +- fix: fix sentry reading undefined ([#27584](https://github.com/MetaMask/metamask-extension/pull/27584)) +- fix: fix sentry reading null ([#27582](https://github.com/MetaMask/metamask-extension/pull/27582)) +- fix(btc): disable balanceIsCached flag ([#27636](https://github.com/MetaMask/metamask-extension/pull/27636)) +- chore: update accounts related packages ([#27284](https://github.com/MetaMask/metamask-extension/pull/27284)) +- chore: set bridge src network, tokens and top assets ([#26214](https://github.com/MetaMask/metamask-extension/pull/26214)) +- test: [Snaps E2E] add delay to installed snaps test to reduce flaking ([#27521](https://github.com/MetaMask/metamask-extension/pull/27521)) +- chore: set bridge dest network, tokens and top assets ([#26213](https://github.com/MetaMask/metamask-extension/pull/26213)) +- fix: fix reading address from market data ([#27604](https://github.com/MetaMask/metamask-extension/pull/27604)) +- feat: Migrate AccountTrackerController to BaseController v2 ([#27258](https://github.com/MetaMask/metamask-extension/pull/27258)) +- fix: disable transaction data decode if deployment ([#27586](https://github.com/MetaMask/metamask-extension/pull/27586)) +- fix: revert jest collect coverage patterns ([#27583](https://github.com/MetaMask/metamask-extension/pull/27583)) +- fix: add amount row for contract deployment ([#27594](https://github.com/MetaMask/metamask-extension/pull/27594)) +- fix: "Dapp viewed Event @no-mmi is sent when refreshing da..." flaky test ([#27381](https://github.com/MetaMask/metamask-extension/pull/27381)) +- chore: fix deps audit ([#27620](https://github.com/MetaMask/metamask-extension/pull/27620)) +- fix: Max approval and array value spending cap bugs ([#27573](https://github.com/MetaMask/metamask-extension/pull/27573)) +- feat: add power users survey support ([#27361](https://github.com/MetaMask/metamask-extension/pull/27361)) +- fix: Recreate offscreen document if it already exists ([#27596](https://github.com/MetaMask/metamask-extension/pull/27596)) +- fix: flaky test `Block Explorer links to the token tracker in the explorer` ([#27599](https://github.com/MetaMask/metamask-extension/pull/27599)) +- fix(snaps): `Copyable` more button color ([#27600](https://github.com/MetaMask/metamask-extension/pull/27600)) +- fix: flaky test `Import flow allows importing multiple tokens from search` ([#27567](https://github.com/MetaMask/metamask-extension/pull/27567)) +- fix(27428): fix if we type enter anything followed by a \ in settings search ([#27432](https://github.com/MetaMask/metamask-extension/pull/27432)) +- fix: flaky test `Address Book Edit entry in address book` due to race condition with mmi menu ([#27557](https://github.com/MetaMask/metamask-extension/pull/27557)) +- refactor: Typescript conversion of get-provider-state.js ([#23635](https://github.com/MetaMask/metamask-extension/pull/23635)) +- chore: Use "gas_included" event prop ([#27559](https://github.com/MetaMask/metamask-extension/pull/27559)) +- fix: mock locale in unit test ([#27574](https://github.com/MetaMask/metamask-extension/pull/27574)) +- feat: codefence Account Watcher for flask ([#27543](https://github.com/MetaMask/metamask-extension/pull/27543)) +- chore: start upgrade to React Router v6 ([#27185](https://github.com/MetaMask/metamask-extension/pull/27185)) +- fix: AmonHenV2 connection flow incremental permitted chain approval and account address case comparison ([#27518](https://github.com/MetaMask/metamask-extension/pull/27518)) +- fix: flaky test `Backup and Restore should backup the account settings` ([#27565](https://github.com/MetaMask/metamask-extension/pull/27565)) +- fix: Apply flex to Snaps buttons only when containing images and icons ([#27564](https://github.com/MetaMask/metamask-extension/pull/27564)) +- feat: aggregated balance feature ([#27097](https://github.com/MetaMask/metamask-extension/pull/27097)) +- feat: Add redesign integration tests ([#27259](https://github.com/MetaMask/metamask-extension/pull/27259)) +- fix: flaky test `4byte setting does not try to get contract method name from 4byte when the setting is off` ([#27560](https://github.com/MetaMask/metamask-extension/pull/27560)) +- feat: add merge queue ([#26871](https://github.com/MetaMask/metamask-extension/pull/26871)) +- feat: remove squiggle animation from swaps smart transactions ([#27264](https://github.com/MetaMask/metamask-extension/pull/27264)) +- feat: Enable gas included swaps ([#27427](https://github.com/MetaMask/metamask-extension/pull/27427)) +- fix(snaps): Fix custom UI buttons submitting forms ([#27531](https://github.com/MetaMask/metamask-extension/pull/27531)) +- chore: Master sync following v12.3.1 ([#27538](https://github.com/MetaMask/metamask-extension/pull/27538)) +- Merge origin/develop into master-sync +- fix(NOTIFY-1171): account syncing performance and bug fixes ([#27529](https://github.com/MetaMask/metamask-extension/pull/27529)) +- fix: genUnapprovedApproveConfirmation import path ([#27530](https://github.com/MetaMask/metamask-extension/pull/27530)) +- fix(snaps): Keep focus on input if interface re-renders ([#27429](https://github.com/MetaMask/metamask-extension/pull/27429)) +- fix: Allow state updates in Snaps interfaces to state values that are falsy ([#27488](https://github.com/MetaMask/metamask-extension/pull/27488)) +- fix: updated ui for connect and review page ([#27478](https://github.com/MetaMask/metamask-extension/pull/27478)) +- feat: Custom header for wallet initiated confirmations ([#27391](https://github.com/MetaMask/metamask-extension/pull/27391)) +- feat: convert account tracker to typescript ([#27231](https://github.com/MetaMask/metamask-extension/pull/27231)) +- fix: Fix snaps permission connection for `CHAIN_PERMISSIONS` feature flag ([#27459](https://github.com/MetaMask/metamask-extension/pull/27459)) +- fix: flaky test `Navigation Signature - Different signature types initiates multiple signatures and rejects all` ([#27481](https://github.com/MetaMask/metamask-extension/pull/27481)) +- feat: Double Sentry performance trace sample rate ([#27468](https://github.com/MetaMask/metamask-extension/pull/27468)) +- ci: Expand github bot policy update comment to be more actionable ([#27242](https://github.com/MetaMask/metamask-extension/pull/27242)) +- chore: Add `useLedgerConnection` unit tests ([#27358](https://github.com/MetaMask/metamask-extension/pull/27358)) +- ci: Sentry reporting only on develop branch, with Git message overrides ([#27412](https://github.com/MetaMask/metamask-extension/pull/27412)) +- test: Fix flaky permit test ([#27450](https://github.com/MetaMask/metamask-extension/pull/27450)) +- fix: removed closeMenu for ConnectedAccountsMenu ([#27460](https://github.com/MetaMask/metamask-extension/pull/27460)) +- fix(snaps): Set proper text color for secondary button ([#27335](https://github.com/MetaMask/metamask-extension/pull/27335)) +- chore: set bridge selected tokens and amount ([#26212](https://github.com/MetaMask/metamask-extension/pull/26212)) +- fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi`aded ([#27420](https://github.com/MetaMask/metamask-extension/pull/27420)) +- fix: flaky test `Responsive UI Send Transaction from responsive window` ([#27417](https://github.com/MetaMask/metamask-extension/pull/27417)) +- fix: flaky test `Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx should queue send tx after switch network confirmation and transaction should target the correct network after switch is confirmed` ([#27352](https://github.com/MetaMask/metamask-extension/pull/27352)) +- fix: Change speed key color ([#27416](https://github.com/MetaMask/metamask-extension/pull/27416)) +- feat: Display setApprovalForAll and revoke setApprovalForAll to users… ([#27401](https://github.com/MetaMask/metamask-extension/pull/27401)) +- fix: "Warning: Invalid argument supplied to oneOfType" ([#27267](https://github.com/MetaMask/metamask-extension/pull/27267)) +- feat: Editing flow ([#26635](https://github.com/MetaMask/metamask-extension/pull/26635)) +- chore: bump profile-sync-controller to 0.9.3 ([#27415](https://github.com/MetaMask/metamask-extension/pull/27415)) +- fix: Remove duplication ([#27421](https://github.com/MetaMask/metamask-extension/pull/27421)) +- fix: Confirm Page test failing in CI/CD ([#27423](https://github.com/MetaMask/metamask-extension/pull/27423)) +- feat: Display approve, increaseAllowance and revoke approval to users… ([#26985](https://github.com/MetaMask/metamask-extension/pull/26985)) +- feat: Add performance metrics for signature requests ([#26967](https://github.com/MetaMask/metamask-extension/pull/26967)) +- fix: Permit DataTree token decimals ([#27328](https://github.com/MetaMask/metamask-extension/pull/27328)) +- fix: alert system and refine SIWE and contract interaction alerts ([#27205](https://github.com/MetaMask/metamask-extension/pull/27205)) +- fix(NOTIFY-1166): rename account sync event names ([#27413](https://github.com/MetaMask/metamask-extension/pull/27413)) +- feat: ERC20 Revoke Allowance ([#26906](https://github.com/MetaMask/metamask-extension/pull/26906)) + ## [12.5.0] ### Added - New UI and functionality for adding and managing networks ([#26433](https://github.com/MetaMask/metamask-extension/pull/26433)), ([#27085](https://github.com/MetaMask/metamask-extension/pull/27085)) @@ -5223,7 +5436,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.5.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.6.0...HEAD +[12.6.0]: https://github.com/MetaMask/metamask-extension/compare/v12.5.0...v12.6.0 [12.5.0]: https://github.com/MetaMask/metamask-extension/compare/v12.4.2...v12.5.0 [12.4.2]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...v12.4.2 [12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1 diff --git a/package.json b/package.json index c3b60bfa1e48..fec8f2ffb498 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.5.0", + "version": "12.6.0", "private": true, "repository": { "type": "git", From b940d04e8c9bef42841aa3816a91d7e8c1a7bea9 Mon Sep 17 00:00:00 2001 From: martahj <marta.hourigan.johnson@gmail.com> Date: Wed, 23 Oct 2024 15:56:53 -0500 Subject: [PATCH 210/226] fix: adjust spacing of quote rate in swaps (#28051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Cherry-pick of https://github.com/MetaMask/metamask-extension/pull/28016 into v12.6 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28016?quickstart=1) ## **Manual testing steps** 1. Start a swap 2. Notice that the quote rate is back on one line and the value is left-aligned ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-11 at 11 32 09 AM](https://github.com/user-attachments/assets/aae5da2f-ae66-46f5-9168-6c6ed496a2a8) ### **After** <img width="359" alt="Screenshot 2024-10-22 at 12 13 50 PM" src="https://github.com/user-attachments/assets/f5974e8a-62a6-464e-a037-b7ce38774f42"> ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/pages/swaps/prepare-swap-page/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/pages/swaps/prepare-swap-page/index.scss b/ui/pages/swaps/prepare-swap-page/index.scss index 60e24c6cdbce..b443006330d3 100644 --- a/ui/pages/swaps/prepare-swap-page/index.scss +++ b/ui/pages/swaps/prepare-swap-page/index.scss @@ -263,7 +263,7 @@ } &__exchange-rate-display { - color: var(--color-text-alternative); + width: auto !important; } } From 77cbfe8a01f00c5b9a95cf58c6fb9494ddd999fa Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 24 Oct 2024 19:00:01 +0100 Subject: [PATCH 211/226] fix: cherry-pick: Gas changes for low Max base fee and Priority fee (#28037) (#28073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick: https://github.com/MetaMask/metamask-extension/pull/28037 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Previously, if the Max base fee and Priority fee were reduced to very low values, the Network fee wouldn't update accordingly. This is a discrepancy with the gas calculations in the old flows. What fixes it is, for low enough values of `maxFeePerGas` (low enough to be lower than `minimumFeePerGas`), the Network fee becomes the Max fee -- `maxFeePerGas` times `gasLimit` directly. Apart from fixing the symptom explained above, this ensures that the Network fee is never higher than the Max fee. The PR also fixes this calculation when it comes to the L2 fees (inside `useTransactionGasFeeEstimate`). It also adds a missing override of `dappSuggestedFees` for both `maxFeePerGas` and `maxPriorityFeePerGas` (inside `useEIP1559TxFees`). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28037?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27802 ## **Manual testing steps** See original report above. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28073?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../confirm/info/hooks/useEIP1559TxFees.ts | 5 ++++- .../confirm/info/hooks/useFeeCalculations.ts | 13 ++++++++++++- .../info/hooks/useTransactionGasFeeEstimate.ts | 12 +++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts b/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts index 40aca7cf2d31..e4bfaad8d779 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts @@ -8,8 +8,11 @@ export const useEIP1559TxFees = ( maxFeePerGas: string; maxPriorityFeePerGas: string; } => { - const hexMaxFeePerGas = transactionMeta?.txParams?.maxFeePerGas; + const hexMaxFeePerGas = + transactionMeta.dappSuggestedGasFees?.maxFeePerGas || + transactionMeta?.txParams?.maxFeePerGas; const hexMaxPriorityFeePerGas = + transactionMeta.dappSuggestedGasFees?.maxPriorityFeePerGas || transactionMeta?.txParams?.maxPriorityFeePerGas; return useMemo(() => { diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts index ceb8a4b2d248..587d70c9c9ef 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts @@ -12,6 +12,7 @@ import { getValueFromWeiHex, multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; import { getConversionRate } from '../../../../../../ducks/metamask/metamask'; import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; @@ -114,11 +115,21 @@ export function useFeeCalculations(transactionMeta: TransactionMeta) { } // Logic for any network without L1 and L2 fee components - const minimumFeePerGas = addHexes( + let minimumFeePerGas = addHexes( decGWEIToHexWEI(estimatedBaseFee) || HEX_ZERO, decimalToHex(maxPriorityFeePerGas), ); + // `minimumFeePerGas` should never be higher than the `maxFeePerGas` + if ( + new Numeric(minimumFeePerGas, 16).greaterThan( + decimalToHex(maxFeePerGas), + 16, + ) + ) { + minimumFeePerGas = decimalToHex(maxFeePerGas); + } + const estimatedFee = multiplyHexes( supportsEIP1559 ? (minimumFeePerGas as Hex) : (gasPrice as Hex), gasLimit as Hex, diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts index 31802eb22feb..f5866a283935 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts @@ -5,6 +5,7 @@ import { addHexes, multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; import { HEX_ZERO } from '../shared/constants'; @@ -28,15 +29,24 @@ export function useTransactionGasFeeEstimate( transactionMeta.dappSuggestedGasFees?.maxPriorityFeePerGas || transactionMeta.txParams?.maxPriorityFeePerGas || HEX_ZERO; + const maxFeePerGas = + transactionMeta.dappSuggestedGasFees?.maxFeePerGas || + transactionMeta.txParams?.maxFeePerGas || + HEX_ZERO; let gasEstimate: Hex; if (supportsEIP1559) { // Minimum Total Fee = (estimatedBaseFee + maxPriorityFeePerGas) * gasLimit - const minimumFeePerGas = addHexes( + let minimumFeePerGas = addHexes( estimatedBaseFee || HEX_ZERO, maxPriorityFeePerGas, ); + // `minimumFeePerGas` should never be higher than the `maxFeePerGas` + if (new Numeric(minimumFeePerGas, 16).greaterThan(maxFeePerGas, 16)) { + minimumFeePerGas = maxFeePerGas; + } + gasEstimate = multiplyHexes(minimumFeePerGas as Hex, gasLimit as Hex); } else { gasEstimate = multiplyHexes(gasPrice as Hex, gasLimit as Hex); From d1da8609213d4ca682a2e972b5c3db15d0abf256 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 24 Oct 2024 19:01:35 +0100 Subject: [PATCH 212/226] fix: Cherry-pick Support dynamic native token name on gas component (#28048) (#28071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick https://github.com/MetaMask/metamask-extension/pull/28048 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Uses the multinetwork ticker. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28048?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28001 ## **Manual testing steps** See original ticket linked above. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="472" alt="Screenshot 2024-10-23 at 16 16 19" src="https://github.com/user-attachments/assets/636f475e-808f-42d1-8651-d71bdb51b145"> ## **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. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28071?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../transactions/contract-deployment.test.tsx | 4 ++-- .../transactions/contract-interaction.test.tsx | 4 ++-- .../confirm/info/hooks/useFeeCalculations.test.ts | 4 ++-- .../confirm/info/hooks/useFeeCalculations.ts | 12 ++++++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx index ecef04f30861..c2625e06e3e7 100644 --- a/test/integration/confirmations/transactions/contract-deployment.test.tsx +++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx @@ -283,7 +283,7 @@ describe('Contract Deployment Confirmation', () => { expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); - expect(firstGasField).toHaveTextContent('0.0001 ETH'); + expect(firstGasField).toHaveTextContent('0.0001 SepoliaETH'); const editGasFeeNativeCurrency = within(editGasFeesRow).getByTestId('native-currency'); expect(editGasFeeNativeCurrency).toHaveTextContent('$0.47'); @@ -371,7 +371,7 @@ describe('Contract Deployment Confirmation', () => { const maxFee = screen.getByTestId('gas-fee-details-max-fee'); expect(gasFeesSection).toContainElement(maxFee); expect(maxFee).toHaveTextContent(tEn('maxFee') as string); - expect(maxFee).toHaveTextContent('0.0023 ETH'); + expect(maxFee).toHaveTextContent('0.0023 SepoliaETH'); expect(maxFee).toHaveTextContent('$7.72'); const nonceSection = screen.getByTestId('advanced-details-nonce-section'); diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx index 1102cb21c67d..b77e48f1d660 100644 --- a/test/integration/confirmations/transactions/contract-interaction.test.tsx +++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx @@ -301,7 +301,7 @@ describe('Contract Interaction Confirmation', () => { expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); - expect(firstGasField).toHaveTextContent('0.0001 ETH'); + expect(firstGasField).toHaveTextContent('0.0001 SepoliaETH'); const editGasFeeNativeCurrency = within(editGasFeesRow).getByTestId('native-currency'); expect(editGasFeeNativeCurrency).toHaveTextContent('$0.47'); @@ -402,7 +402,7 @@ describe('Contract Interaction Confirmation', () => { const maxFee = screen.getByTestId('gas-fee-details-max-fee'); expect(gasFeesSection).toContainElement(maxFee); expect(maxFee).toHaveTextContent(tEn('maxFee') as string); - expect(maxFee).toHaveTextContent('0.0023 ETH'); + expect(maxFee).toHaveTextContent('0.0023 SepoliaETH'); expect(maxFee).toHaveTextContent('$7.72'); const nonceSection = screen.getByTestId('advanced-details-nonce-section'); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts index 911cdb20118c..17c8ab8dd8f6 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts @@ -22,13 +22,13 @@ describe('useFeeCalculations', () => { expect(result.current).toMatchInlineSnapshot(` { "estimatedFeeFiat": "$0.00", - "estimatedFeeNative": "0 WEI", + "estimatedFeeNative": "0 ETH", "l1FeeFiat": "", "l1FeeNative": "", "l2FeeFiat": "", "l2FeeNative": "", "maxFeeFiat": "$0.00", - "maxFeeNative": "0 WEI", + "maxFeeNative": "0 ETH", } `); }); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts index 587d70c9c9ef..70bd2c0e3af2 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts @@ -8,7 +8,6 @@ import { addHexes, decGWEIToHexWEI, decimalToHex, - getEthConversionFromWeiHex, getValueFromWeiHex, multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; @@ -17,6 +16,7 @@ import { getConversionRate } from '../../../../../../ducks/metamask/metamask'; import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; import { getCurrentCurrency } from '../../../../../../selectors'; +import { getMultichainNetwork } from '../../../../../../selectors/multichain'; import { HEX_ZERO } from '../shared/constants'; import { useEIP1559TxFees } from './useEIP1559TxFees'; import { useSupportsEIP1559 } from './useSupportsEIP1559'; @@ -33,14 +33,18 @@ export function useFeeCalculations(transactionMeta: TransactionMeta) { const conversionRate = useSelector(getConversionRate); const fiatFormatter = useFiatFormatter(); + const multichainNetwork = useSelector(getMultichainNetwork); + const ticker = multichainNetwork?.network?.ticker; + const getFeesFromHex = useCallback( (hexFee: string) => { - const nativeCurrencyFee = - getEthConversionFromWeiHex({ + const nativeCurrencyFee = `${ + getValueFromWeiHex({ value: hexFee, fromCurrency: EtherDenomination.GWEI, numberOfDecimals: 4, - }) || `0 ${EtherDenomination.ETH}`; + }) || 0 + } ${ticker}`; const currentCurrencyFee = fiatFormatter( Number( From a0c0e9165c7b960999d0f0d40c02774028054d1a Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 24 Oct 2024 19:03:26 +0100 Subject: [PATCH 213/226] fix: cherry-pick: Fall back to token list for the token symbol (#28003) (#28078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick: https://github.com/MetaMask/metamask-extension/pull/28003 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28003?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27970 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="472" alt="Screenshot 2024-10-22 at 11 19 10" src="https://github.com/user-attachments/assets/c7a09d9f-c5de-44c7-b3bb-c2e759e2b8c8"> ## **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. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28078?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- ...-image.test.ts => useTokenDetails.test.ts} | 32 +++++++++++++------ ...{use-token-image.ts => useTokenDetails.ts} | 13 ++++++-- .../info/shared/send-heading/send-heading.tsx | 13 ++++---- 3 files changed, 38 insertions(+), 20 deletions(-) rename ui/pages/confirmations/components/confirm/info/hooks/{use-token-image.test.ts => useTokenDetails.test.ts} (73%) rename ui/pages/confirmations/components/confirm/info/hooks/{use-token-image.ts => useTokenDetails.ts} (65%) diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts similarity index 73% rename from ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts rename to ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts index 23e4cc3c1bda..efdf2b66ac56 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts @@ -2,9 +2,9 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; import mockState from '../../../../../../../test/data/mock-state.json'; import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; -import { useTokenImage } from './use-token-image'; +import { useTokenDetails } from './useTokenDetails'; -describe('useTokenImage', () => { +describe('useTokenDetails', () => { it('returns iconUrl from selected token if it exists', () => { const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, @@ -19,11 +19,14 @@ describe('useTokenImage', () => { }; const { result } = renderHookWithProvider( - () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), mockState, ); - expect(result.current).toEqual({ tokenImage: 'iconUrl' }); + expect(result.current).toEqual({ + tokenImage: 'iconUrl', + tokenSymbol: 'symbol', + }); }); it('returns selected token image if no iconUrl is included', () => { @@ -39,11 +42,14 @@ describe('useTokenImage', () => { }; const { result } = renderHookWithProvider( - () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), mockState, ); - expect(result.current).toEqual({ tokenImage: 'image' }); + expect(result.current).toEqual({ + tokenImage: 'image', + tokenSymbol: 'symbol', + }); }); it('returns token list icon url if no image is included in the token', () => { @@ -58,7 +64,7 @@ describe('useTokenImage', () => { }; const { result } = renderHookWithProvider( - () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), { ...mockState, metamask: { @@ -72,7 +78,10 @@ describe('useTokenImage', () => { }, ); - expect(result.current).toEqual({ tokenImage: 'tokenListIconUrl' }); + expect(result.current).toEqual({ + tokenImage: 'tokenListIconUrl', + tokenSymbol: 'symbol', + }); }); it('returns undefined if no image is found', () => { @@ -87,10 +96,13 @@ describe('useTokenImage', () => { }; const { result } = renderHookWithProvider( - () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), mockState, ); - expect(result.current).toEqual({ tokenImage: undefined }); + expect(result.current).toEqual({ + tokenImage: undefined, + tokenSymbol: 'symbol', + }); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts similarity index 65% rename from ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts rename to ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts index 5817d08028ab..be9578496205 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts @@ -1,20 +1,27 @@ import { TokenListMap } from '@metamask/assets-controllers'; import { TransactionMeta } from '@metamask/transaction-controller'; import { useSelector } from 'react-redux'; +import { useI18nContext } from '../../../../../../hooks/useI18nContext'; import { getTokenList } from '../../../../../../selectors'; import { SelectedToken } from '../shared/selected-token'; -export const useTokenImage = ( +export const useTokenDetails = ( transactionMeta: TransactionMeta, selectedToken: SelectedToken, ) => { + const t = useI18nContext(); + const tokenList = useSelector(getTokenList) as TokenListMap; - // TODO: Add support for NFT images in one of the following tasks const tokenImage = selectedToken?.iconUrl || selectedToken?.image || tokenList[transactionMeta?.txParams?.to as string]?.iconUrl; - return { tokenImage }; + const tokenSymbol = + selectedToken?.symbol || + tokenList[transactionMeta?.txParams?.to as string]?.symbol || + t('unknown'); + + return { tokenImage, tokenSymbol }; }; 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 2806c33936c0..40c571d4bc75 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 @@ -16,22 +16,23 @@ import { TextColor, TextVariant, } from '../../../../../../../helpers/constants/design-system'; -import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; import { getWatchedToken } from '../../../../../../../selectors'; import { MultichainState } from '../../../../../../../selectors/multichain'; import { useConfirmContext } from '../../../../../context/confirm'; -import { useTokenImage } from '../../hooks/use-token-image'; +import { useTokenDetails } from '../../hooks/useTokenDetails'; import { useTokenValues } from '../../hooks/use-token-values'; import { ConfirmLoader } from '../confirm-loader/confirm-loader'; const SendHeading = () => { - const t = useI18nContext(); const { currentConfirmation: transactionMeta } = useConfirmContext<TransactionMeta>(); const selectedToken = useSelector((state: MultichainState) => getWatchedToken(transactionMeta)(state), ); - const { tokenImage } = useTokenImage(transactionMeta, selectedToken); + const { tokenImage, tokenSymbol } = useTokenDetails( + transactionMeta, + selectedToken, + ); const { decodedTransferValue, fiatDisplayValue, pending } = useTokenValues(transactionMeta); @@ -57,9 +58,7 @@ const SendHeading = () => { variant={TextVariant.headingLg} color={TextColor.inherit} marginTop={3} - >{`${decodedTransferValue || ''} ${ - selectedToken?.symbol || t('unknown') - }`}</Text> + >{`${decodedTransferValue || ''} ${tokenSymbol}`}</Text> {fiatDisplayValue && ( <Text variant={TextVariant.bodyMd} color={TextColor.textAlternative}> {fiatDisplayValue} From 157b377e98630fd9ab041cb77d26adee5a4aec73 Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:10:17 -0400 Subject: [PATCH 214/226] fix: Cherry-pick: Fix c2 detection bypass by supporting all network requests types (#28087) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Cherry pick: #28057 This update addresses a bypass that allowed scammers to bypass C2 detection by using alternative network request types to communicate with their Command and Control (C2) servers. Previously, we only listened for a limited set of request types (e.g., main_frame, sub_frame, xmlhttprequest), which left the system exposed to other methods of calling C2s. With this fix, we now listen to all network request types and cross-check them against our client-side blocklist, ensuring better coverage and preventing these types of bypasses. Changes: Updated maybeDetectPhishing in background.js to listen for all network requests by removing restrictions on request types. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28057?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to a website known to be on the C2 domain blocklist. For now we made our test website https://develop.d3bkcslj57l47p.amplifyapp.com/ have a malicious C2 Request that is on our blocklist. 2. Attempt to interact with the site. 3. Verify that on visiting the website you get redirected to the Metamask phishing page. 4. Repeat with a site that is not on the blocklist to confirm normal operation. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> --- app/scripts/background.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 9f203b35661d..ad6e3b6f22c2 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -324,7 +324,6 @@ function maybeDetectPhishing(theController) { return {}; }, { - types: ['main_frame', 'sub_frame', 'xmlhttprequest'], urls: ['http://*/*', 'https://*/*'], }, isManifestV2 ? ['blocking'] : [], From f37388c180158e5af057f2d8b050cde79885ef13 Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:47:28 -0400 Subject: [PATCH 215/226] feat: Cherry-pick: Please view the attached issue (#28133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** *Please view the attached issue within MetaMask planning for details regarding this PR* <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28133?quickstart=1) ## **Related issues** Fixes: *Please view the attached issue within MetaMask planning for details regarding this PR* ## **Manual testing steps** 1. Go to a website known to be on the C2 domain blocklist. For now we made our test website https://develop.d3bkcslj57l47p.amplifyapp.com/ have a malicious C2 Request that is on our blocklist. 2. Attempt to interact with the site. 3. Verify that on visiting the website you get redirected to the Metamask phishing page. 4. Repeat with a site that is not on the blocklist to confirm normal operation. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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. --- lavamoat/browserify/beta/policy.json | 1 + lavamoat/browserify/flask/policy.json | 1 + lavamoat/browserify/main/policy.json | 1 + lavamoat/browserify/mmi/policy.json | 1 + package.json | 2 +- yarn.lock | 23 +++++++++++------------ 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 9cbdda6ac03e..b9b6062a30d7 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2044,6 +2044,7 @@ "globals": { "TextEncoder": true, "URL": true, + "console.error": true, "fetch": true }, "packages": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 9cbdda6ac03e..b9b6062a30d7 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2044,6 +2044,7 @@ "globals": { "TextEncoder": true, "URL": true, + "console.error": true, "fetch": true }, "packages": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 9cbdda6ac03e..b9b6062a30d7 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2044,6 +2044,7 @@ "globals": { "TextEncoder": true, "URL": true, + "console.error": true, "fetch": true }, "packages": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index fae253f8b9d5..073882b78df5 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2136,6 +2136,7 @@ "globals": { "TextEncoder": true, "URL": true, + "console.error": true, "fetch": true }, "packages": { diff --git a/package.json b/package.json index fec8f2ffb498..767646ae91ed 100644 --- a/package.json +++ b/package.json @@ -339,7 +339,7 @@ "@metamask/obs-store": "^9.0.0", "@metamask/permission-controller": "^10.0.0", "@metamask/permission-log-controller": "^2.0.1", - "@metamask/phishing-controller": "^12.0.1", + "@metamask/phishing-controller": "^12.3.0", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.35.1", "@metamask/preinstalled-example-snap": "^0.2.0", diff --git a/yarn.lock b/yarn.lock index af059f8960e9..54b160bc821e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5015,9 +5015,9 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0": - version: 11.3.0 - resolution: "@metamask/controller-utils@npm:11.3.0" +"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0": + version: 11.4.0 + resolution: "@metamask/controller-utils@npm:11.4.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/eth-query": "npm:^4.0.0" @@ -5028,7 +5028,7 @@ __metadata: bn.js: "npm:^5.2.1" eth-ens-namehash: "npm:^2.0.8" fast-deep-equal: "npm:^3.1.3" - checksum: 10/3200228d1f4ea5fa095228db4e5050529caf0470e072382eb8f7571bb9b07515516ca9e846b7751388399d9ae967e4985dafd6120902ef6c998e98f4eb36d964 + checksum: 10/f34d24880eab264bddaa5bef21afaecb206db6978364565d0f7b7a54b1d411f129eb84175041df3be8a66394c2d49e83b6648b5cbde6f34662a60fc553c31458 languageName: node linkType: hard @@ -5962,19 +5962,18 @@ __metadata: languageName: node linkType: hard -"@metamask/phishing-controller@npm:^12.0.1, @metamask/phishing-controller@npm:^12.0.2": - version: 12.0.2 - resolution: "@metamask/phishing-controller@npm:12.0.2" +"@metamask/phishing-controller@npm:^12.0.2, @metamask/phishing-controller@npm:^12.3.0": + version: 12.3.0 + resolution: "@metamask/phishing-controller@npm:12.3.0" dependencies: - "@metamask/base-controller": "npm:^7.0.0" - "@metamask/controller-utils": "npm:^11.2.0" + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.4.0" "@noble/hashes": "npm:^1.4.0" "@types/punycode": "npm:^2.1.0" - eth-phishing-detect: "npm:^1.2.0" ethereum-cryptography: "npm:^2.1.2" fastest-levenshtein: "npm:^1.0.16" punycode: "npm:^2.1.1" - checksum: 10/78781e1b781c838e303677157616fb3b5e581030fe8f0ed8913f6b75fbcb7ee2ba59a44831936cc68cca8b295ef6546761b40ea3277d810b68d8ed39a58d0e29 + checksum: 10/15e64adff57996486c36d0c73747a76543e8f7ad79020fc2746726f81f3858251b2e256c04e8d9caf1daf71c41f7ddf575c901d2a46174a5884d2836c60a3b2d languageName: node linkType: hard @@ -26143,7 +26142,7 @@ __metadata: "@metamask/obs-store": "npm:^9.0.0" "@metamask/permission-controller": "npm:^10.0.0" "@metamask/permission-log-controller": "npm:^2.0.1" - "@metamask/phishing-controller": "npm:^12.0.1" + "@metamask/phishing-controller": "npm:^12.3.0" "@metamask/phishing-warning": "npm:^4.0.0" "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.35.1" From 8a695e4f7c4ebedb560cdf2a73cf8e1509dfc04a Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Wed, 30 Oct 2024 14:04:16 +0100 Subject: [PATCH 216/226] chore (cherry-pick): ignore warning for ethereumjs-wallet (#28145) (#28162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry picks https://github.com/MetaMask/metamask-extension/pull/28145 ## **Description** Silent deprecation audit warning for `ethereumjs-wallet`. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28162?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** N/A ## **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: sahar-fehri <sahar.fehri@consensys.net> --- .yarnrc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index f4d8fc7fa471..cc0c959e2722 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -117,7 +117,8 @@ npmAuditIgnoreAdvisories: # Currently in use for the network list drag and drop functionality. # Maintenance has stopped and the project will be archived in 2025. - 'react-beautiful-dnd (deprecation)' - + # New package name format for new versions: @ethereumjs/wallet. + - 'ethereumjs-wallet (deprecation)' npmRegistries: 'https://npm.pkg.github.com': npmAlwaysAuth: true From 43e43b360e65dfe26244ff0070a039f15d3dbfa9 Mon Sep 17 00:00:00 2001 From: Charly Chevalier <charly.chevalier@consensys.net> Date: Wed, 30 Oct 2024 21:18:00 +0100 Subject: [PATCH 217/226] chore(cherry-pick): update @metamask/bitcoin-wallet-snap to 0.8.2 (#28135) (#28140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We did update the permission for the Bitcoin Snap in the 0.8.2. We'd like to have this in the upcoming release as discussed internally. > [!IMPORTANT] > This update does not invalidate anything regarding the testing that has been done for Bitcoin support. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28140?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. `yarn start:flask` 2. Enable Bitcoin support 3. Create your Bitcoin accounts (mainnet + testnet) 4. Interact with your accounts: - Check the balance - Initiate a send flow ## **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 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 767646ae91ed..d4517b42229d 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.8.1", + "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", diff --git a/yarn.lock b/yarn.lock index 54b160bc821e..3558c24d8d5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4982,10 +4982,10 @@ __metadata: languageName: node linkType: hard -"@metamask/bitcoin-wallet-snap@npm:^0.8.1": - version: 0.8.1 - resolution: "@metamask/bitcoin-wallet-snap@npm:0.8.1" - checksum: 10/0fff706a98c6f798ae0ae78bf9a8913c0b056b18aff64f994e521c5005ab7e326fafe1d383b2b7c248456948eaa263df3b31a081d620d82ed7c266857c94a955 +"@metamask/bitcoin-wallet-snap@npm:^0.8.2": + version: 0.8.2 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.8.2" + checksum: 10/42da719ae59b12d7150e513f082351dab8f901587ca12897b43c0b5d9123bbf066a2666c48b81b25e594f97ef237e1d1d7e9ccea8bd9bfb54910c5cd8d43b420 languageName: node linkType: hard @@ -26097,7 +26097,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.8.1" + "@metamask/bitcoin-wallet-snap": "npm:^0.8.2" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" From ac1173b9c2fde2980aa3dff8910253627417bc5f Mon Sep 17 00:00:00 2001 From: Nidhi Kumari <nidhi.kumari@consensys.net> Date: Thu, 31 Oct 2024 02:37:02 +0530 Subject: [PATCH 218/226] =?UTF-8?q?feat=20(cherry-pick):=20added=20test=20?= =?UTF-8?q?network=20as=20selected=20network=20if=20globally=20selected=20?= =?UTF-8?q?for=E2=80=A6=20(#28139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … connection Request (#27980) This PR is to select a test network in the default selected networks list if its the globally selected network at the time of connection request. ## **Related issues** Fixes: [#27891](https://github.com/MetaMask/metamask-extension/issues/27891) ## **Manual testing steps** 1. Run extension with yarn start 2. Switch to Sepolia 3. Go to test-dapp, click on connect. 4. In the connections page, check Sepolia is the selected along with non test networks 5. Click confirm, dapp should be connected to MM and user should be on Sepolia network in MM. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/127fc7bb-2e68-411a-b407-7f6d5158e911 ### **After** https://github.com/user-attachments/assets/dd0b5aa6-404a-421f-93a4-67cab43d60c6 ## **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. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28139?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../api-specs/ConfirmationRejectionRule.ts | 14 ---- test/e2e/helpers.js | 10 --- test/e2e/json-rpc/switchEthereumChain.spec.js | 65 ++++++++++++++++--- test/e2e/page-objects/pages/test-dapp.ts | 9 --- .../e2e/snaps/test-snap-txinsights-v2.spec.js | 5 ++ .../connections/edit-networks-flow.spec.js | 8 --- .../dapp1-switch-dapp2-send.spec.js | 32 ++++++--- ...multi-dapp-sendTx-revokePermission.spec.js | 4 +- .../switchChain-watchAsset.spec.js | 14 +++- .../connect-page/connect-page.tsx | 17 ++++- 10 files changed, 114 insertions(+), 64 deletions(-) diff --git a/test/e2e/api-specs/ConfirmationRejectionRule.ts b/test/e2e/api-specs/ConfirmationRejectionRule.ts index 3e37dcd07fd7..43046d8b0943 100644 --- a/test/e2e/api-specs/ConfirmationRejectionRule.ts +++ b/test/e2e/api-specs/ConfirmationRejectionRule.ts @@ -73,20 +73,6 @@ export class ConfirmationsRejectRule implements Rule { tag: 'button', }); - const editButtons = await this.driver.findElements( - '[data-testid="edit"]', - ); - await editButtons[1].click(); - - await this.driver.clickElement({ - text: 'Localhost 8545', - tag: 'p', - }); - - await this.driver.clickElement( - '[data-testid="connect-more-chains-button"]', - ); - const screenshotTwo = await this.driver.driver.takeScreenshot(); call.attachments.push({ type: 'image', diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index c857838f0810..6d2ccebeb7c7 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -783,16 +783,6 @@ const connectToDapp = async (driver) => { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const editButtons = await driver.findElements('[data-testid="edit"]'); - await editButtons[1].click(); - - await driver.clickElement({ - text: 'Localhost 8545', - tag: 'p', - }); - - await driver.clickElement('[data-testid="connect-more-chains-button"]'); - await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', diff --git a/test/e2e/json-rpc/switchEthereumChain.spec.js b/test/e2e/json-rpc/switchEthereumChain.spec.js index fba06db48131..60ba4eb9aacb 100644 --- a/test/e2e/json-rpc/switchEthereumChain.spec.js +++ b/test/e2e/json-rpc/switchEthereumChain.spec.js @@ -157,8 +157,34 @@ describe('Switch Ethereum Chain for two dapps', function () { tag: 'button', }); - await driver.switchToWindowWithUrl(DAPP_ONE_URL); + // Switch to Dapp One and connect it + await driver.switchToWindowWithUrl(DAPP_URL); + await driver.findClickableElement({ + text: 'Connect', + tag: 'button', + }); + await driver.clickElement('#connectButton'); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + await editButtons[1].click(); + + // Disconnect Localhost 8545 + await driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + // Switch to Dapp Two + await driver.switchToWindowWithUrl(DAPP_ONE_URL); // Initiate send transaction on Dapp two await driver.clickElement('#sendButton'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); @@ -181,8 +207,6 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); - - // Switch to tx and confirm send tx. await switchToNotificationWindow(driver, 4); await driver.findClickableElements({ text: 'Confirm', @@ -192,7 +216,6 @@ describe('Switch Ethereum Chain for two dapps', function () { text: 'Confirm', tag: 'button', }); - // Delay here after notification for second notification popup for switchEthereumChain await driver.delay(1000); @@ -203,7 +226,12 @@ describe('Switch Ethereum Chain for two dapps', function () { text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithUrl(DAPP_URL); + await driver.findElement({ css: '#chainId', text: '0x539' }); }, ); }); @@ -273,7 +301,18 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.clickElement('#connectButton'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + // Click the edit button for networks + await editButtons[1].click(); + + // Disconnect Mainnet + await driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + await driver.clickElement('[data-testid="connect-more-chains-button"]'); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', @@ -293,14 +332,11 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); - - // Switch to notification of switchEthereumChain await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElements({ text: 'Confirm', tag: 'button', }); - // Switch back to dapp one await driver.switchToWindow(dappOne); assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); @@ -397,11 +433,22 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + // Click the edit button for networks + await editButtons[1].click(); + + // Disconnect Mainnet + await driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', }); - await driver.switchToWindow(dappTwo); assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 4a02d80459e0..b9487ee599b9 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -209,15 +209,6 @@ class TestDapp { 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, ); diff --git a/test/e2e/snaps/test-snap-txinsights-v2.spec.js b/test/e2e/snaps/test-snap-txinsights-v2.spec.js index 5fb56687de96..830629d1c43e 100644 --- a/test/e2e/snaps/test-snap-txinsights-v2.spec.js +++ b/test/e2e/snaps/test-snap-txinsights-v2.spec.js @@ -127,6 +127,11 @@ describe('Test Snap TxInsights-v2', function () { tag: 'button', text: 'Activity', }); + // wait for transaction confirmation + await driver.waitForSelector({ + css: '.transaction-status-label', + text: 'Confirmed', + }); }, ); }); diff --git a/test/e2e/tests/connections/edit-networks-flow.spec.js b/test/e2e/tests/connections/edit-networks-flow.spec.js index e14e1ae325d5..1db224f0ac0a 100644 --- a/test/e2e/tests/connections/edit-networks-flow.spec.js +++ b/test/e2e/tests/connections/edit-networks-flow.spec.js @@ -9,11 +9,6 @@ const { } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); -async function switchToNetworkByName(driver, networkName) { - await driver.clickElement('.mm-picker-network'); - await driver.clickElement(`[data-testid="${networkName}"]`); -} - describe('Edit Networks Flow', function () { it('should be able to edit networks', async function () { await withFixtures( @@ -43,9 +38,6 @@ describe('Edit Networks Flow', function () { await driver.clickElement( '.mm-modal-content__dialog button[aria-label="Close"]', ); - - // Switch to first network, whose send transaction was just confirmed - await switchToNetworkByName(driver, 'Localhost 8545'); await locateAccountBalanceDOM(driver); await driver.clickElement( '[data-testid ="account-options-menu-button"]', diff --git a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js index c330596c48f3..c98e0eb229c6 100644 --- a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js @@ -51,6 +51,17 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + await editButtons[1].click(); + + // Disconnect Localhost 8545 + await driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', @@ -93,7 +104,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { params: [{ chainId: '0x539' }], }); - // Initiate switchEthereumChain on Dapp Two + // Initiate switchEthereumChain on Dapp One await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); @@ -192,7 +203,17 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.clickElement('#connectButton'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + await editButtons[1].click(); + + // Disconnect Localhost 8545 + await driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + await driver.clickElement('[data-testid="connect-more-chains-button"]'); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', @@ -235,17 +256,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { params: [{ chainId: '0x539' }], }); - // Initiate switchEthereumChain on Dapp Two + // Initiate switchEthereumChain on Dapp One await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement({ - text: 'Use your enabled networks', - tag: 'p', - }); - await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.clickElement('#sendButton'); @@ -259,6 +274,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { // There is an extra window appearing and disappearing // so we leave this delay until the issue is fixed (#27360) await driver.delay(5000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the send confirmation. diff --git a/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js b/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js index d32e96e29571..06d232635131 100644 --- a/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js +++ b/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js @@ -88,7 +88,7 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok await driver.switchToWindowWithUrl(DAPP_URL); await driver.findElement({ css: '[id="chainId"]', - text: '0x1', + text: '0x539', }); await driver.clickElement('#sendButton'); @@ -108,7 +108,7 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok await driver.switchToWindowWithUrl(DAPP_URL); await driver.findElement({ css: '[id="chainId"]', - text: '0x1', + text: '0x539', }); await driver.assertElementNotPresent({ css: '[id="chainId"]', diff --git a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js index 308a9c36914b..5767bd26def5 100644 --- a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js @@ -3,9 +3,9 @@ const { defaultGanacheOptions, logInWithBalanceValidation, openDapp, - switchToNotificationWindow, WINDOW_TITLES, withFixtures, + switchToNotificationWindow, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); const { DAPP_URL } = require('../../constants'); @@ -48,7 +48,17 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { await driver.clickElement('#connectButton'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + await editButtons[1].click(); + // Disconnect Localhost 8545. By Default, this was the globally selected network + await driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', @@ -72,7 +82,6 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { text: 'Use your enabled networks', tag: 'p', }); - // Switch back to test dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -81,7 +90,6 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { text: 'Add Token(s) to Wallet', tag: 'button', }); - await switchToNotificationWindow(driver); // Confirm Switch Network diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index 0ae22b3d9e0f..e002e54ef34e 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -39,6 +39,7 @@ import { EndowmentTypes, RestrictedMethods, } from '../../../../shared/constants/permissions'; +import { getMultichainNetwork } from '../../../selectors/multichain'; export type ConnectPageRequest = { id: string; @@ -92,10 +93,24 @@ export const ConnectPage: React.FC<ConnectPageProps> = ({ ), [networkConfigurations], ); + + // By default, if a non test network is the globally selected network. We will only show non test networks as default selected. + const currentlySelectedNetwork = useSelector(getMultichainNetwork); + const currentlySelectedNetworkChainId = + currentlySelectedNetwork.network.chainId; + // If globally selected network is a test network, include that in the default selcted networks for connection request + const selectedTestNetwork = testNetworks.find( + (network: { chainId: string }) => + network.chainId === currentlySelectedNetworkChainId, + ); + + const selectedNetworksList = selectedTestNetwork + ? [...nonTestNetworks, selectedTestNetwork] + : nonTestNetworks; const defaultSelectedChainIds = requestedChainIds.length > 0 ? requestedChainIds - : nonTestNetworks.map(({ chainId }) => chainId); + : selectedNetworksList.map(({ chainId }) => chainId); const [selectedChainIds, setSelectedChainIds] = useState( defaultSelectedChainIds, ); From 334c63c8b5db83c13ed013fd83180e242f8709fe Mon Sep 17 00:00:00 2001 From: Mark Stacey <markjstacey@gmail.com> Date: Wed, 30 Oct 2024 19:39:02 -0230 Subject: [PATCH 219/226] [cherry pick] Fix bugs related to queued requests (#28197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a cherry-pick of #28090 for v12.6.0. Original description: <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Bumps `@metamask/queued-request-controller` to fix queueing issue with Chain Permission `wallet_switchEthereumChain` and `wallet_addEthereumChain` when switching to a previously permitted chain and with `wallet_addEthereumChain` not being enqueued when it still should be. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28090?quickstart=1) ## **Related issues** Related: https://github.com/MetaMask/core/pull/4846 Fixes: https://github.com/MetaMask/metamask-extension/issues/28101 Fixes: https://github.com/MetaMask/metamask-extension/issues/27977 Fixes: #28102 ## **Manual testing steps** The easiest way to test this would be a combination of using the test dapp and the following request to switch chains ``` await window.ethereum.request({ "method": "wallet_switchEthereumChain", "params": [ { chainId: "0x1" } ], }); ``` The behaviors you should see include: **One dapp:** * On a dapp permissioned for chain A and B, on chain A, queue up several send transactions, then use wallet_switchEthereumChain to switch to chain B. The send transactions should NOT get cleared immediately after requesting the chain switch. Chain switch should NOT happen until the previous approvals are approved/rejected. * On a dapp permissioned for chain A and B, on chain A, queue up one send transaction, then use wallet_switchEthereumChain to switch to chain B, then queue up several more send transactions. Reject/approve the first transaction. Afterwards, you should see chain B as the active chain for the dapp, and all subsequent approvals cleared/rejected automatically. * On a dapp permissioned for ONLY chain A, on chain A, queue up one send transaction, then use wallet_switchEthereumChain to switch to chain B, then queue up several more send transactions. Reject/approve the first transaction. Afterwards, you should an approval prompt for adding chain B. If you approve it, the dapp should then be on chain B, with all subsequent approvals cleared/rejected. If you disapprove it, you should be prompted with the subsequent approvals. * On a dapp permissioned for ONLY chain A, on chain A, wallet_switchEthereumChain to switch to chain B, then queue up several more send transactions. Reject/approve the first transaction. Afterwards, you should an approval prompt for adding chain B. If you approve it, the dapp should then be on chain B, with all subsequent approvals cleared/rejected. If you disapprove it, you should be prompted with the subsequent approvals. **Two dapps:** * On a dapp permissioned for chain A, on chain A, queue up several send transactions, On a separate dapp permissioned for chain A and B, on chain A, use wallet_switchEthereumChain to switch to chain B. The send transactions should NOT get cleared immediately after requesting the chain switch. Chain switch should NOT happen until the previous approvals are approved/rejected. * On a dapp permissioned for chain A and B, on chain A, queue up one send transaction. On a separate dapp permissioned for chain A and B, on chain A, use wallet_switchEthereumChain to switch to chain B. Then on the first dapp queue up several more send transactions. Reject/approve the first transaction. Afterwards, you should see chain B as the active chain for the second dapp, and then you should still be prompted with the subsequent approvals for the first dapp. * One one dapp, start a wallet_addEthereumChain for a chain that does not exist in the wallet and leave the approval alone. On a different dapp, do the same thing. Only the request from the first dapp should be accessible (i.e. no scrubbing between both of them). After rejecting the first request, the second request should then appear (which will look exactly the same of course). Wallet should not lock up if you repeat this and accept either of the requests ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** https://github.com/user-attachments/assets/2634119f-67db-4866-8520-9320a9400b1d https://github.com/user-attachments/assets/c78c13ab-ea4f-4420-bccc-70959786e8db ## **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: jiexi <jiexiluan@gmail.com> Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com> --- ...ToNonEvmAccountReqFilterMiddleware.test.ts | 7 +- app/scripts/metamask-controller.js | 46 ++++--- lavamoat/browserify/beta/policy.json | 112 +++++++++--------- lavamoat/browserify/flask/policy.json | 112 +++++++++--------- lavamoat/browserify/main/policy.json | 112 +++++++++--------- lavamoat/browserify/mmi/policy.json | 112 +++++++++--------- package.json | 2 +- shared/constants/methods-tags.ts | 19 +++ yarn.lock | 84 +++++++------ 9 files changed, 314 insertions(+), 292 deletions(-) diff --git a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts index 063271a9984a..09893ea05a5e 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts @@ -1,12 +1,11 @@ import { jsonrpc2, Json } from '@metamask/utils'; import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; -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: Record<string, Json>) => ({ jsonrpc: jsonrpc2, id: 1, method, @@ -286,7 +285,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { }: { accountType: EthAccountType | BtcAccountType; method: string; - params: Json; + params: Record<string, Json>; calledNext: number; }) => { const filterFn = createEvmMethodsToNonEvmAccountReqFilterMiddleware({ @@ -298,7 +297,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { const mockEnd = jest.fn(); filterFn( - getMockRequest(method, params) as JsonRpcRequest<JsonRpcParams>, + getMockRequest(method, params), getMockResponse(), mockNext, mockEnd, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c33485f665b7..4284a2614a9d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -157,7 +157,11 @@ import { NotificationServicesController, } from '@metamask/notification-services-controller'; import { isProduction } from '../../shared/modules/environment'; -import { methodsRequiringNetworkSwitch } from '../../shared/constants/methods-tags'; +import { + methodsRequiringNetworkSwitch, + methodsThatCanSwitchNetworkWithoutApproval, + methodsThatShouldBeEnqueued, +} from '../../shared/constants/methods-tags'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; @@ -486,22 +490,6 @@ export default class MetamaskController extends EventEmitter { this.approvalController.clear(providerErrors.userRejectedRequest()); }; - this.queuedRequestController = new QueuedRequestController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'QueuedRequestController', - allowedActions: [ - 'NetworkController:getState', - 'NetworkController:setActiveNetwork', - 'SelectedNetworkController:getNetworkClientIdForDomain', - ], - allowedEvents: ['SelectedNetworkController:stateChange'], - }), - shouldRequestSwitchNetwork: ({ method }) => - methodsRequiringNetworkSwitch.includes(method), - clearPendingConfirmations, - showApprovalRequest: opts.showUserConfirmation, - }); - this.approvalController = new ApprovalController({ messenger: this.controllerMessenger.getRestricted({ name: 'ApprovalController', @@ -517,6 +505,28 @@ export default class MetamaskController extends EventEmitter { ], }); + this.queuedRequestController = new QueuedRequestController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'QueuedRequestController', + allowedActions: [ + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', + 'SelectedNetworkController:getNetworkClientIdForDomain', + ], + allowedEvents: ['SelectedNetworkController:stateChange'], + }), + shouldRequestSwitchNetwork: ({ method }) => + methodsRequiringNetworkSwitch.includes(method), + canRequestSwitchNetworkWithoutApproval: ({ method }) => + methodsThatCanSwitchNetworkWithoutApproval.includes(method), + clearPendingConfirmations, + showApprovalRequest: () => { + if (this.approvalController.getTotalApprovalCount() > 0) { + opts.showUserConfirmation(); + } + }, + }); + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) this.mmiConfigurationController = new MmiConfigurationController({ initState: initState.MmiConfigurationController, @@ -5642,7 +5652,7 @@ export default class MetamaskController extends EventEmitter { this.preferencesController, ), shouldEnqueueRequest: (request) => { - return methodsRequiringNetworkSwitch.includes(request.method); + return methodsThatShouldBeEnqueued.includes(request.method); }, }); engine.push(requestQueueMiddleware); diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index b9b6062a30d7..943338c29bdb 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -787,15 +787,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1354,9 +1369,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/keyring-api": { @@ -2197,64 +2227,13 @@ }, "@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/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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, - "@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, @@ -2326,8 +2305,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@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/rpc-methods-flask>nanoid": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index b9b6062a30d7..943338c29bdb 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -787,15 +787,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1354,9 +1369,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/keyring-api": { @@ -2197,64 +2227,13 @@ }, "@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/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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, - "@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, @@ -2326,8 +2305,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@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/rpc-methods-flask>nanoid": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index b9b6062a30d7..943338c29bdb 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -787,15 +787,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1354,9 +1369,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/keyring-api": { @@ -2197,64 +2227,13 @@ }, "@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/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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, - "@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, @@ -2326,8 +2305,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@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/rpc-methods-flask>nanoid": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 073882b78df5..321e8ea7e6ff 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -879,15 +879,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1446,9 +1461,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/keyring-api": { @@ -2289,64 +2319,13 @@ }, "@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/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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, - "@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, @@ -2418,8 +2397,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@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/rpc-methods-flask>nanoid": { diff --git a/package.json b/package.json index d4517b42229d..bd0ec3a52409 100644 --- a/package.json +++ b/package.json @@ -345,7 +345,7 @@ "@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", + "@metamask/queued-request-controller": "^7.0.0", "@metamask/rate-limit-controller": "^6.0.0", "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", diff --git a/shared/constants/methods-tags.ts b/shared/constants/methods-tags.ts index a35954769b1b..329c0d493244 100644 --- a/shared/constants/methods-tags.ts +++ b/shared/constants/methods-tags.ts @@ -16,3 +16,22 @@ export const methodsRequiringNetworkSwitch = [ 'eth_signTypedData_v4', 'personal_sign', ] as const; + +/** + * This is a list of methods that may change the globally selected network + * without prompting for user approval. For UI/UX reasons these type of + * requests must be treated specially in the QueuedRequestController. + */ +export const methodsThatCanSwitchNetworkWithoutApproval = [ + 'wallet_addEthereumChain', + 'wallet_switchEthereumChain', +]; + +/** + * This is a list of methods that require special handling and must + * be enqueued and processed by the QueuedRequestController. + */ +export const methodsThatShouldBeEnqueued = [ + ...methodsRequiringNetworkSwitch, + ...methodsThatCanSwitchNetworkWithoutApproval, +]; diff --git a/yarn.lock b/yarn.lock index 3558c24d8d5d..2643747a815a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4972,13 +4972,13 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1": - version: 7.0.1 - resolution: "@metamask/base-controller@npm:7.0.1" +"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1, @metamask/base-controller@npm:^7.0.2": + version: 7.0.2 + resolution: "@metamask/base-controller@npm:7.0.2" dependencies: - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^10.0.0" immer: "npm:^9.0.6" - checksum: 10/774b6d68ac95a5ec187e890d321bede50065f8a6f1ba7b49a19f5971366274054ac0e401548b51d3b014d0bca5d650409fb554dd13ce120e7fb3495b4e8e67b1 + checksum: 10/6f78ec5af840c9947aa8eac6e402df6469600260d613a92196daefd5b072097a176fe5da1c386f2d36853513254b74140d667d817a12880c46f088e18ff3606a languageName: node linkType: hard @@ -5015,20 +5015,21 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0": - version: 11.4.0 - resolution: "@metamask/controller-utils@npm:11.4.0" +"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@npm:^11.4.1": + version: 11.4.2 + resolution: "@metamask/controller-utils@npm:11.4.2" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^10.0.0" "@spruceid/siwe-parser": "npm:2.1.0" "@types/bn.js": "npm:^5.1.5" + bignumber.js: "npm:^9.1.2" bn.js: "npm:^5.2.1" eth-ens-namehash: "npm:^2.0.8" fast-deep-equal: "npm:^3.1.3" - checksum: 10/f34d24880eab264bddaa5bef21afaecb206db6978364565d0f7b7a54b1d411f129eb84175041df3be8a66394c2d49e83b6648b5cbde6f34662a60fc553c31458 + checksum: 10/fdae49ee97e7a2a1bb6414011ca59932f8712a768a9c4c43673a2504c9fa9e61d83df53a21ff0506ef6a8cf774704f2df58a6d71385c8786ec5cab4359c051e1 languageName: node linkType: hard @@ -5582,14 +5583,14 @@ __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" +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/json-rpc-engine@npm:10.0.1" dependencies: - "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.1.0" - checksum: 10/2c401a4a64392aeb11c4f7ca8d7b458ba1106cff1e0b3dba8b3e0cc90e82f8c55ac2dc9fdfcd914b289e3298fb726d637cf21382336dde2c207cf76129ce5eab + "@metamask/utils": "npm:^10.0.0" + checksum: 10/15a8eeab9af39b9ed87311da728e81169484ace733a8ef9fc469bd887654e37afa19f9e5228246dc80daad3fbf9b16067e73b2969d37d44acf5bc6ffa2c70082 languageName: node linkType: hard @@ -6137,20 +6138,20 @@ __metadata: languageName: node linkType: hard -"@metamask/queued-request-controller@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/queued-request-controller@npm:2.0.0" +"@metamask/queued-request-controller@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/queued-request-controller@npm:7.0.0" dependencies: - "@metamask/base-controller": "npm:^6.0.0" - "@metamask/controller-utils": "npm:^11.0.0" - "@metamask/json-rpc-engine": "npm:^9.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/swappable-obj-proxy": "npm:^2.2.0" - "@metamask/utils": "npm:^8.3.0" + "@metamask/utils": "npm:^10.0.0" peerDependencies: - "@metamask/network-controller": ^19.0.0 - "@metamask/selected-network-controller": ^15.0.0 - checksum: 10/b618fa05465a52e5b689d932d99b47552b5987a9141d58260966611f1057190132f14b1a2123c48399f218fc57c577e1c86375e8ee2b43871cdc597fbaeedb7a + "@metamask/network-controller": ^22.0.0 + "@metamask/selected-network-controller": ^19.0.0 + checksum: 10/69118c11e3faecdbec7c9f02f4ecec4734ce0950115bfac0cdd4338309898690ae3187bcef1cc4f75f54c5c02eff07d80286d3ef29088a665039c13cb50bef88 languageName: node linkType: hard @@ -6175,13 +6176,13 @@ __metadata: languageName: node linkType: hard -"@metamask/rpc-errors@npm:^7.0.0": - version: 7.0.0 - resolution: "@metamask/rpc-errors@npm:7.0.0" +"@metamask/rpc-errors@npm:^7.0.0, @metamask/rpc-errors@npm:^7.0.1": + version: 7.0.1 + resolution: "@metamask/rpc-errors@npm:7.0.1" dependencies: - "@metamask/utils": "npm:^9.0.0" + "@metamask/utils": "npm:^10.0.0" fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd + checksum: 10/819708b4a7d9695ee67fd867d8f94bb5a273b479a242b17bd53c83d1fceec421fc42928f0bb340f4f138ec803dd82ec9659ce7b09a86aedad6a81d5a39ec5c35 languageName: node linkType: hard @@ -6583,6 +6584,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/utils@npm:10.0.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/9c2e6421f685d8a45145b6026a6f9fd0701eb5a2e8490fc6d18e64c103d5a62097f301cbc797790da52ceb5853bd9f65845c934b00299e69e5e6736c52b32f0f + languageName: node + linkType: hard + "@metamask/utils@npm:^8.1.0, @metamask/utils@npm:^8.2.0, @metamask/utils@npm:^8.3.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" @@ -26150,7 +26168,7 @@ __metadata: "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" - "@metamask/queued-request-controller": "npm:^2.0.0" + "@metamask/queued-request-controller": "npm:^7.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" From 8b7ad816b3b9481dcc17bbb6f8df799b28fe656d Mon Sep 17 00:00:00 2001 From: Matthew Walsh <matthew.walsh@consensys.net> Date: Thu, 31 Oct 2024 09:06:42 +0000 Subject: [PATCH 220/226] fix (cherry-pick): incorrect standard swap gas fee estimation (#28127) (#28191) --- .../swaps/prepare-swap-page/review-quote.js | 3 +- .../prepare-swap-page/review-quote.test.js | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 13d11a93cd1f..680ece113f5d 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -257,7 +257,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { ); const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); - const { estimatedBaseFee = '0' } = useGasFeeEstimates(); + const { gasFeeEstimates: networkGasFeeEstimates } = useGasFeeEstimates(); + const { estimatedBaseFee = '0' } = networkGasFeeEstimates ?? {}; const gasFeeEstimates = useAsyncResult(async () => { if (!networkAndAccountSupports1559) { diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.test.js b/ui/pages/swaps/prepare-swap-page/review-quote.test.js index cacd52ca47ed..1e4ab9199226 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.test.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.test.js @@ -10,6 +10,7 @@ import { } from '../../../../test/jest'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { getSwap1559GasFeeEstimates } from '../swaps.util'; +import { getNetworkConfigurationByNetworkClientId } from '../../../store/actions'; import ReviewQuote from './review-quote'; jest.mock( @@ -17,11 +18,18 @@ jest.mock( () => () => '<InfoTooltipIcon />', ); +jest.mock('../../../store/actions', () => ({ + ...jest.requireActual('../../../store/actions'), + getNetworkConfigurationByNetworkClientId: jest.fn(), +})); + jest.mock('../swaps.util', () => ({ ...jest.requireActual('../swaps.util'), getSwap1559GasFeeEstimates: jest.fn(), })); +const ESTIMATED_BASE_FEE_MOCK = '1234'; + const middleware = [thunk]; const createProps = (customProps = {}) => { return { @@ -31,6 +39,15 @@ const createProps = (customProps = {}) => { }; describe('ReviewQuote', () => { + const getNetworkConfigurationByNetworkClientIdMock = jest.mocked( + getNetworkConfigurationByNetworkClientId, + ); + + beforeEach(() => { + jest.resetAllMocks(); + getNetworkConfigurationByNetworkClientIdMock.mockResolvedValue(undefined); + }); + const getSwap1559GasFeeEstimatesMock = jest.mocked( getSwap1559GasFeeEstimates, ); @@ -210,5 +227,45 @@ describe('ReviewQuote', () => { expect(getByText('Max fee:')).toBeInTheDocument(); expect(getByText('$8.15')).toBeInTheDocument(); }); + + it('extracts estimated base fee from network gas fee estimates', async () => { + getNetworkConfigurationByNetworkClientIdMock.mockResolvedValueOnce({ + chainId: CHAIN_IDS.MAINNET, + }); + + smartDisabled1559State.metamask.gasFeeEstimatesByChainId = { + [CHAIN_IDS.MAINNET]: { + gasFeeEstimates: { + estimatedBaseFee: ESTIMATED_BASE_FEE_MOCK, + }, + }, + }; + + getSwap1559GasFeeEstimatesMock.mockResolvedValueOnce({ + estimatedBaseFee: '0x1', + tradeGasFeeEstimates: { + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + baseAndPriorityFeePerGas: '0x123456789123', + }, + approveGasFeeEstimates: undefined, + }); + + const store = configureMockStore(middleware)(smartDisabled1559State); + const props = createProps(); + + renderWithProvider(<ReviewQuote {...props} />, store); + + await act(() => { + // Intentionally empty + }); + + expect(getSwap1559GasFeeEstimatesMock).toHaveBeenCalledWith( + expect.any(Object), + null, + ESTIMATED_BASE_FEE_MOCK, + CHAIN_IDS.MAINNET, + ); + }); }); }); From 94dc115ccf132bbbc5256cc6d7ef0b3dffc18d75 Mon Sep 17 00:00:00 2001 From: MetaMask Bot <metamaskbot@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:38:44 +0000 Subject: [PATCH 221/226] Update LavaMoat policies --- lavamoat/browserify/beta/policy.json | 37 +++++++++++---------------- lavamoat/browserify/flask/policy.json | 37 +++++++++++---------------- lavamoat/browserify/main/policy.json | 37 +++++++++++---------------- lavamoat/browserify/mmi/policy.json | 37 +++++++++++---------------- 4 files changed, 60 insertions(+), 88 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 943338c29bdb..ef4c915328c2 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2971,9 +2971,9 @@ "@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, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2999,10 +2999,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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": { @@ -4151,10 +4160,10 @@ "eth-lattice-keyring>gridplus-sdk>elliptic": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, "eth-lattice-keyring>gridplus-sdk>rlp": true, - "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, + "ganache>secp256k1": true, "lodash": true } }, @@ -4267,22 +4276,6 @@ "TextEncoder": true } }, - "eth-lattice-keyring>gridplus-sdk>secp256k1": { - "packages": { - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": true - } - }, - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true - } - }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 943338c29bdb..ef4c915328c2 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2971,9 +2971,9 @@ "@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, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2999,10 +2999,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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": { @@ -4151,10 +4160,10 @@ "eth-lattice-keyring>gridplus-sdk>elliptic": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, "eth-lattice-keyring>gridplus-sdk>rlp": true, - "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, + "ganache>secp256k1": true, "lodash": true } }, @@ -4267,22 +4276,6 @@ "TextEncoder": true } }, - "eth-lattice-keyring>gridplus-sdk>secp256k1": { - "packages": { - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": true - } - }, - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true - } - }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 943338c29bdb..ef4c915328c2 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2971,9 +2971,9 @@ "@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, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2999,10 +2999,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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": { @@ -4151,10 +4160,10 @@ "eth-lattice-keyring>gridplus-sdk>elliptic": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, "eth-lattice-keyring>gridplus-sdk>rlp": true, - "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, + "ganache>secp256k1": true, "lodash": true } }, @@ -4267,22 +4276,6 @@ "TextEncoder": true } }, - "eth-lattice-keyring>gridplus-sdk>secp256k1": { - "packages": { - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": true - } - }, - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true - } - }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 321e8ea7e6ff..94c331a71ee5 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -3063,9 +3063,9 @@ "@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, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -3091,10 +3091,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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": { @@ -4243,10 +4252,10 @@ "eth-lattice-keyring>gridplus-sdk>elliptic": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, "eth-lattice-keyring>gridplus-sdk>rlp": true, - "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, + "ganache>secp256k1": true, "lodash": true } }, @@ -4359,22 +4368,6 @@ "TextEncoder": true } }, - "eth-lattice-keyring>gridplus-sdk>secp256k1": { - "packages": { - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": true - } - }, - "eth-lattice-keyring>gridplus-sdk>secp256k1>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true - } - }, "eth-lattice-keyring>gridplus-sdk>uuid": { "globals": { "crypto": true From eb9a2edf465e1429dd54302645e46d6b32f112dd Mon Sep 17 00:00:00 2001 From: Harika <153644847+hjetpoluru@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:57:09 -0400 Subject: [PATCH 222/226] chore: Cherry pick data deletion into v12.6.0 (#28223) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Cherry-pick PR https://github.com/MetaMask/metamask-extension/pull/28221 <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28223?quickstart=1) Co-authored-by: Mark Stacey <markjstacey@gmail.com> --- app/scripts/services/data-deletion-service.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/scripts/services/data-deletion-service.ts b/app/scripts/services/data-deletion-service.ts index 3bdafc03b582..5ec9ede75a87 100644 --- a/app/scripts/services/data-deletion-service.ts +++ b/app/scripts/services/data-deletion-service.ts @@ -12,11 +12,17 @@ import { import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import { DeleteRegulationStatus } from '../../../shared/constants/metametrics'; -const DEFAULT_ANALYTICS_DATA_DELETION_SOURCE_ID = - process.env.ANALYTICS_DATA_DELETION_SOURCE_ID ?? 'test'; -const DEFAULT_ANALYTICS_DATA_DELETION_ENDPOINT = - process.env.ANALYTICS_DATA_DELETION_ENDPOINT ?? - 'https://metametrics.metamask.test'; +const inTest = process.env.IN_TEST; +const fallbackSourceId = 'test'; +const fallbackDataDeletionEndpoint = 'https://metametrics.metamask.test'; + +const DEFAULT_ANALYTICS_DATA_DELETION_SOURCE_ID = inTest + ? fallbackSourceId + : process.env.ANALYTICS_DATA_DELETION_SOURCE_ID ?? fallbackSourceId; +const DEFAULT_ANALYTICS_DATA_DELETION_ENDPOINT = inTest + ? fallbackDataDeletionEndpoint + : process.env.ANALYTICS_DATA_DELETION_ENDPOINT ?? + fallbackDataDeletionEndpoint; /** * The number of times we retry a specific failed request to the data deletion API. From 8fa97988b5c2f92e589357b234d9fda6b87dc31c Mon Sep 17 00:00:00 2001 From: Marina Boboc <120041701+benjisclowder@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:30:37 +0200 Subject: [PATCH 223/226] V12.6.0 Changelog (#28166) ## **Description** Adding 12.6.0 changelog entries. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28166?quickstart=1) --------- Co-authored-by: Dan J Miller <danjm.com@gmail.com> --- CHANGELOG.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db903de48796..59734a8a23f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [12.6.0] +### Added +- Added the APE network icon ([#27841](https://github.com/MetaMask/metamask-extension/pull/27841)) +- Added token sorting and improved token importing on the Asset List page ([#27184](https://github.com/MetaMask/metamask-extension/pull/27184)) +- Added an aggregated balance feature and updated settings to toggle between fiat and native token balances ([#27097](https://github.com/MetaMask/metamask-extension/pull/27097)) +- Added a network picker to the AssetPicker for easier cross-chain swaps ([#26559](https://github.com/MetaMask/metamask-extension/pull/26559)) +- Added new header and conditional simulations for dapp-initiated token transfer confirmations ([#27875](https://github.com/MetaMask/metamask-extension/pull/27875)) +- Added simulation section to NFT permit confirmations ([#27825](https://github.com/MetaMask/metamask-extension/pull/27825)) +- Added transaction flow and details sections for wallet-initiated ERC20 token transfer confirmations ([#27654](https://github.com/MetaMask/metamask-extension/pull/27654)) +- Added support for typed sign requests for NFT permits ([#27796](https://github.com/MetaMask/metamask-extension/pull/27796)) +- Added support for gas fee flows in standard swaps on EIP-1559 networks ([#27612](https://github.com/MetaMask/metamask-extension/pull/27612)) +- Added a Token Send Heading component ([#27562](https://github.com/MetaMask/metamask-extension/pull/27562)) +- Added support for Etherscan API keys and improved transaction history logging ([#27611](https://github.com/MetaMask/metamask-extension/pull/27611)) +- Added a custom header for wallet-initiated ERC20 token transfer confirmations ([#27391](https://github.com/MetaMask/metamask-extension/pull/27391)) +- Added redesigned screens for setApprovalForAll and revoke setApprovalForAll for users who opt into experimental transaction screens ([#27401](https://github.com/MetaMask/metamask-extension/pull/27401)) +- Added new screens for approve, increaseAllowance, and revoke approval for users who enable experimental transaction screens ([#26985](https://github.com/MetaMask/metamask-extension/pull/26985)) +- Added support for revoking ERC20 allowances ([#26906](https://github.com/MetaMask/metamask-extension/pull/26906)) +- Added a "Delete MetaMetrics Data" button to the Security & Privacy tab, allowing users to delete their MetaMetrics data ([#24571](https://github.com/MetaMask/metamask-extension/pull/24571)) +- Added a new Default Settings view and updated Congratulations views in the onboarding process ([#24562](https://github.com/MetaMask/metamask-extension/pull/24562)) +- Added a delay for Linea swap approvals to increase success rate and updated token symbol retrieval on the awaiting swap page ([#27810](https://github.com/MetaMask/metamask-extension/pull/27810)) +- Enabled smart transactions by default for new users and updated selectors to handle user preferences and metrics separately ([#27885](https://github.com/MetaMask/metamask-extension/pull/27885)) +- Added animations and cosmetic changes to the smart transaction status page ([#27650](https://github.com/MetaMask/metamask-extension/pull/27650)) +- Enabled gas-included swaps for users with insufficient ETH when smart transactions are enabled ([#27427](https://github.com/MetaMask/metamask-extension/pull/27427)) +- Added padding to center-align text on the permissions page when no site or snap is connected ([#27660](https://github.com/MetaMask/metamask-extension/pull/27660)) +- Released Chain Permissions by removing feature flags ([#27561](https://github.com/MetaMask/metamask-extension/pull/27561)) +- Added support for power users survey with toast notifications ([#27361](https://github.com/MetaMask/metamask-extension/pull/27361)) +- Added editing flow for switching networks via dapp ([#26635](https://github.com/MetaMask/metamask-extension/pull/26635)) +- [FLASK] Added the ability to send Bitcoin from Bitcoin accounts ([#27964](https://github.com/MetaMask/metamask-extension/pull/27964)) + +### Changed +- Bumped snap-keyring to version 4.4.0 to sanitize redirect URLs passed by a Snap ([#27864](https://github.com/MetaMask/metamask-extension/pull/27864)) +- Updated the insufficient funds alert to replace "transaction fees" with "network fees." ([#27762](https://github.com/MetaMask/metamask-extension/pull/27762)) +- Updated the SIWE signature page to display the parsed URI instead of the domain ([#27754](https://github.com/MetaMask/metamask-extension/pull/27754)) +- Limited the number of decimals on the spending cap modal to match the token's supported decimals ([#27672](https://github.com/MetaMask/metamask-extension/pull/27672)) +- Updated petnames component to prefer displaying token symbols over token names for brevity ([#27693](https://github.com/MetaMask/metamask-extension/pull/27693)) +- Updated banner alert to render multiple general alerts and fixed related UI issues ([#27339](https://github.com/MetaMask/metamask-extension/pull/27339)) +- Updated Trezor Connect to v9.4.0 and removed outdated workarounds ([#27112](https://github.com/MetaMask/metamask-extension/pull/27112)) +- Restored the ability to switch between pending confirmations when routed to a specific confirmation ([#27753](https://github.com/MetaMask/metamask-extension/pull/27753)) +- Updated edit modals with design improvements and a fixed update button ([#27623](https://github.com/MetaMask/metamask-extension/pull/27623)) +- Updated copy for the onboarding message and settings screens ([#27821](https://github.com/MetaMask/metamask-extension/pull/27821)) +- Updated copy and spacing in the Permissions Screen ([#27658](https://github.com/MetaMask/metamask-extension/pull/27658)) +- Removed phishing detection from the onboarding Security group ([#27819](https://github.com/MetaMask/metamask-extension/pull/27819)) +- Removed the "Alerts" section from Settings, keeping alert features enabled by default ([#27709](https://github.com/MetaMask/metamask-extension/pull/27709)) +- Updated the toast component and its copy ([#27656](https://github.com/MetaMask/metamask-extension/pull/27656)) +- Changed survey timeout from one week to one day ([#27603](https://github.com/MetaMask/metamask-extension/pull/27603)) +- Updated UI for the connect and review permissions pages ([#27478](https://github.com/MetaMask/metamask-extension/pull/27478)) + +### Fixed +- Fixed an error when starting a "Send ETH" flow from a dapp with a Bitcoin account selected ([#27566](https://github.com/MetaMask/metamask-extension/pull/27566)) +- Fixed currency display to show token balance when fiat conversion rate is unavailable ([#27893](https://github.com/MetaMask/metamask-extension/pull/27893)) +- Fixed the issue where the add token modal couldn't be dismissed in MMI ([#27855](https://github.com/MetaMask/metamask-extension/pull/27855)) +- Fixed an issue that caused the app to crash when switching networks ([#27604](https://github.com/MetaMask/metamask-extension/pull/27604)) +- Fixed navigation error between transactions when one transaction is of type "Approve All." ([#27985](https://github.com/MetaMask/metamask-extension/pull/27985)) +- Fixed nonce value updating issue when multiple transactions are created in parallel ([#27874](https://github.com/MetaMask/metamask-extension/pull/27874)) +- Fixed issue with nonce not resetting when switching networks ([#27789](https://github.com/MetaMask/metamask-extension/pull/27789)) +- Fixed design issues and spacing in the redesigned transactions, and corrected loader behavior for confirmations ([#27605](https://github.com/MetaMask/metamask-extension/pull/27605)) +- Fixed bugs related to max approval values and array value spending caps ([#27573](https://github.com/MetaMask/metamask-extension/pull/27573)) +- Reverted the color change for the "Speed" key by removing the variant causing the issue ([#27416](https://github.com/MetaMask/metamask-extension/pull/27416)) +- Improved token decimal handling by using verified contract details when available and added support for tokens with null decimals ([#27328](https://github.com/MetaMask/metamask-extension/pull/27328)) +- Improved the alert system and refined alerts for SIWE and contract interactions ([#27205](https://github.com/MetaMask/metamask-extension/pull/27205)) +- Fixed an issue where entering a backslash in the settings search would cause a crash ([#27432](https://github.com/MetaMask/metamask-extension/pull/27432)) +- Automatically expand the first insight on the confirmation page ([#27872](https://github.com/MetaMask/metamask-extension/pull/27872)) +- Removed HTML arrows from custom UI inputs of type number in Snaps ([#27953](https://github.com/MetaMask/metamask-extension/pull/27953)) +- Hid the options menu and info icon in the Snaps header for preinstalled Snaps ([#27937](https://github.com/MetaMask/metamask-extension/pull/27937)) +- Fixed sticky footer UI issue on Snaps Home Page in extended view ([#27799](https://github.com/MetaMask/metamask-extension/pull/27799)) +- Fixed issue with Snap name truncation in the Snap Authorship Header ([#27752](https://github.com/MetaMask/metamask-extension/pull/27752)) +- Fixed the color of the "more" button in the Copyable component ([#27600](https://github.com/MetaMask/metamask-extension/pull/27600)) +- Fixed alignment issue by applying flex to Snaps buttons only when containing images and icons ([#27564](https://github.com/MetaMask/metamask-extension/pull/27564)) +- Fixed issue with input focus being lost on re-render in Snaps interfaces ([#27429](https://github.com/MetaMask/metamask-extension/pull/27429)) +- Fixed issue where state updates with falsy values were ignored in Snaps interfaces ([#27488](https://github.com/MetaMask/metamask-extension/pull/27488)) +- Fixed text color for secondary buttons in Snaps footer on hover and corrected footer variant when only one action is provided ([#27335](https://github.com/MetaMask/metamask-extension/pull/27335)) +- Fixed an issue where hardware wallet users were taken to the "Processing..." screen before approving transactions during swaps ([#27117](https://github.com/MetaMask/metamask-extension/pull/27117)) ### Uncategorized - ci: reduced Sentry frequency on CircleCI develop ([#27912](https://github.com/MetaMask/metamask-extension/pull/27912)) - chore:Master sync ([#27935](https://github.com/MetaMask/metamask-extension/pull/27935)) @@ -231,7 +302,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Instead of having different networks in the network list for the same chain but different RPC urls, there are now multiple selectable RPC urls per chain - For the UI, networks are now added, edited, and deleted directly in the network list. Networks are no longer edited via the settings page. - Users with multiple RPC endpoints per chain are shown a modal upon upgrade, allowing them to select a different endpoint as the default. - - The UI for wallet_addEthereumChain is changed, to message that users may be adding an additional endpoint to an existing network, rather than adding a new network. + - The UI for wallet_addEthereumChain is changed, to message that users may be adding an additional endpoint to an existing network, rather than adding a new network. - Added display of names and images for ERC721 NFTs to the simulations in transaction confirmations ([#25692](https://github.com/MetaMask/metamask-extension/pull/25692)) - Added a modal to edit the spending cap for ERC20 approve and increase allowance ([#26845](https://github.com/MetaMask/metamask-extension/pull/26845)) - Added a new modal to help users with zero balance buy, receive, or transfer tokens ([#26426](https://github.com/MetaMask/metamask-extension/pull/26426)) From 3df0a1683659b5f8d1856c667cb74de94fe28fbe Mon Sep 17 00:00:00 2001 From: Dan J Miller <danjm.com@gmail.com> Date: Thu, 31 Oct 2024 16:21:41 -0230 Subject: [PATCH 224/226] v12.6.0 changelog lint fix (#28228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- CHANGELOG.md | 212 +-------------------------------------------------- 1 file changed, 1 insertion(+), 211 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59734a8a23f9..af63a4ed61d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,217 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed issue where state updates with falsy values were ignored in Snaps interfaces ([#27488](https://github.com/MetaMask/metamask-extension/pull/27488)) - Fixed text color for secondary buttons in Snaps footer on hover and corrected footer variant when only one action is provided ([#27335](https://github.com/MetaMask/metamask-extension/pull/27335)) - Fixed an issue where hardware wallet users were taken to the "Processing..." screen before approving transactions during swaps ([#27117](https://github.com/MetaMask/metamask-extension/pull/27117)) -### Uncategorized -- ci: reduced Sentry frequency on CircleCI develop ([#27912](https://github.com/MetaMask/metamask-extension/pull/27912)) -- chore:Master sync ([#27935](https://github.com/MetaMask/metamask-extension/pull/27935)) -- Merge origin/develop into master-sync -- test: Completing missing step for import ERC1155 token origin dapp in existing E2E test ([#27680](https://github.com/MetaMask/metamask-extension/pull/27680)) -- fix: error in navigating between transaction when one of the transaction is approve all ([#27985](https://github.com/MetaMask/metamask-extension/pull/27985)) -- fix: Automatically expand first insight ([#27872](https://github.com/MetaMask/metamask-extension/pull/27872)) -- feat(metametrics): use specific `account_hardware_type` for OneKey devices ([#27296](https://github.com/MetaMask/metamask-extension/pull/27296)) -- feat: add migration 131 ([#27364](https://github.com/MetaMask/metamask-extension/pull/27364)) -- fix(snaps): Remove arrows of custom UI inputs ([#27953](https://github.com/MetaMask/metamask-extension/pull/27953)) -- chore: Disable account syncing in prod ([#27943](https://github.com/MetaMask/metamask-extension/pull/27943)) -- test: Remove delays from onboarding tests ([#27961](https://github.com/MetaMask/metamask-extension/pull/27961)) -- perf: Create custom trace to measure performance of opening the account list ([#27907](https://github.com/MetaMask/metamask-extension/pull/27907)) -- feat: add BTC send flow ([#27964](https://github.com/MetaMask/metamask-extension/pull/27964)) -- fix: flaky test `Confirmation Redesign ERC721 Approve Component Submit an Approve transaction @no-mmi Sends a type 2 transaction (EIP1559)` ([#27928](https://github.com/MetaMask/metamask-extension/pull/27928)) -- fix: lint-lockfile flaky job by changing resources from medium to medium-plus ([#27950](https://github.com/MetaMask/metamask-extension/pull/27950)) -- feat: add “Incomplete Asset Displayed” metric & fix: should only set default decimals if ERC20 ([#27494](https://github.com/MetaMask/metamask-extension/pull/27494)) -- feat: Convert AppStateController to typescript ([#27572](https://github.com/MetaMask/metamask-extension/pull/27572)) -- chore(deps): upgrade from json-rpc-engine to @metamask/json-rpc-engine ([#22875](https://github.com/MetaMask/metamask-extension/pull/22875)) -- feat: dapp initiated token transfer ([#27875](https://github.com/MetaMask/metamask-extension/pull/27875)) -- chore: bump signature controller to remove message managers ([#27787](https://github.com/MetaMask/metamask-extension/pull/27787)) -- chore: add testing-library/dom dependency ([#27493](https://github.com/MetaMask/metamask-extension/pull/27493)) -- test: [POM] Migrate contract interaction with snap account e2e tests to page object modal ([#27924](https://github.com/MetaMask/metamask-extension/pull/27924)) -- fix: bump message signing snap to support portfolio automatic connections ([#27936](https://github.com/MetaMask/metamask-extension/pull/27936)) -- fix: hide options menu that was being shown for preinstalled Snaps ([#27937](https://github.com/MetaMask/metamask-extension/pull/27937)) -- fix: bump `@metamask/ppom-validator` from `0.34.0` to `0.35.1` ([#27939](https://github.com/MetaMask/metamask-extension/pull/27939)) -- fix: add APE network icon ([#27841](https://github.com/MetaMask/metamask-extension/pull/27841)) -- feat: NFT permit simulations ([#27825](https://github.com/MetaMask/metamask-extension/pull/27825)) -- fix: fix currency display when tokenToFiatConversion rate is not avai… ([#27893](https://github.com/MetaMask/metamask-extension/pull/27893)) -- feat: convert AlertController to typescript ([#27764](https://github.com/MetaMask/metamask-extension/pull/27764)) -- feat(TXL-435): turn smart transactions on by default for new users ([#27885](https://github.com/MetaMask/metamask-extension/pull/27885)) -- feat: Add transaction flow and details sections ([#27654](https://github.com/MetaMask/metamask-extension/pull/27654)) -- fix: flaky test `Vault Decryptor Page is able to decrypt the vault pasting the text in the vault-decryptor webapp` ([#27921](https://github.com/MetaMask/metamask-extension/pull/27921)) -- chore: bump `@metamask/eth-snap-keyring` to version 4.4.0 ([#27864](https://github.com/MetaMask/metamask-extension/pull/27864)) -- fix: flaky tests `Add existing token using search renders the balance for the chosen token` ([#27853](https://github.com/MetaMask/metamask-extension/pull/27853)) -- feat(logging): add extension request logging and retrieval ([#27655](https://github.com/MetaMask/metamask-extension/pull/27655)) -- test: Update test-dapp to verison 8.7.0 ([#27816](https://github.com/MetaMask/metamask-extension/pull/27816)) -- fix: fall back to bundled chainlist ([#23392](https://github.com/MetaMask/metamask-extension/pull/23392)) -- fix: SonarCloud for forks ([#27700](https://github.com/MetaMask/metamask-extension/pull/27700)) -- fix(deps): update from eth-rpc-errors to @metamask/rpc-errors (cause edition) ([#24496](https://github.com/MetaMask/metamask-extension/pull/24496)) -- fix: swapQuotesError as a property in the reported metric ([#27712](https://github.com/MetaMask/metamask-extension/pull/27712)) -- chore: Bump Snaps packages ([#27376](https://github.com/MetaMask/metamask-extension/pull/27376)) -- chore: update @metamask/bitcoin-wallet-snap to 0.7.0 ([#27730](https://github.com/MetaMask/metamask-extension/pull/27730)) -- fix: Onboarding: Code style nits ([#27767](https://github.com/MetaMask/metamask-extension/pull/27767)) -- fix: updated edit modals ([#27623](https://github.com/MetaMask/metamask-extension/pull/27623)) -- feat: use asset pickers with network dropdown in cross-chain swaps page ([#27522](https://github.com/MetaMask/metamask-extension/pull/27522)) -- test: set ENABLE_MV3 automatically ([#27748](https://github.com/MetaMask/metamask-extension/pull/27748)) -- feat: Adding typed sign support for NFT permit ([#27796](https://github.com/MetaMask/metamask-extension/pull/27796)) -- fix: Contract Interaction - cannot read the property `text_signature` ([#27686](https://github.com/MetaMask/metamask-extension/pull/27686)) -- feat: Use requested permissions as default selected values for AmonHenV2 connection flow with case insensitive address comparison ([#27517](https://github.com/MetaMask/metamask-extension/pull/27517)) -- test: [POM] Migrate signature with snap account e2e tests to page object modal ([#27829](https://github.com/MetaMask/metamask-extension/pull/27829)) -- fix: flaky test `ERC1155 NFTs testdapp interaction should batch transfers ERC1155 token` ([#27897](https://github.com/MetaMask/metamask-extension/pull/27897)) -- chore: Master sync following v12.4.1 ([#27793](https://github.com/MetaMask/metamask-extension/pull/27793)) -- fix: flaky test `Permissions sets permissions and connect to Dapp` ([#27888](https://github.com/MetaMask/metamask-extension/pull/27888)) -- fix: flaky test `ERC721 NFTs testdapp interaction should prompt users to add their NFTs to their wallet (all at once)` ([#27889](https://github.com/MetaMask/metamask-extension/pull/27889)) -- fix: flaky test `Wallet Revoke Permissions should revoke eth_accounts permissions via test dapp` ([#27894](https://github.com/MetaMask/metamask-extension/pull/27894)) -- fix: flaky test `Snap Account Signatures and Disconnects can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)` ([#27887](https://github.com/MetaMask/metamask-extension/pull/27887)) -- test(mock-e2e): add private domains logic for the privacy report ([#27844](https://github.com/MetaMask/metamask-extension/pull/27844)) -- fix: SENTRY_DSN_FAKE problem ([#27881](https://github.com/MetaMask/metamask-extension/pull/27881)) -- chore: remove unused swaps code ([#27679](https://github.com/MetaMask/metamask-extension/pull/27679)) -- test(TXL-308): initial e2e for stx using swaps ([#27215](https://github.com/MetaMask/metamask-extension/pull/27215)) -- feat: upgrade assets-controllers to v38.3.0 ([#27755](https://github.com/MetaMask/metamask-extension/pull/27755)) -- fix: nonce value when there are multiple transactions in parallel ([#27874](https://github.com/MetaMask/metamask-extension/pull/27874)) -- fix: phishing test to not check c2 domains ([#27846](https://github.com/MetaMask/metamask-extension/pull/27846)) -- feat: use messenger in AccountTracker to get Preferences state ([#27711](https://github.com/MetaMask/metamask-extension/pull/27711)) -- fix: "Update Network: should update added rpc url for exis..." flaky tests ([#27437](https://github.com/MetaMask/metamask-extension/pull/27437)) -- feat: update copy for 'Default settings' ([#27821](https://github.com/MetaMask/metamask-extension/pull/27821)) -- fix: updated permissions flow copy changes ([#27658](https://github.com/MetaMask/metamask-extension/pull/27658)) -- fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi` ([#27834](https://github.com/MetaMask/metamask-extension/pull/27834)) -- fix: hackily wait longer for linea swap approval tx to increase chance of success ([#27810](https://github.com/MetaMask/metamask-extension/pull/27810)) -- fix: flaky test `MultiRpc: should select rpc from settings @no-mmi` ([#27858](https://github.com/MetaMask/metamask-extension/pull/27858)) -- perf: include custom traces in benchmark results ([#27701](https://github.com/MetaMask/metamask-extension/pull/27701)) -- fix: Reset nonce as network is switched ([#27789](https://github.com/MetaMask/metamask-extension/pull/27789)) -- fix: dismiss addToken modal for mmi ([#27855](https://github.com/MetaMask/metamask-extension/pull/27855)) -- fix(multichain): fix eth send flow (from dapp) when a btc account is selected ([#27566](https://github.com/MetaMask/metamask-extension/pull/27566)) -- chore: Add react-beautiful-dnd to deprecated packages list ([#27856](https://github.com/MetaMask/metamask-extension/pull/27856)) -- feat: Create a quality gate for typescript coverage ([#27717](https://github.com/MetaMask/metamask-extension/pull/27717)) -- feat: preferences controller to base controller v2 ([#27398](https://github.com/MetaMask/metamask-extension/pull/27398)) -- revert: use networkClientId to resolve chainId in PPOM Middleware ([#27570](https://github.com/MetaMask/metamask-extension/pull/27570)) -- feat: Added metrics for edit networks and accounts ([#27820](https://github.com/MetaMask/metamask-extension/pull/27820)) -- fix: no connected state for permissions page ([#27660](https://github.com/MetaMask/metamask-extension/pull/27660)) -- feat: remove phishing detection from onboarding Security group ([#27819](https://github.com/MetaMask/metamask-extension/pull/27819)) -- ci: Revert minimum E2E timeout to 20 minutes ([#27827](https://github.com/MetaMask/metamask-extension/pull/27827)) -- fix: disable balance checker for Sepolia in account tracker ([#27763](https://github.com/MetaMask/metamask-extension/pull/27763)) -- ci: Improve validation for `sentry:publish` script ([#26580](https://github.com/MetaMask/metamask-extension/pull/26580)) -- test: Fix Vault Decryptor Page e2e test on develop branch ([#27794](https://github.com/MetaMask/metamask-extension/pull/27794)) -- chore: remove old token details page ([#27774](https://github.com/MetaMask/metamask-extension/pull/27774)) -- chore: remove token list display component ([#27772](https://github.com/MetaMask/metamask-extension/pull/27772)) -- chore: update Trezor Connect to v9.4.0, remove workarounds ([#27112](https://github.com/MetaMask/metamask-extension/pull/27112)) -- test: [POM] Migrate transaction with snap account e2e tests to page object modal ([#27760](https://github.com/MetaMask/metamask-extension/pull/27760)) -- fix(snaps): Restore confirmation switching on routed confirmation ([#27753](https://github.com/MetaMask/metamask-extension/pull/27753)) -- Merge origin/develop into master-sync -- test: Onboarding: Fix vault-decryption-chrome.spec.js ([#27779](https://github.com/MetaMask/metamask-extension/pull/27779)) -- feat: support gas fee flows in standard swaps ([#27612](https://github.com/MetaMask/metamask-extension/pull/27612)) -- feat: Token send heading component ([#27562](https://github.com/MetaMask/metamask-extension/pull/27562)) -- feat: adds the new default settings view to onboarding ([#24562](https://github.com/MetaMask/metamask-extension/pull/24562)) -- chore(3212): remove alert settings ([#27709](https://github.com/MetaMask/metamask-extension/pull/27709)) -- docs: remove outdated Medium link, update "Twitter" to "X" ([#26692](https://github.com/MetaMask/metamask-extension/pull/26692)) -- fix: Replace 'transaction fees' with 'network fees' in the insufficie… ([#27762](https://github.com/MetaMask/metamask-extension/pull/27762)) -- fix: issue with Snap title in Snap Authorship Header ([#27752](https://github.com/MetaMask/metamask-extension/pull/27752)) -- fix: SIWE signature page displays parsed URI instead of domain ([#27754](https://github.com/MetaMask/metamask-extension/pull/27754)) -- fix: updated toasts component and copy ([#27656](https://github.com/MetaMask/metamask-extension/pull/27656)) -- feat: add network picker to AssetPicker ([#26559](https://github.com/MetaMask/metamask-extension/pull/26559)) -- fix(btc): fix jazzicons generations ([#27662](https://github.com/MetaMask/metamask-extension/pull/27662)) -- feat: Release Chain Permissions ([#27561](https://github.com/MetaMask/metamask-extension/pull/27561)) -- feat: upgrade assets-controllers to v38.2.0 ([#27629](https://github.com/MetaMask/metamask-extension/pull/27629)) -- ci: followup to CircleCI Sentry reporting ([#27548](https://github.com/MetaMask/metamask-extension/pull/27548)) -- chore: Master sync ([#27729](https://github.com/MetaMask/metamask-extension/pull/27729)) -- fix(multichain): fix getMultichainCurrentCurrency selector ([#27726](https://github.com/MetaMask/metamask-extension/pull/27726)) -- fix: Limit amount of decimals on spending cap modal ([#27672](https://github.com/MetaMask/metamask-extension/pull/27672)) -- Merge origin/develop into master-sync -- test: [POM] Migrate create snap account e2e tests to page object modal ([#27697](https://github.com/MetaMask/metamask-extension/pull/27697)) -- fix: Prefer token symbol to token name ([#27693](https://github.com/MetaMask/metamask-extension/pull/27693)) -- fix(btc): fetch btc balance right after account creation ([#27628](https://github.com/MetaMask/metamask-extension/pull/27628)) -- fix: UI startup with no Sentry DSN ([#27714](https://github.com/MetaMask/metamask-extension/pull/27714)) -- feat: Sort/Import Tokens in Extension ([#27184](https://github.com/MetaMask/metamask-extension/pull/27184)) -- ci: make git-diff-develop work for PRs from foreign repos ([#27268](https://github.com/MetaMask/metamask-extension/pull/27268)) -- test: Convert json-rpc e2e tests to TypeScript ([#27659](https://github.com/MetaMask/metamask-extension/pull/27659)) -- fix: allow getAddTransactionRequest to pass through other params ([#27117](https://github.com/MetaMask/metamask-extension/pull/27117)) -- perf: add tags to UI startup trace ([#27550](https://github.com/MetaMask/metamask-extension/pull/27550)) -- fix: Disable redirecting Extension users using beta & flask build and dev env to the existing offboarding page ([#27226](https://github.com/MetaMask/metamask-extension/pull/27226)) -- feat(NOTIFY-1193): add profile sync dev menu ([#27666](https://github.com/MetaMask/metamask-extension/pull/27666)) -- refactor: Typescript conversion of log-web3-shim-usage.js ([#23732](https://github.com/MetaMask/metamask-extension/pull/23732)) -- test: removing race condition for asserting inner values (PR-#2) ([#27664](https://github.com/MetaMask/metamask-extension/pull/27664)) -- fix(btc): fix address validation ([#27690](https://github.com/MetaMask/metamask-extension/pull/27690)) -- chore: Update coverage.json ([#27696](https://github.com/MetaMask/metamask-extension/pull/27696)) -- fix: test coverage quality gate ([#27691](https://github.com/MetaMask/metamask-extension/pull/27691)) -- fix: banner alert to render multiple general alerts ([#27339](https://github.com/MetaMask/metamask-extension/pull/27339)) -- refactor: routes constants ([#27078](https://github.com/MetaMask/metamask-extension/pull/27078)) -- fix: Test coverage quality gate ([#27581](https://github.com/MetaMask/metamask-extension/pull/27581)) -- feat: Adding delete metametrics data to security and privacy tab ([#24571](https://github.com/MetaMask/metamask-extension/pull/24571)) -- feat(stx): animations and cosmetic changes to smart transaction status page ([#27650](https://github.com/MetaMask/metamask-extension/pull/27650)) -- build: add lottie-web dependency to extension ([#27632](https://github.com/MetaMask/metamask-extension/pull/27632)) -- fix(btc): do not show percentage for tokens ([#27637](https://github.com/MetaMask/metamask-extension/pull/27637)) -- feat: support Etherscan API keys ([#27611](https://github.com/MetaMask/metamask-extension/pull/27611)) -- feat: change survey timeout time from a week to a day ([#27603](https://github.com/MetaMask/metamask-extension/pull/27603)) -- fix: Design papercuts for redesigned transactions ([#27605](https://github.com/MetaMask/metamask-extension/pull/27605)) -- test: removing race condition for asserting inner values (PR-#1) ([#27606](https://github.com/MetaMask/metamask-extension/pull/27606)) -- test: [POM] Migrate Snap Simple Keyring page and Snap List page to page object modal ([#27327](https://github.com/MetaMask/metamask-extension/pull/27327)) -- fix: fix sentry reading undefined ([#27584](https://github.com/MetaMask/metamask-extension/pull/27584)) -- fix: fix sentry reading null ([#27582](https://github.com/MetaMask/metamask-extension/pull/27582)) -- fix(btc): disable balanceIsCached flag ([#27636](https://github.com/MetaMask/metamask-extension/pull/27636)) -- chore: update accounts related packages ([#27284](https://github.com/MetaMask/metamask-extension/pull/27284)) -- chore: set bridge src network, tokens and top assets ([#26214](https://github.com/MetaMask/metamask-extension/pull/26214)) -- test: [Snaps E2E] add delay to installed snaps test to reduce flaking ([#27521](https://github.com/MetaMask/metamask-extension/pull/27521)) -- chore: set bridge dest network, tokens and top assets ([#26213](https://github.com/MetaMask/metamask-extension/pull/26213)) -- fix: fix reading address from market data ([#27604](https://github.com/MetaMask/metamask-extension/pull/27604)) -- feat: Migrate AccountTrackerController to BaseController v2 ([#27258](https://github.com/MetaMask/metamask-extension/pull/27258)) -- fix: disable transaction data decode if deployment ([#27586](https://github.com/MetaMask/metamask-extension/pull/27586)) -- fix: revert jest collect coverage patterns ([#27583](https://github.com/MetaMask/metamask-extension/pull/27583)) -- fix: add amount row for contract deployment ([#27594](https://github.com/MetaMask/metamask-extension/pull/27594)) -- fix: "Dapp viewed Event @no-mmi is sent when refreshing da..." flaky test ([#27381](https://github.com/MetaMask/metamask-extension/pull/27381)) -- chore: fix deps audit ([#27620](https://github.com/MetaMask/metamask-extension/pull/27620)) -- fix: Max approval and array value spending cap bugs ([#27573](https://github.com/MetaMask/metamask-extension/pull/27573)) -- feat: add power users survey support ([#27361](https://github.com/MetaMask/metamask-extension/pull/27361)) -- fix: Recreate offscreen document if it already exists ([#27596](https://github.com/MetaMask/metamask-extension/pull/27596)) -- fix: flaky test `Block Explorer links to the token tracker in the explorer` ([#27599](https://github.com/MetaMask/metamask-extension/pull/27599)) -- fix(snaps): `Copyable` more button color ([#27600](https://github.com/MetaMask/metamask-extension/pull/27600)) -- fix: flaky test `Import flow allows importing multiple tokens from search` ([#27567](https://github.com/MetaMask/metamask-extension/pull/27567)) -- fix(27428): fix if we type enter anything followed by a \ in settings search ([#27432](https://github.com/MetaMask/metamask-extension/pull/27432)) -- fix: flaky test `Address Book Edit entry in address book` due to race condition with mmi menu ([#27557](https://github.com/MetaMask/metamask-extension/pull/27557)) -- refactor: Typescript conversion of get-provider-state.js ([#23635](https://github.com/MetaMask/metamask-extension/pull/23635)) -- chore: Use "gas_included" event prop ([#27559](https://github.com/MetaMask/metamask-extension/pull/27559)) -- fix: mock locale in unit test ([#27574](https://github.com/MetaMask/metamask-extension/pull/27574)) -- feat: codefence Account Watcher for flask ([#27543](https://github.com/MetaMask/metamask-extension/pull/27543)) -- chore: start upgrade to React Router v6 ([#27185](https://github.com/MetaMask/metamask-extension/pull/27185)) -- fix: AmonHenV2 connection flow incremental permitted chain approval and account address case comparison ([#27518](https://github.com/MetaMask/metamask-extension/pull/27518)) -- fix: flaky test `Backup and Restore should backup the account settings` ([#27565](https://github.com/MetaMask/metamask-extension/pull/27565)) -- fix: Apply flex to Snaps buttons only when containing images and icons ([#27564](https://github.com/MetaMask/metamask-extension/pull/27564)) -- feat: aggregated balance feature ([#27097](https://github.com/MetaMask/metamask-extension/pull/27097)) -- feat: Add redesign integration tests ([#27259](https://github.com/MetaMask/metamask-extension/pull/27259)) -- fix: flaky test `4byte setting does not try to get contract method name from 4byte when the setting is off` ([#27560](https://github.com/MetaMask/metamask-extension/pull/27560)) -- feat: add merge queue ([#26871](https://github.com/MetaMask/metamask-extension/pull/26871)) -- feat: remove squiggle animation from swaps smart transactions ([#27264](https://github.com/MetaMask/metamask-extension/pull/27264)) -- feat: Enable gas included swaps ([#27427](https://github.com/MetaMask/metamask-extension/pull/27427)) -- fix(snaps): Fix custom UI buttons submitting forms ([#27531](https://github.com/MetaMask/metamask-extension/pull/27531)) -- chore: Master sync following v12.3.1 ([#27538](https://github.com/MetaMask/metamask-extension/pull/27538)) -- Merge origin/develop into master-sync -- fix(NOTIFY-1171): account syncing performance and bug fixes ([#27529](https://github.com/MetaMask/metamask-extension/pull/27529)) -- fix: genUnapprovedApproveConfirmation import path ([#27530](https://github.com/MetaMask/metamask-extension/pull/27530)) -- fix(snaps): Keep focus on input if interface re-renders ([#27429](https://github.com/MetaMask/metamask-extension/pull/27429)) -- fix: Allow state updates in Snaps interfaces to state values that are falsy ([#27488](https://github.com/MetaMask/metamask-extension/pull/27488)) -- fix: updated ui for connect and review page ([#27478](https://github.com/MetaMask/metamask-extension/pull/27478)) -- feat: Custom header for wallet initiated confirmations ([#27391](https://github.com/MetaMask/metamask-extension/pull/27391)) -- feat: convert account tracker to typescript ([#27231](https://github.com/MetaMask/metamask-extension/pull/27231)) -- fix: Fix snaps permission connection for `CHAIN_PERMISSIONS` feature flag ([#27459](https://github.com/MetaMask/metamask-extension/pull/27459)) -- fix: flaky test `Navigation Signature - Different signature types initiates multiple signatures and rejects all` ([#27481](https://github.com/MetaMask/metamask-extension/pull/27481)) -- feat: Double Sentry performance trace sample rate ([#27468](https://github.com/MetaMask/metamask-extension/pull/27468)) -- ci: Expand github bot policy update comment to be more actionable ([#27242](https://github.com/MetaMask/metamask-extension/pull/27242)) -- chore: Add `useLedgerConnection` unit tests ([#27358](https://github.com/MetaMask/metamask-extension/pull/27358)) -- ci: Sentry reporting only on develop branch, with Git message overrides ([#27412](https://github.com/MetaMask/metamask-extension/pull/27412)) -- test: Fix flaky permit test ([#27450](https://github.com/MetaMask/metamask-extension/pull/27450)) -- fix: removed closeMenu for ConnectedAccountsMenu ([#27460](https://github.com/MetaMask/metamask-extension/pull/27460)) -- fix(snaps): Set proper text color for secondary button ([#27335](https://github.com/MetaMask/metamask-extension/pull/27335)) -- chore: set bridge selected tokens and amount ([#26212](https://github.com/MetaMask/metamask-extension/pull/26212)) -- fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi`aded ([#27420](https://github.com/MetaMask/metamask-extension/pull/27420)) -- fix: flaky test `Responsive UI Send Transaction from responsive window` ([#27417](https://github.com/MetaMask/metamask-extension/pull/27417)) -- fix: flaky test `Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx should queue send tx after switch network confirmation and transaction should target the correct network after switch is confirmed` ([#27352](https://github.com/MetaMask/metamask-extension/pull/27352)) -- fix: Change speed key color ([#27416](https://github.com/MetaMask/metamask-extension/pull/27416)) -- feat: Display setApprovalForAll and revoke setApprovalForAll to users… ([#27401](https://github.com/MetaMask/metamask-extension/pull/27401)) -- fix: "Warning: Invalid argument supplied to oneOfType" ([#27267](https://github.com/MetaMask/metamask-extension/pull/27267)) -- feat: Editing flow ([#26635](https://github.com/MetaMask/metamask-extension/pull/26635)) -- chore: bump profile-sync-controller to 0.9.3 ([#27415](https://github.com/MetaMask/metamask-extension/pull/27415)) -- fix: Remove duplication ([#27421](https://github.com/MetaMask/metamask-extension/pull/27421)) -- fix: Confirm Page test failing in CI/CD ([#27423](https://github.com/MetaMask/metamask-extension/pull/27423)) -- feat: Display approve, increaseAllowance and revoke approval to users… ([#26985](https://github.com/MetaMask/metamask-extension/pull/26985)) -- feat: Add performance metrics for signature requests ([#26967](https://github.com/MetaMask/metamask-extension/pull/26967)) -- fix: Permit DataTree token decimals ([#27328](https://github.com/MetaMask/metamask-extension/pull/27328)) -- fix: alert system and refine SIWE and contract interaction alerts ([#27205](https://github.com/MetaMask/metamask-extension/pull/27205)) -- fix(NOTIFY-1166): rename account sync event names ([#27413](https://github.com/MetaMask/metamask-extension/pull/27413)) -- feat: ERC20 Revoke Allowance ([#26906](https://github.com/MetaMask/metamask-extension/pull/26906)) + ## [12.5.1] ### Changed - Improve accuracy of transaction simulation warnings in some scenarios ([#26845](https://github.com/MetaMask/metamask-extension/pull/26845)) From 2c9ad97c9017a41dbb322957e36279ec4ab597b9 Mon Sep 17 00:00:00 2001 From: Mark Stacey <markjstacey@gmail.com> Date: Thu, 31 Oct 2024 17:36:52 -0230 Subject: [PATCH 225/226] [cherry pick] Fix left aligned fullscreen (#28218) (#28229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a cherry-pick of #28218 for v12.6.0. Original description: ## **Description** The Home screen was recently updated to make the overview left-aligned. However the fullscreen UI was not considered, and it ended up looking ugly/broken. The fullscreen UI has been updated to be centered, as it was before. The Home screen remains left-aligned in the popup. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28218?quickstart=1) ## **Related issues** Fixes #27593 ## **Manual testing steps** Compare how the Home screen overview looks on the fullscreen UI and the popup. It should be centered on the fullscreen UI, left-aligned on the popup. ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-31 at 11 32 12](https://github.com/user-attachments/assets/989ebd4e-90a5-42ae-a522-f7e4d77f0685) ### **After** ![Screenshot 2024-10-31 at 11 28 35](https://github.com/user-attachments/assets/6802bfab-b462-4168-8536-cabb49aceb53) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/components/app/wallet-overview/index.scss | 8 ++++++++ .../app/wallet-overview/wallet-overview.js | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ui/components/app/wallet-overview/index.scss b/ui/components/app/wallet-overview/index.scss index 4759af1ffa8c..318c26501097 100644 --- a/ui/components/app/wallet-overview/index.scss +++ b/ui/components/app/wallet-overview/index.scss @@ -9,6 +9,10 @@ flex-direction: column; width: 100%; + &-fullscreen { + align-items: center; + } + &__balance { flex: 1; display: flex; @@ -16,6 +20,10 @@ flex-direction: column; align-items: start; width: 100%; + + .wallet-overview-fullscreen > & { + align-items: center; + } } &__icon_button { diff --git a/ui/components/app/wallet-overview/wallet-overview.js b/ui/components/app/wallet-overview/wallet-overview.js index 213a7b2f2317..04127276acaf 100644 --- a/ui/components/app/wallet-overview/wallet-overview.js +++ b/ui/components/app/wallet-overview/wallet-overview.js @@ -2,9 +2,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +// TODO: Move this function to shared +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; + const WalletOverview = ({ balance, buttons, className }) => { return ( - <div className={classnames('wallet-overview', className)}> + <div + className={classnames( + 'wallet-overview', + { + 'wallet-overview-fullscreen': + getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN, + }, + className, + )} + > <div className="wallet-overview__balance">{balance}</div> <div className="wallet-overview__buttons">{buttons}</div> </div> From f6441023cd31e22f49fe02fe29230f3d064b5603 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo <pedro.figueiredo@consensys.net> Date: Thu, 31 Oct 2024 21:49:04 +0000 Subject: [PATCH 226/226] fix: cherry-pick: Prevent coercing small spending caps to zero (#28179) (#28183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick: https://github.com/MetaMask/metamask-extension/pull/28179 <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Previously there was a bug that affected the approve screen. When users had a small spending cap (between 0.001 and 0.0001 or smaller), it was coerced to 0. This was caused by the method `new Intl.NumberFormat(locale).format(spendingCap)` that applied the `1,000` large number formatting, so the fix is to bypass it entirely for values smaller than 1. Additionally, these unformatted small numbers are presented in scientific notation, so we leverage `toNonScientificString(spendingCap)` to prevent that. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28179?quickstart=1) ## **Related issues** Fixes: [#28117](https://github.com/MetaMask/metamask-extension/issues/28117) ## **Manual testing steps** See original bug report. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28183?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. --- .../use-approve-token-simulation.test.ts | 68 ++++++++++++++++++- .../hooks/use-approve-token-simulation.ts | 14 +++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts index 4173d21910c5..0178e2ffff62 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts @@ -65,7 +65,7 @@ describe('useApproveTokenSimulation', () => { expect(result.current).toMatchInlineSnapshot(` { - "formattedSpendingCap": 7, + "formattedSpendingCap": "7", "pending": undefined, "spendingCap": "#7", "value": { @@ -155,4 +155,70 @@ describe('useApproveTokenSimulation', () => { } `); }); + + it('returns correct small decimal number token amount for fungible tokens', async () => { + const useIsNFTMock = jest.fn().mockImplementation(() => ({ isNFT: false })); + + const useDecodedTransactionDataMock = jest.fn().mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: 'approve', + params: [ + { + type: 'address', + value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', + }, + { + type: 'uint256', + value: 10 ** 5, + }, + ], + }, + ], + source: 'FourByte', + }, + })); + + (useIsNFT as jest.Mock).mockImplementation(useIsNFTMock); + (useDecodedTransactionData as jest.Mock).mockImplementation( + useDecodedTransactionDataMock, + ); + + const transactionMeta = genUnapprovedContractInteractionConfirmation({ + address: CONTRACT_INTERACTION_SENDER_ADDRESS, + }) as TransactionMeta; + + const { result } = renderHookWithProvider( + () => useApproveTokenSimulation(transactionMeta, '18'), + mockState, + ); + + expect(result.current).toMatchInlineSnapshot(` + { + "formattedSpendingCap": "0.0000000000001", + "pending": undefined, + "spendingCap": "0.0000000000001", + "value": { + "data": [ + { + "name": "approve", + "params": [ + { + "type": "address", + "value": "0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4", + }, + { + "type": "uint256", + "value": 100000, + }, + ], + }, + ], + "source": "FourByte", + }, + } + `); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts index 19f26c9c9300..8d938a12c461 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts @@ -15,6 +15,15 @@ function isSpendingCapUnlimited(decodedSpendingCap: number) { return decodedSpendingCap >= UNLIMITED_THRESHOLD; } +function toNonScientificString(num: number): string { + if (num >= 10e-18) { + return num.toFixed(18).replace(/\.?0+$/u, ''); + } + + // keep in scientific notation + return num.toString(); +} + export const useApproveTokenSimulation = ( transactionMeta: TransactionMeta, decimals: string, @@ -46,8 +55,9 @@ export const useApproveTokenSimulation = ( }, [value, decimals]); const formattedSpendingCap = useMemo(() => { - return isNFT - ? decodedSpendingCap + // formatting coerces small numbers to 0 + return isNFT || decodedSpendingCap < 1 + ? toNonScientificString(decodedSpendingCap) : new Intl.NumberFormat(locale).format(decodedSpendingCap); }, [decodedSpendingCap, isNFT, locale]);